19

所以,我已经阅读了关于 SO 的所有问答,关于是否将 IQueryable 暴露给你的项目的其余部分(见这里这里),我最终决定我不想要将 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 Storefront中,他创建了另一个称为“服务”层的层,该层从存储库接收 IQueryable 结果并负责应用各种过滤器。

这是我应该做的,还是类似的事情?让我的存储库返回 IQueryable,但通过将其隐藏在一堆过滤器类(如 GetProductByName)后面来限制对它的访问,这些过滤器类将返回像 IList 或 IEnumerable 这样的具体类型?

4

5 回答 5

5

公开 anIQueryable是一个非常可行的解决方案,这就是目前大多数 Repository 实现的方式。(包括 SharpArchitecture 和 FubuMVC contrib,以及。)

这是你错的地方:

但是,如果您不使用 IQueryable,则 GetAll() 之类的方法实际上并不实用,因为延迟评估将不会发生。我不想返回 10,000 条记录,以后只使用其中的 10 条。

这不是真的。您的示例是正确的,您应该将 GetAll() 重命名为更具信息性的名称。

如果您调用它,它不会返回所有项目。这就是 IQueryable 的用途。该概念称为“延迟加载”,因为它仅在您枚举IQueryable.

所以,假设我有一个这样的方法:

IQueryable<T> Retrieve() { ... }

然后,我可以这样称呼它:

Repository.Retrieve<Customer>().Single(c => c.ID == myID);

这只从数据库中检索一行。

还有这个:

Repository.Retrieve<Customer>().Where(c => c.FirstName == "Joe").OrderBy(c => c.LastName);

这也会生成相应的查询,并且仅在您枚举时执行。(它从查询生成一个表达式树,然后查询提供程序应将其转换为针对数据源的适当查询。)

您可以在这篇 MSDN 文章中阅读更多关于它的信息。

于 2010-05-02T22:13:25.137 回答
3

Rob 的方法确实不能解决您的核心问题,即不想为您要运行的每种查询类型编写单独的方法,不幸的是,如果您不使用 IQueryable,那么这就是您剩下的。

当然这些方法可能在“服务”层中,但这仍然意味着必须编写“GetProductsByName,GetProductsByDate”......

另一种方法是这样的:

GetProducts(QueryObject);

与使用 IQueryable 相比,这可能会给您带来一些好处,因为您可以限制返回的内容。

于 2009-06-23T08:19:13.873 回答
3

嗯.. 根据我使用的 ORM 的类型,我以多种方式解决了这个问题。
主要思想是拥有一个存储库基类和一个查询方法,该方法采用许多参数来指示所有可能的 where/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 Storefront 中,他创建了另一个称为“服务”层的层,该层从存储库接收 IQueryable 结果并负责应用各种过滤器。

在所有情况下,除了服务层之外,没有人应该直接与存储库进行交互。

最灵活的事情是让服务以任何他们想要的方式与存储库交互,就像上面的代码一样(但通过一个点 - 就像示例中一样 - 编写 DRY 代码并找到优化的地方)。
但是,就常见的 DDD 模式而言,更正确的方法是使用“规范”模式,将所有过滤器等封装在变量中(类成员,在 LINQ 中通常是委托类型)。当您将 LINQ 与“已编译查询”结合使用时,LINQ 可以从中获得很大的优化优势。如果你用谷歌搜索 {Specification Pattern} 和 {LINQ Compiled Queries},你会更接近我的意思。

于 2009-11-17T11:09:46.143 回答
0

我最终创建了两组方法,一组返回 IEnumerable(在您的情况下为 IQueryable),另一组返回 Collection(在将内容发送出存储库之前提取内容。)

这使我既可以在存储库之外的服务中构建临时查询,也可以使用存储库方法直接返回抗副作用的集合。换句话说,将两个存储库实体连接在一起会产生一个选择查询,而不是针对找到的每个实体进行一个选择查询。

我想你可以设置你的保护级别来防止真正的坏事发生。

于 2010-10-31T18:20:26.393 回答
0

在自己努力寻找解决此问题的可行解决方案后,在 ASP.NET MVC 应用程序中实现存储库和工作单元模式 (9 of 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();
        }
    }

这篇文章没有讨论这个确切的问题,而是讨论了通用的、可重用的存储库方法。

到目前为止,这就是我能够提出的所有解决方案。

于 2013-10-21T11:44:08.047 回答