Specification для EF

Сложность 200-300

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

Некоторое время назад я писал о фасаде доступа к данным на основе Linq2Sql и в этом подходе важную роль играл паттерн «спецификация», с помощью которого модифицировались запросы к базе данных для получения минимальной выборки, чтобы процесс фильтрации проходил средствами SQL сервера, что правильно. Хотелось бы данный подход распространить и на EF, однако все оказалось на первый взгляд не так просто и радужно как в L2S. Однако с помощью интернета и смекалки все трудности были успешно разрешены, и сейчас я расскажу, как это было решено.

Подготовка

Перед тем как собственно приступить к рассказу о реализации, необходимо немного времени уделить подготовке кода и исходных данных, с которыми будем работать. Использовать я собираюсь EF5 RC Code First и соответственно .Net 4.5. В качестве модели данных пусть у нас будет класс героев, для разнообразия (правда со свойствами класса фантазия наверно подкачала).

Заполнение базы будем делать с помощью специального класса HeroicInitializer, приведу пару строчек заполнения:

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

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

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

Первоначальный подход

Так же как и в статье про L2S, организуем некоторый общий класс, который будет отвечать за доступ к базе данных, обрабатывать запросы и всячески ограждать модели от БД. Для некоторого упрощения, я решил использовать что-то в духе репозитория, который будет иметь только один публичный метод Find(), принимающий в качестве параметра спецификацию.

Т.е. передавая спецификацию мы должны получить набор данных удовлетворяющих набору спецификаций. При этом желательно чтобы максимальное количество фильтраций производилось на SQL сервере.  К реализации репозитория перейдем чуть позже.

Обратимся к реализации и виду спецификации. Начнем с интерфейса спецификации, который по большому счету не изменился:

Однако реализация несколько изменилась по сравнению с тем, что было использовано ранее, теперь спецификация принимает в конструкторе выражение для сравнения. Ранее это условие было простым кодом, записанным в теле метода IsSatisfiedBy().

Может быть несколько топорно и негибко сделаны спецификации And и Or, однако все это работает, и для демонстрации подхода годится. Для примера конкретных спецификаций можно взять условия имени, умения летать и сила на гаджетах или нет основана.

Не сложно, получилось в итоге так ведь?

Но сами по себе спецификации никак не помогут достучаться до базы данных и как-то передать туда фильтры. Поэтому займемся репозиторием, в самом кратком виде, который нам нужен.

Основные моменты кода:

  • Репозиторий работает не с классом DbContext напрямую, а использует объект более низкой абстракции ObjectContext. Данный объект предоставляет больше контроля при работе с наборами данных, что собственно нам и надо.
  • Используется сервис по получению множественных имен для сущностей. С помощью него будем получать имена для формирования первоначального запроса.
  • С помощью ObjectContext можно получить запрос к необходимой сущности.
  • После получения запроса типа IQueryable, формируем запрос Where() с передачей спецификации.

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

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

Мда, получается, вся фильтрация происходит на клиенте, и модификация запроса не происходит, как мы на то надеялись.

Модифицированный подход

Проблема заключается в том, что метод IsSatisfiedBy() подается на вход метода Where() который в свою очередь принадлежит результату операции GetQuery().  В таком виде Where() будет применено к результату работы GetQuery(), т.е. фактически будет работа с интерфейсом IEnumerable(), т.к. на вход спецификации подаются отдельные элементы, которые материализуются в результате работы GetQuery(). Для того, чтобы все заработало как ожидается, необходимо в метод IsSatisfiedBy() подавать IQueriable, т.е. метод GetQuery(), тогда Where() будет применен к IQueriable объекту и EF сможет транслировать выражение в SQL запрос (с известной долей ограничения).

Исходя из сказанного, начнем модификацию кода и первым объектом будет спецификация, как основной виновник такого недопонимания между нами и EF.

Добавляем в интерфейс спецификации перегруженный метод IsSatisfiedBy():

Далее следует реализация нового метода:

Пока что неплохо все получается. Кроме этого потребуется еще изменить метод Find() в репозитории:

Казалось бы, на этом все, однако при запуске с составной спецификацией вы получите NullReferenceException, так как в текущем виде не происходит слияния выражений поиска нужным образом.

Для решения данной задачи, требуется перестроить дерево выражений, для работы с запросом. По большей части данный код был слизан и незначительно подправлен для текущих нужд. Итак, требуется композиция выражений.

Данный код определяет тела двух выражений и приводит их к одному выражению связанному логическим условием «И» или «Или», параметр берется из первого выражения. Главное что это работает. После чего классы AndSpecification и OrSpecification можно переписать:

Стало даже лаконичнее чем было!

Готовые, конкретные спецификации никаким модификациям подвергать не надо. Запускающий код тоже можно не трогать, должно все заработать. Запускаем приложение, смотрим профайлер:

Отлично, наши изменения передаются в базу данных. Можно попробовать создать спецификацию на имя, и посмотреть, как будет транслироваться условие «имя начинается с…». Я уже не буду вставлять скриншот, можете поверить на слово, что получается такой запрос:

Запрос при этом выглядел следующим образом:

Так что можно увидеть, что верно расставлены скобки в запросе, условие like сформировано верно. Работает!

Итого

Таким вот непростым способом мы добились простого применения спецификаций при работе с EF. В данном случае остался за рамками разговор о репозиториях и их работе, об этом как-нибудь в другой раз, главное – идея работы с контекстом EF и составлением модифицированного запроса к базе. Надеюсь, я рассказал все достаточно ясно, чтобы вы могли это применить в своей работе и насладиться результатами.

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

Демонстрационный код лежит на Assembla. Проект создан в VS2012RC.

 

Hard’n’Heavy!

 

Violet Tape

 

Оставить комментарий