如何在不将IQueryable暴露给我的应用程序的其余部分的情况下编写一个干净的存储库?
-
06-07-2019 - |
题
所以,我已经在SO上阅读了所有Q& A关于是否将IQueryable暴露给你项目的其余部分的主题(参见 here , here ),我最终决定我不想将IQueryable暴露给除了我的模型以外的任何东西。因为IQueryable与某些持久性实现有关,所以我不喜欢将自己锁定在其中的想法。同样地,我不确定我对调用链中的类进一步修改不在存储库中的实际查询的感觉有多好。
那么,有没有人建议如何编写一个干净简洁的存储库而不这样做?我看到的一个问题是,我的存储库会从大量的方法中迸发出来,以便我可以过滤掉我的查询。
有一堆:
IEnumerable GetProductsSinceDate(DateTime date);
IEnumberable GetProductsByName(string name);
IEnumberable GetProductsByID(int ID);
如果我允许传递IQueryable,我可以很容易地拥有一个看起来像这样的通用存储库:
public interface IRepository<T> where T : class
{
T GetById(int id);
IQueryable<T> GetAll();
void InsertOnSubmit(T entity);
void DeleteOnSubmit(T entity);
void SubmitChanges();
}
但是,如果你没有使用IQueryable,那么像GetAll()这样的方法并不实用,因为延迟评估不会发生。我不想只返回10,000条记录,以后再使用其中的10条。
这里的答案是什么?在 Conery的MVC店面中,他创建了另一个名为“服务”的层。从存储库接收IQueryable结果并负责应用各种过滤器的图层。
这是我应该做的,或类似的东西?让我的存储库返回IQueryable但是通过将它隐藏在一堆过滤器类(如GetProductByName)后限制对它的访问,这将返回像IList或IEnumerable这样的具体类型吗?
解决方案
公开 IQueryable
是一个非常可行的解决方案,这就是现在大多数Repository实现的方法。 (包括SharpArchitecture和FubuMVC contrib。)
这是你错的地方:
但是,如果您不使用 IQueryable然后像GetAll()的方法 从懒惰开始并不真实 评估不会发生 这条线。我不想回来 10,000条记录仅使用10条记录 后面。
这不是真的。您的示例是正确的,您应该将GetAll()重命名为更具信息性的名称。
如果你打电话,它不会返回所有项目。这就是IQueryable的用途。该概念称为“延迟加载”,因为它只在您枚举 IQueryable
时加载数据(并产生数据库请求)。
所以,假设我有一个这样的方法:
IQueryable<T> Retrieve() { ... }
然后,我可以这样称呼它:
Repository.Retrieve<Customer>().Single(c => c.ID == myID);
此ONLY从数据库中检索一行。
而且:
Repository.Retrieve<Customer>().Where(c => c.FirstName == "Joe").OrderBy(c => c.LastName);
这也会生成相应的查询,并且仅在枚举时执行。 (它从查询中生成表达式树,然后查询提供程序应将其转换为针对数据源的适当查询。)
您可以在此MSDN文章中了解更多相关信息 / A>
其他提示
Rob的方法确实无法解决您的核心问题,并且不想为您想要运行的每种类型的查询编写单独的方法,不幸的是,如果您不使用IQueryable,那就是您的意思离开了。
确定方法可能在“服务”中。图层,但它仍然意味着必须写“GetProductsByName,GetProductsByDate”......
另一种方法是:
GetProducts(QueryObject);
这可能会比使用IQueryable带来一些好处,因为你可以限制返回的内容。
嗯..我在很多方面解决了这个问题,具体取决于我使用的ORM类型。
主要思想是拥有一个存储库基类和一个查询方法,该方法采用如此多的参数来指示所有可能的/ orderby / expand | include / paging / etc选项。
这是一个使用LINQ to NHibernate的快速而脏的示例(当然整个存储库应该是实现细节):
public class RepositoryBase
{
private ISession Session;
public RepositoryBase()
{
Session = SessionPlaceHolder.Session;
}
public TEntity[] GetPaged<TEntity>(IEnumerable<Expression<Func<TEntity, bool>>> filters,
IEnumerable<Expression<Func<TEntity, object>>> relatedObjects,
IEnumerable<Expression<Func<TEntity, object>>> orderCriterias,
IEnumerable<Expression<Func<TEntity, object>>> descOrderCriterias,
int pageNumber, int pageSize, out int totalPages)
{
INHibernateQueryable<TEntity> nhQuery = Session.Linq<TEntity>();
if (relatedObjects != null)
foreach (var relatedObject in relatedObjects)
{
if (relatedObject == null) continue;
nhQuery = nhQuery.Expand(relatedObject);
}
IQueryable<TEntity> query = nhQuery;
if (filters != null)
foreach (var filter in filters)
{
if (filter == null) continue;
query = query.Where(filter);
}
bool pagingEnabled = pageSize > 0;
if (pagingEnabled)
totalPages = (int) Math.Ceiling((decimal) query.Count()/(decimal) pageSize);
else
totalPages = 1;
if (orderCriterias != null)
foreach (var orderCriteria in orderCriterias)
{
if (orderCriteria == null) continue;
query = query.OrderBy(orderCriteria);
}
if (descOrderCriterias != null)
foreach (var descOrderCriteria in descOrderCriterias)
{
if (descOrderCriteria == null) continue;
query = query.OrderByDescending(descOrderCriteria);
}
if (pagingEnabled)
query = query.Skip(pageSize*(pageNumber - 1)).Take(pageSize);
return query.ToArray();
}
}
通常情况下,当你不需要分页等时,你会想要添加许多链接重载作为快捷方式。
这是另一个脏的。对不起,我不确定我是否可以揭露最后的那些。这些都是草稿,可以展示:
using Context = Project.Services.Repositories.EntityFrameworkContext;
using EntitiesContext = Project.Domain.DomainSpecificEntitiesContext;
namespace Project.Services.Repositories
{
public class EntityFrameworkRepository : IRepository
{
#region IRepository Members
public bool TryFindOne<T>(Expression<Func<T, bool>> filter, out T result)
{
result = Find(filter, null).FirstOrDefault();
return !Equals(result, default(T));
}
public T FindOne<T>(Expression<Func<T, bool>> filter)
{
T result;
if (TryFindOne(filter, out result))
return result;
return default(T);
}
public IList<T> Find<T>() where T : class, IEntityWithKey
{
int count;
return new List<T>(Find<T>(null, null, 0, 0, out count));
}
public IList<T> Find<T>(Expression<Func<T, bool>> filter, Expression<Func<T, object>> sort)
{
int count;
return new List<T>(Find(filter, sort, 0, 0, out count));
}
public IEnumerable<T> Find<T>(Expression<Func<T, bool>> filter, Expression<Func<T, object>> sort, int pageSize,
int pageNumber, out int count)
{
return ExecuteQuery(filter, sort, pageSize, pageNumber, out count) ?? new T[] {};
}
public bool Save<T>(T entity)
{
var contextSource = new EntityFrameworkContext();
EntitiesContext context = contextSource.Context;
EntityKey key = context.CreateEntityKey(GetEntitySetName(entity.GetType()), entity);
object originalItem;
if (context.TryGetObjectByKey(key, out originalItem))
{
context.ApplyPropertyChanges(key.EntitySetName, entity);
}
else
{
context.AddObject(GetEntitySetName(entity.GetType()), entity);
//Attach(context, entity);
}
return context.SaveChanges() > 0;
}
public bool Delete<T>(Expression<Func<T, bool>> filter)
{
var contextSource = new EntityFrameworkContext();
EntitiesContext context = contextSource.Context;
int numberOfObjectsFound = 0;
foreach (T entity in context.CreateQuery<T>(GetEntitySetName(typeof (T))).Where(filter))
{
context.DeleteObject(entity);
++numberOfObjectsFound;
}
return context.SaveChanges() >= numberOfObjectsFound;
}
#endregion
protected IEnumerable<T> ExecuteQuery<T>(Expression<Func<T, bool>> filter, Expression<Func<T, object>> sort,
int pageSize, int pageNumber,
out int count)
{
IEnumerable<T> result;
var contextSource = new EntityFrameworkContext();
EntitiesContext context = contextSource.Context;
ObjectQuery<T> originalQuery = CreateQuery<T>(context);
IQueryable<T> query = originalQuery;
if (filter != null)
query = query.Where(filter);
if (sort != null)
query = query.OrderBy(sort);
if (pageSize > 0)
{
int pageIndex = pageNumber > 0 ? pageNumber - 1 : 0;
query = query.Skip(pageIndex).Take(pageSize);
count = query.Count();
}
else
count = -1;
result = ExecuteQuery(context, query);
//if no paging total count is count of the entire result set
if (count == -1) count = result.Count();
return result;
}
protected internal event Action<ObjectContext, IEnumerable> EntitiesFound;
protected void OnEntitiesFound<T>(ObjectContext context, params T[] entities)
{
if (EntitiesFound != null && entities != null && entities.Length > 0)
{
EntitiesFound(context, entities);
}
}
//Allowing room for system-specific-requirement extensibility
protected Action<IEnumerable> ItemsFound;
protected IEnumerable<T> ExecuteQuery<T>(ObjectContext context, IQueryable<T> query)
{
IEnumerable<T> result = null;
if (query is ObjectQuery)
{
var objectQuery = (ObjectQuery<T>) query;
objectQuery.EnablePlanCaching = false;
objectQuery.MergeOption = MergeOption.PreserveChanges;
result = new List<T>(objectQuery);
if (ItemsFound != null)
ItemsFound(result);
return result;
}
return result;
}
internal static RelationshipManager GetRelationshipManager(object entity)
{
var entityWithRelationships = entity as IEntityWithRelationships;
if (entityWithRelationships != null)
{
return entityWithRelationships.RelationshipManager;
}
return null;
}
protected ObjectQuery<T> CreateQuery<T>(ObjectContext context)
{
ObjectQuery<T> query = context.CreateQuery<T>(GetEntitySetName(typeof (T)));
query = this.AggregateEntities(query);
return query;
}
protected virtual ObjectQuery<T> AggregateEntities<T>(ObjectQuery<T> query)
{
return query;
}
private static string GetEntitySetName(Type entityType)
{
return string.Format("{0}Set", entityType.Name);
}
}
public class EntityFrameworkContext
{
private const string CtxKey = "ctx";
private bool contextInitialized
{
get { return HttpContext.Current.Items[CtxKey] != null; }
}
public EntitiesContext Context
{
get
{
if (contextInitialized == false)
{
HttpContext.Current.Items[CtxKey] = new EntitiesContext(ConfigurationManager.ConnectionStrings["CoonectionStringName"].ToString());
}
return (EntitiesContext)HttpContext.Current.Items[CtxKey];
}
}
public void TrulyDispose()
{
if (contextInitialized)
{
Context.Dispose();
HttpContext.Current.Items[CtxKey] = null;
}
}
}
internal static class EntityFrameworkExtensions
{
internal static ObjectQuery<T> Include<T>(this ObjectQuery<T> query,
Expression<Func<T, object>> propertyToInclude)
{
string include = string.Join(".", propertyToInclude.Body.ToString().Split('.').Skip(1).ToArray());
const string collectionsLinqProxy = ".First()";
include = include.Replace(collectionsLinqProxy, "");
return query.Include(include);
}
internal static string After(this string original, string search)
{
if (string.IsNullOrEmpty(original))
return string.Empty;
int index = original.IndexOf(search);
return original.Substring(index + search.Length);
}
}
}
在Conery的MVC店面中他创造了 另一层称为“服务” 收到IQueryable的图层 来自资源库的结果 负责申请各种 过滤器。
在所有情况下,除服务层外,没有人应该直接与存储库进行交互。
最灵活的事情是让服务以任何他们想要的方式与Repository交互,就像在上面的代码中一样(但是通过一个单点 - 例如也是 - 编写DRY代码并找到优化的地方)。
然而,就普通DDD模式而言,更正确的方式是使用“规范”。模式,您将所有过滤器等封装在变量(类成员,LINQ通常是委托类型)中。当您将“组合查询”与“编译查询”结合使用时,LINQ可以从中获得很大的优化效益。如果你谷歌{规范模式}和{LINQ编译查询}你会更接近我的意思。
我最终创建了两组方法,一组返回IEnumerable(在你的情况下是IQueryable),另一组返回Collection(在将内容发送出存储库之前拉取内容。)
这允许我在存储库外的服务中构建即席查询,并使用Repository方法直接返回防止副作用的集合。换句话说,将两个存储库实体连接在一起会产生一个选择查询,而不是找到每个实体的一个选择查询。
我想你可以设置你的保护级别,以防止真正发生的事情发生。
我自己努力找到解决这个问题的可行方案,但在在一个实现存储库和工作单元模式中ASP.NET MVC应用程序(9/10)文章。
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}
本文没有讨论这个确切的问题,但确实讨论了通用的,可重用的存储库方法。
到目前为止,这是我能够提出的所有解决方案。