Работа с запросами в шаблоне репозитория с несколькими конкретными реализациями?

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

Представьте ситуацию, когда у вас есть объект Person

public class Person {
    public string Name {get;set;}
    public int Age {get;set;}
}

и Контракт репозитория для извлечения их из некоторого хранилища сохраняемости...

public class IPersonRepository {
    public IEnumerable<Person> Search(*** SOME_METHOD_SIGNATURE ***);
}

Ваше потребительское приложение действительно не заботится о конкретной реализации. Он просто возьмет правильную конкретную реализацию от Unity/Ninject и начнет запрос.

IPersonRespository repo = GetConcreteImplementationFromConfig();
repo.Search( ... );

Что мне интересно, так это то, что вы использовали бы для своей Подписи Метода здесь, которые являются гибкими и расширяемыми независимо от реализации.

Вариант 1.

public IEnumerable<Person> Search(Expression<Func<Person, bool>> expression);

Это хорошо, потому что, если вы используете контекст данных LINQ Capable (например, EntityFramework), вы можете просто передать это выражение непосредственно в свой контекст. Этот параметр, похоже, падает, хотя, если вы выполняете реализацию, необходимо использовать созданные вручную procs/sql/ADO.NET и т.д....

Вариант 2.

public IEnumerable<Person> Search(PersonSearch parameters);

public class PersonSearch {
    public int? Age {get;set;}
    public string FullName {get;set;}
    public string PartialName { get; set; }
}

Это кажется наиболее гибким (в том смысле, что он будет работать с Linq, Plain Old SQL.

Но он просто воняет "Написание собственного языка запросов", потому что вам нужно учитывать все возможные запросы, которые потребитель может захотеть сделать. например Возраст >= 18 && Возраст <= 65 && Имя LIKE '% John%'

Вариант 3.

Есть ли другие варианты?

Ответ 1

  • Ваше утверждение о том, что "вариант 1, похоже, падает, хотя, если вы выполняете реализацию, необходимо использовать обработанные вручную обработчики proc/sql/ADO.NET и т.д.". Можно полностью создать модель, аналогичную модели в варианте 2, путем интерпретации выражения запроса с реализацией ExpressionVisitor.

  • Если вы собираетесь вернуть IEnumerable<T> вместо IQueryable<T> из метода LINQ Search, вы должны включить некоторый механизм для поддержки (1) подкачки и (2) сортировка.

  • Мне действительно не нравится вариант №2. Он не делает явным то, что вы ищете. например Если возраст равен нулю, означает ли это, что вы ищете пользователей с нулевым возрастом или игнорируете параметр возраста? Что, если указано имя и PartialName? С помощью подхода LINQ вы можете делать такие вещи, как StartsWith, Contains, Equals и т.д.

  • Реализация шаблона репозитория должна абстрагироваться от логики доступа к данным и выставлять бизнес-ориентированный интерфейс. Прямой доступ к репозиторию с помощью общего интерфейса (Expression<Func<Person,bool>>), вы немного теряете это, потому что интерфейс не передает намерение вызывающему. Есть несколько способов сделать это лучше.

    • Реализация LINQ-выражения Спецификация шаблона создает более строго типизированные запросы. Таким образом, вместо запроса для взрослых с помощью Search(person => person.Age.HasValue && person.Age.Value > 18) вы должны использовать синтаксис спецификации, например Search(new PersonIsAdultSpecification());, где спецификация обертывает базовое выражение LINQ, но предоставляет бизнес-ориентированный интерфейс.
      • Лично мне нравится этот подход, но Айенде называет его "архитектором в яме гибели" , потому что он может легко привести к чрезмерному, машиностроение. Его альтернативное предложение состоит в том, чтобы обернуть определенные запросы, подобные этому, в методах расширения. Я думаю, что это, вероятно, одинаково жизнеспособно, но я предпочитаю иметь сильно типизированный объект.
      • Самый простой способ сделать это - фактически объявить реалистичные запросы, которые вы будете выполнять в интерфейсе IPersonRepository. Таким образом, интерфейс фактически объявит метод SearchForAdults().

Обычно. Всякий раз, когда вы запрашиваете базу данных, вы должны сознательно пытаться получить определенные данные. Всякий раз, когда вы запрашиваете репозиторий, вы должны сознательно пытаться получить бизнес-объекты, которые удовлетворяют определенному бизнес-ограничению. Почему бы не сделать это ограничение бизнеса более явным? Определение хорошего интерфейса для любой услуги зависит от потребителя этой услуги. Существует не общий интерфейс репозитория волшебной пули, но вы можете создать лучший или худший в контексте вашего конкретного приложения. Идея состоит в том, чтобы помнить, почему вы используете шаблон репозитория в первую очередь, и это упрощает логику и создает более интуитивную точку доступа.

Ответ 2

Я не уверен, что в этом случае есть что-то вроде "официального источника", но я лидировал в разработке LINQ для Visual Basic, поэтому, возможно, это что-то значит. В любом случае, здесь я беру на себя:

Проблема, которую вы, кажется, ставите, заключается в том, что вы хотите иметь возможность передавать предикатное выражение на бэкэнд "независимо от реализации [backend]". Тем не менее, мне кажется, что два варианта, которые вы перечисляете, действительно являются просто вариантами друг друга, поскольку в обоих случаях бэкэнд должен уметь понимать спецификацию предиката, которая передается. Вариант один оказывается более общим и гибким чем второй вариант (но это также намного больнее переводить). Вариант второй более ограничен, чем второй вариант, но является более простой структурой, с которой приходится иметь дело (хотя, поскольку вы позволяете задавать более сложные запросы во втором варианте, структура неизбежно будет все больше напоминать структуру из первого варианта).

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

С другой стороны, если (как это более вероятно) список возможных поставщиков, которых вы получите, будет конечным, вам, вероятно, будет лучше решать, что для вас наиболее важно (простота использования/выразительность и непринужденность реализации) и выберите, какие ограничения вы хотите наложить на поставщиков. Деревья выражений, безусловно, были бы самыми приятными во многих отношениях для программиста для потребительских приложений, поэтому, если вы думаете, что можете избавиться от этого ограничения, пойдите для этого. В противном случае какая-то более простая пользовательская структура, вероятно, вызовет у вас меньше боли и душевной боли. (Я не рекомендую переводить деревья выражений самостоятельно, если у вас нет большого времени и терпения.)

Ответ 3

Одна вещь, которая всегда беспокоила меня по шаблону репозитория, - это негибкость, когда дело доходит до таких вещей, как группировка, агрегирование подсчетов и т.д. Вот почему я решил также предоставить отдельный интерфейс (например, IExtendedRepository), который поставляется с сигнатурой метода funcy следующим образом:

TResult Aggregate<TResult>(Func<IQueryable<TEntity>, TResult> aggregatorFunc);

Этот метод представляет собой немного монстра, который можно использовать многими опрятными способами:

var countSomething = repository.Aggregate(x => x.Where(y => y.SomeProperty).Count());

Обратите внимание, что (в зависимости от реализации репозитория) вызов Count() будет скомпилирован в счетчик SQL, поэтому он не будет в памяти Count().