Как я могу написать чистый репозиторий, не подвергая IQueryable остальной части моего приложения? - PullRequest
19 голосов
/ 23 июня 2009

Итак, я прочитал все вопросы и ответы здесь на 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 (), на самом деле не практичны, поскольку отложенная оценка не будет выполняться в дальнейшем. Я не хочу возвращать 10000 записей, чтобы использовать 10 из них позже.

Какой ответ здесь? В витрине MVC Store Conery он создал еще один слой, называемый слоем «Сервис», который получал результаты IQueryable из репозитория и отвечал за применение различных фильтров.

Это то, что я должен делать, или что-то подобное? Мой репозиторий возвращает IQueryable, но ограничивает доступ к нему, скрывая его за группой классов фильтров, таких как GetProductByName, которые будут возвращать конкретный тип, такой как IList или IEnumerable?

Ответы [ 5 ]

5 голосов
/ 03 мая 2010

Предоставление IQueryable является очень жизнеспособным решением, и именно так большинство реализаций Repository делают прямо сейчас. (Включая также SharpArchitecture и FubuMVC contrib.)

Здесь вы ошибаетесь:

Однако, если вы не используете IQueryable тогда методы, такие как GetAll () не очень практично, так как ленивый оценка не будет проходить вниз линия. Я не хочу возвращаться 10000 записей только для использования 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 .

3 голосов
/ 17 ноября 2009

хмм .. Я решил это разными способами в зависимости от типа 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);
        }
    }
}

В витрине MVC Конери он создал другой слой называется "Сервис" слой, который получил IQueryable Результаты из репозитория и был ответственность за применение различных фильтры.

Во всех случаях никто не должен взаимодействовать с хранилищем напрямую, кроме уровня служб.

Самая гибкая вещь - позволить Службам взаимодействовать с Репозиторием любым способом, каким они захотят, так же, как в приведенном выше коде (хотя и через одну единственную точку - как, например, также - для написания DRY-кода и поиска места для оптимизации). Однако с точки зрения общих шаблонов DDD более правильным является использование шаблона «Спецификация», в котором вы инкапсулируете все свои фильтры и т. Д. В переменные (члены класса, в LINQ обычно имеют типы делегатов). LINQ может извлечь большую выгоду из оптимизации, если вы объедините его с «Скомпилированными запросами». Если вы воспользуетесь Google {Specification Pattern} и {LINQ Compiled Queries}, вы поймете, что я имею в виду.

3 голосов
/ 23 июня 2009

Метод Роба действительно не решает вашу основную проблему, и он не хочет писать отдельные методы для каждого типа запроса, который вы хотите выполнить, и, к сожалению, если вы не используете IQueryable, то это то, что вы осталось с.

Конечно, методы могут быть на уровне "service", но это все равно означает, что нужно написать "GetProductsByName, GetProductsByDate" ...

Другой метод - что-то вроде:

GetProducts(QueryObject);

Это может дать вам некоторое преимущество по сравнению с использованием IQueryable, поскольку вы можете ограничить возвращаемое значение.

0 голосов
/ 21 октября 2013

Изо всех сил пытаясь найти жизнеспособное решение этой проблемы, есть то, что кажется хорошим решением в Реализация шаблонов репозитория и единицы работы в приложении 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();
        }
    }

В статье не говорится об этой конкретной проблеме, но говорится об общих, многократно используемых методах хранилища.

Пока это все, что я смог найти в качестве решения.

0 голосов
/ 31 октября 2010

Я закончил тем, что создал два набора методов, один из которых возвращает IEnumerable (в вашем случае IQueryable), и тот, который возвращает Collection (извлекает содержимое перед отправкой из хранилища.)

Это позволяет мне как создавать специальные запросы в Службах вне репозитория, так и использовать методы Репозитория, напрямую возвращающие устойчивые к побочным эффектам Коллекции. Другими словами, объединение двух объектов репозитория приводит к одному запросу выбора вместо одного запроса выбора для каждой найденной сущности.

Я полагаю, вы могли бы установить уровень защиты, чтобы предотвратить действительно плохие вещи.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...