Я довольно долго искал хорошее решение проблем, представленных типичным шаблоном репозитория (растущий список методов для специализированных запросов и т. Д. См .: http://ayende.com/blog/3955/repository- это новый синглтон ).
Мне очень нравится идея использования командных запросов, особенно с использованием шаблона спецификации. Однако моя проблема со спецификацией заключается в том, что она относится только к критериям простого выбора (в основном, предложение where) и не касается других вопросов запросов, таких как объединение, группировка, выбор подмножества или проекция и т. Д. в основном, все лишние обручи, которые должны пройти многие запросы, чтобы получить правильный набор данных.
(примечание: я использую термин «команда», как в шаблоне «Команда», также известном как объекты запроса. Я не говорю о команде, как в разделении команд / запросов, где существует различие между запросами и командами (обновление, удаление, вставить))
Поэтому я ищу альтернативы, которые инкапсулируют весь запрос, но при этом достаточно гибкие, чтобы вы не просто меняли местами спагетти-репозитории для взрыва класса команд.
Я использовал, например, Linqspecs, и хотя я нахожу некоторую ценность в возможности назначать значимые имена критериям выбора, этого просто недостаточно. Возможно, я ищу смешанное решение, сочетающее в себе несколько подходов.
Я ищу решения, которые могли быть разработаны другими для решения этой проблемы или решения другой проблемы, но все же удовлетворяющие этим требованиям. В связанной статье Айенде предлагает напрямую использовать контекст nHibernate, но я считаю, что это значительно усложняет ваш бизнес-уровень, поскольку теперь он также должен содержать информацию о запросах.
Я предложу вознаграждение за это, как только истечет период ожидания. Так что, пожалуйста, сделайте ваши решения достойными награды, с хорошими объяснениями, и я выберу лучшее решение и проголосую за участников.
ПРИМЕЧАНИЕ: я ищу что-то, основанное на ORM. Не обязательно явно указывать EF или nHibernate, но они являются наиболее распространенными и подходят лучше всего. Если его можно легко адаптировать к другим ORM, это будет бонусом. Совместимость с Linq тоже была бы неплоха.
ОБНОВЛЕНИЕ: я действительно удивлен, что здесь не так много хороших предложений. Кажется, что люди либо полностью относятся к CQRS, либо полностью принадлежат к лагерю репозитория. Большинство моих приложений недостаточно сложны, чтобы гарантировать использование CQRS (большинство сторонников CQRS с готовностью говорят, что вам не следует его использовать).
ОБНОВЛЕНИЕ: здесь есть небольшая путаница. Я ищу не новую технологию доступа к данным, а достаточно хорошо продуманный интерфейс между бизнесом и данными.
В идеале я ищу нечто среднее между объектами запроса, шаблоном спецификации и репозиторием. Как я уже сказал выше, шаблон спецификации имеет дело только с аспектом предложения where, а не с другими аспектами запроса, такими как соединения, подвыборки и т. Д. Репозитории обрабатывают весь запрос, но через некоторое время выходят из-под контроля . Объекты запроса также обрабатывают весь запрос, но я не хочу просто заменять репозитории взрывами объектов запроса.
источник
Ответы:
Отказ от ответственности: поскольку пока нет хороших ответов, я решил опубликовать часть из отличного сообщения в блоге, которое я прочитал некоторое время назад, скопированный почти дословно. Вы можете найти полную запись в блоге здесь . Итак, вот оно:
Мы можем определить следующие два интерфейса:
public interface IQuery<TResult> { } public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult> { TResult Handle(TQuery query); }
В
IQuery<TResult>
определяет сообщение , которое определяет конкретный запрос с данными возвращаемой с помощьюTResult
универсального типа. С помощью ранее определенного интерфейса мы можем определить сообщение запроса следующим образом:public class FindUsersBySearchTextQuery : IQuery<User[]> { public string SearchText { get; set; } public bool IncludeInactiveUsers { get; set; } }
Этот класс определяет операцию запроса с двумя параметрами, результатом которой будет массив
User
объектов. Класс, обрабатывающий это сообщение, можно определить следующим образом:public class FindUsersBySearchTextQueryHandler : IQueryHandler<FindUsersBySearchTextQuery, User[]> { private readonly NorthwindUnitOfWork db; public FindUsersBySearchTextQueryHandler(NorthwindUnitOfWork db) { this.db = db; } public User[] Handle(FindUsersBySearchTextQuery query) { return db.Users.Where(x => x.Name.Contains(query.SearchText)).ToArray(); } }
Теперь мы можем позволить потребителям зависеть от универсального
IQueryHandler
интерфейса:public class UserController : Controller { IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler; public UserController( IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler) { this.findUsersBySearchTextHandler = findUsersBySearchTextHandler; } public View SearchUsers(string searchString) { var query = new FindUsersBySearchTextQuery { SearchText = searchString, IncludeInactiveUsers = false }; User[] users = this.findUsersBySearchTextHandler.Handle(query); return View(users); } }
Эта модель сразу же дает нам большую гибкость, потому что теперь мы можем решить, что вводить в
UserController
. Мы можем внедрить совершенно другую реализацию или ту, которая обертывает реальную реализацию, без необходимости вносить изменения вUserController
(и всех других потребителей этого интерфейса).IQuery<TResult>
Интерфейс дает нам время компиляции поддержки при указании или инъекционныхIQueryHandlers
в нашем коде. Когда мы изменить ,FindUsersBySearchTextQuery
чтобы вернутьсяUserInfo[]
вместо этого ( за счет реализацииIQuery<UserInfo[]>
), тоUserController
не будет компилироваться, так как общий тип ограничения наIQueryHandler<TQuery, TResult>
не будет в состоянии отобразитьFindUsersBySearchTextQuery
вUser[]
.Однако внедрение
IQueryHandler
интерфейса в потребителя имеет несколько менее очевидных проблем, которые все еще необходимо решить. Количество зависимостей наших потребителей может стать слишком большим и может привести к чрезмерному внедрению конструктора - когда конструктор принимает слишком много аргументов. Количество запросов, выполняемых классом, может часто меняться, что потребует постоянного изменения количества аргументов конструктора.Мы можем решить проблему слишком большого количества инъекций
IQueryHandlers
с помощью дополнительного уровня абстракции. Мы создаем посредника, который находится между потребителями и обработчиками запросов:public interface IQueryProcessor { TResult Process<TResult>(IQuery<TResult> query); }
Это
IQueryProcessor
неуниверсальный интерфейс с одним универсальным методом. Как вы можете видеть в определении интерфейса, этоIQueryProcessor
зависит отIQuery<TResult>
интерфейса. Это позволяет нам иметь поддержку времени компиляции в наших потребителях, которые зависят отIQueryProcessor
. Давайте перепишем,UserController
чтобы использовать новыйIQueryProcessor
:public class UserController : Controller { private IQueryProcessor queryProcessor; public UserController(IQueryProcessor queryProcessor) { this.queryProcessor = queryProcessor; } public View SearchUsers(string searchString) { var query = new FindUsersBySearchTextQuery { SearchText = searchString, IncludeInactiveUsers = false }; // Note how we omit the generic type argument, // but still have type safety. User[] users = this.queryProcessor.Process(query); return this.View(users); } }
UserController
Теперь зависит наIQueryProcessor
который может обрабатывать все наши запросы. ВUserController
«SSearchUsers
метод вызываетIQueryProcessor.Process
метод , проходящий в инициализированном объекте запроса. ПосколькуFindUsersBySearchTextQuery
реализуетIQuery<User[]>
интерфейс, мы можем передать его универсальномуExecute<TResult>(IQuery<TResult> query)
методу. Благодаря выводу типа C # компилятор может определять универсальный тип, и это избавляет нас от необходимости явно указывать тип.Process
Также известен возвращаемый тип метода.Теперь
IQueryProcessor
поиск правильных решений является обязанностью реализацииIQueryHandler
. Это требует некоторой динамической типизации и, возможно, использования инфраструктуры внедрения зависимостей, и все это может быть выполнено с помощью всего нескольких строк кода:sealed class QueryProcessor : IQueryProcessor { private readonly Container container; public QueryProcessor(Container container) { this.container = container; } [DebuggerStepThrough] public TResult Process<TResult>(IQuery<TResult> query) { var handlerType = typeof(IQueryHandler<,>) .MakeGenericType(query.GetType(), typeof(TResult)); dynamic handler = container.GetInstance(handlerType); return handler.Handle((dynamic)query); } }
QueryProcessor
Класс создает определенныйIQueryHandler<TQuery, TResult>
тип , основанный на типе экземпляра прилагаемого запроса. Этот тип используется, чтобы запросить предоставленный класс контейнера для получения экземпляра этого типа. К сожалению, нам нужно вызватьHandle
метод с помощью отражения (в данном случае с использованием ключевого слова dymamic C # 4.0), потому что на этом этапе невозможно привести экземпляр обработчика, поскольку универсальныйTQuery
аргумент недоступен во время компиляции. Однако, еслиHandle
метод не будет переименован или не получит другие аргументы, этот вызов никогда не завершится ошибкой, и если вы захотите, очень легко написать модульный тест для этого класса. Использование отражения даст небольшое снижение, но не о чем беспокоиться.Чтобы ответить на одну из ваших проблем:
Следствием использования этого дизайна является то, что в системе будет много маленьких классов, но наличие большого количества маленьких / сфокусированных классов (с понятными именами) - это хорошо. Этот подход явно намного лучше, чем наличие множества перегрузок с разными параметрами для одного и того же метода в репозитории, поскольку вы можете сгруппировать их в один класс запроса. Так что классов запросов по-прежнему гораздо меньше, чем методов в репозитории.
источник
TResult
параметрIQuery
интерфейса бесполезен. Однако в моем обновленном ответеTResult
параметр используетсяProcess
методомIQueryProcessor
для разрешения ошибкиIQueryHandler
во время выполнения.IQueryable
и убедившись, что не перечислил коллекцию, а затем из того, чтоQueryHandler
я только что вызвал / связал запросы. Это дало мне гибкость для модульного тестирования моих запросов и их объединения в цепочку. У меня есть служба приложений поверх моейQueryHandler
, и мой контроллер отвечает заМой способ справиться с этим на самом деле упрощен и не зависит от ORM. Я считаю, что репозиторий таков: задача репозитория - предоставить приложению модель, необходимую для контекста, поэтому приложение просто запрашивает репозиторий о том, что ему нужно, но не сообщает, как это получить.
Я предоставляю метод репозитория с критериями (да, стиль DDD), который будет использоваться репозиторием для создания запроса (или чего-то еще, что требуется - это может быть запрос веб-службы). Объединения и группы imho - это детали того, как, а не что и критерии должны быть только основой для создания предложения where.
Модель = конечный объект или структура данных, необходимая приложению.
public class MyCriteria { public Guid Id {get;set;} public string Name {get;set;} //etc } public interface Repository { MyModel GetModel(Expression<Func<MyCriteria,bool>> criteria); }
Возможно, вы можете использовать критерии ORM (Nhibernate) напрямую, если хотите. Реализация репозитория должна знать, как использовать критерии с базовым хранилищем или DAO.
Я не знаю ваш домен и требования к модели, но было бы странно, если бы приложение само создавало запрос. Модель настолько меняется, что невозможно определить что-то стабильное?
Это решение явно требует некоторого дополнительного кода, но оно не связывает остальную часть с ORM или чем-то еще, что вы используете для доступа к хранилищу. Репозиторий выполняет свою работу, чтобы действовать как фасад, и ИМО он чист, а код «перевода критериев» можно использовать повторно.
источник
Я сделал это, поддержал это и отменил это.
Основная проблема заключается в следующем: независимо от того, как вы это делаете, добавленная абстракция не дает вам независимости. Это будет утечка по определению. По сути, вы изобретаете целый слой только для того, чтобы ваш код выглядел привлекательно ... но это не снижает необходимость в обслуживании, не улучшает читаемость и не дает вам какой-либо модели агностицизма.
Самое интересное, что вы ответили на свой вопрос в ответ на ответ Оливье: «это по сути дублирует функциональность Linq без всех преимуществ, которые вы получаете от Linq».
Спросите себя: как же этого не могло быть?
источник
Вы можете использовать свободный интерфейс. Основная идея состоит в том, что методы класса возвращают текущий экземпляр этого самого класса после выполнения некоторого действия. Это позволяет связывать вызовы методов.
Создав соответствующую иерархию классов, вы можете создать логический поток доступных методов.
public class FinalQuery { protected string _table; protected string[] _selectFields; protected string _where; protected string[] _groupBy; protected string _having; protected string[] _orderByDescending; protected string[] _orderBy; protected FinalQuery() { } public override string ToString() { var sb = new StringBuilder("SELECT "); AppendFields(sb, _selectFields); sb.AppendLine(); sb.Append("FROM "); sb.Append("[").Append(_table).AppendLine("]"); if (_where != null) { sb.Append("WHERE").AppendLine(_where); } if (_groupBy != null) { sb.Append("GROUP BY "); AppendFields(sb, _groupBy); sb.AppendLine(); } if (_having != null) { sb.Append("HAVING").AppendLine(_having); } if (_orderBy != null) { sb.Append("ORDER BY "); AppendFields(sb, _orderBy); sb.AppendLine(); } else if (_orderByDescending != null) { sb.Append("ORDER BY "); AppendFields(sb, _orderByDescending); sb.Append(" DESC").AppendLine(); } return sb.ToString(); } private static void AppendFields(StringBuilder sb, string[] fields) { foreach (string field in fields) { sb.Append(field).Append(", "); } sb.Length -= 2; } } public class GroupedQuery : FinalQuery { protected GroupedQuery() { } public GroupedQuery Having(string condition) { if (_groupBy == null) { throw new InvalidOperationException("HAVING clause without GROUP BY clause"); } if (_having == null) { _having = " (" + condition + ")"; } else { _having += " AND (" + condition + ")"; } return this; } public FinalQuery OrderBy(params string[] fields) { _orderBy = fields; return this; } public FinalQuery OrderByDescending(params string[] fields) { _orderByDescending = fields; return this; } } public class Query : GroupedQuery { public Query(string table, params string[] selectFields) { _table = table; _selectFields = selectFields; } public Query Where(string condition) { if (_where == null) { _where = " (" + condition + ")"; } else { _where += " AND (" + condition + ")"; } return this; } public GroupedQuery GroupBy(params string[] fields) { _groupBy = fields; return this; } }
Вы бы назвали это так
string query = new Query("myTable", "name", "SUM(amount) AS total") .Where("name LIKE 'A%'") .GroupBy("name") .Having("COUNT(*) > 2") .OrderBy("name") .ToString();
Вы можете создать только новый экземпляр
Query
. Остальные классы имеют защищенный конструктор. Смысл иерархии в том, чтобы «отключить» методы. Например,GroupBy
метод возвращает,GroupedQuery
который является базовым классомQuery
и не имеетWhere
метода (метод where объявлен вQuery
). Поэтому позвонитьWhere
позже невозможноGroupBy
.Однако это не идеально. С помощью этой иерархии классов вы можете последовательно скрывать элементы, но не показывать новые. Поэтому
Having
выдает исключение при его предыдущем вызовеGroupBy
.Учтите, что звонить можно
Where
несколько раз. Это добавляет новые условияAND
к существующим условиям. Это упрощает создание фильтров программным способом из отдельных условий. То же самое возможно и сHaving
.У методов, принимающих списки полей, есть параметр
params string[] fields
. Он позволяет передавать имена отдельных полей или массив строк.Fluent-интерфейсы очень гибкие и не требуют создания множества перегрузок методов с различными комбинациями параметров. Мой пример работает со строками, однако этот подход можно распространить на другие типы. Вы также можете объявить предопределенные методы для особых случаев или методы, принимающие настраиваемые типы. Вы также можете добавить такие методы, как
ExecuteReader
илиExceuteScalar<T>
. Это позволит вам определять такие запросыvar reader = new Query<Employee>(new MonthlyReportFields{ IncludeSalary = true }) .Where(new CurrentMonthCondition()) .Where(new DivisionCondition{ DivisionType = DivisionType.Production}) .OrderBy(new StandardMonthlyReportSorting()) .ExecuteReader();
Даже команды SQL, построенные таким образом, могут иметь параметры команд, что позволяет избежать проблем с внедрением SQL-кода и в то же время позволяет кэшировать команды на сервере базы данных. Это не замена O / R-mapper, но может помочь в ситуациях, когда в противном случае вы создавали бы команды, используя простую конкатенацию строк.
источник