При разработке приложений, работающих с базой данных, неизменно встает вопрос проектирования слоя доступа к этим самым данным. Использование «чистого» ADO.Net в рамках больших и сложных приложений рассматривается всё реже, и использование ORM становится уже стандартом де-факто.
Однако и после выбора ORM вопросы проектирования не заканчиваются — встает проблема собственно организации слоя доступа к данным. Здесь есть несколько ставших уже классическими подходов:
- Репозиторий. Стандартный шаблон, где на каждую сущность БД (точнее, на каждый корень агрегации) создается репозиторий, инкапсулирующий запросы к БД и возвращающий что-то вроде IEnumerable<Entity>. Выглядит это обычно так:
public IEnumerable<Account> GetActiveAccounts(); public IEnumerable<Account> GetSuspendedAccounts(); public IEnumerable<Account> GetAccountsByFirstName(string firstName);
Основная проблема в таком подходе — разрастание репозитория (подобных функций становится много) и нарушение принципа Interface Segregation (ради использования одной функции репозитория обычно запрашивается репозиторий целиком).
- CQS-подход. Применительно к запросам, этот подход предлагает выделение функций репозитория (подобных вышеприведенным) в отдельные классы-Query, ответственность которых — в выборке данных, необходимых для какой-либо вьюшки. Преимущество подхода в соблюдении SRP принципа, легкости наследования и удобства наложения сквозной логики вроде кэширования или авторизации.
Проблема, на мой взгляд, состоит в некотором over-engineering’e, если данный подход используется как единственный во всём приложении. Для простых запросов вроде Session.Customers.Where(customer => customer.Email.Contains(«gmail.com»)) создание отдельных классов выглядит избыточным. -
IQueryable-репозиторий с использованием спецификаций. Совмещение первого и второго подхода: присутствуют репозитории на каждую сущность, с набором функций типа:
public IQueryable<Account> Find(ISpecification<Account> spec); public Account Single(ISpecification<Account> spec); public IQueryable<Account> FindAll(); ...
Репозитории остаются лёгкими, возможные дубликации кода устраняются использованием спецификаций, view-специфичная логика(вроде eager-loading, сортировки и пагинации) остается ближе к вьюшке и не захламляет ни Query-классы, ни репозитории. Собственно, подход выглядит оптимальным, но не лишен некоторых неудобств (например, если класс активно работает с несколькими типами БД-сущностей, то для каждого типа нужен отдельный специфичный репозиторий).
-
UnitOfWork как слой доступа к данным. Подход представляет собой нечто среднее между IQueryable-репозиторием и прямым доступом к Session из приложения (к слову, за такой тип доступа ратует Ayende, аргументируя замечательной фразой: Getting data from the database is a common operation, and should be treated as such). Над Сессией (ObjectContext в случае EF) создается враппер, включающий в себя функции IQueryable-репозитория и стандартные для UnitOfWork Commit\Dispose операции.
Плюсы подхода — в применении спецификаций (дающих возможность повторно использовать запросы), абстрагировании от ORM (запросы, выполненные через спецификации, легко можно перевести на другую ORM, хоть это и не самоцель) и сохранении простоты кода (минимум слоев абстракции).
К слову, в качестве спецификаций я использую Linq Query Specifications и конструкцию LinqSpec.For<Entity>(), которая позволяет создавать спецификации в одну строчку, вместо создания отдельного класса для каждой спецификации.
Как показывает практика, последний подход гладко ложится на 90% случаев. Остальные 10 — это сложные запросы, инкапсуляция которых в спецификацию сделала бы спецификации слишком громоздкими (например, запросы со сложными ограничениями на join-таблицы). Для таких случаев оправданным выглядит применение CQS-подхода, который, к тому же, упростит тестирование данной сложной выборки.