Как я могу написать чистый репозиторий, не подвергая IQueryable остальной части моего приложения?

StackOverflow https://stackoverflow.com/questions/1030992

Вопрос

Итак, я прочитал все вопросы и ответы по 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 из них позже.

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

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

Это было полезно?

Решение

Предоставление IQueryable является очень жизнеспособным решением, и именно так большинство реализаций репозитория делают это прямо сейчас. (Включая также 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 .

Другие советы

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

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

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top