EF5 шаблонный AddOrUpdate

Сложность 400

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

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

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

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

Типичный пример работы с обновляемыми данными выглядит следующим образом:

Уже сам факт наличия маркера IsEditMode несколько напрягает. Не обращаем внимания на разные переменные типа SelectedTradingMode, они не будут играть никакой роли в данном случае. Гораздо важнее здесь следующее, каждый раз берется контекст, даже судя по всему создается, раз оборачивается в usings. Далее, в первом случае, строится репозиторий на основе полученного контекста, что, по сути, не верно, так как репозиторий должен был построиться еще в начале работы «экрана»/«use case», но допустим. Новый объект помещается в репозиторий, а сохранение идет через контекст. Эмм.. допустим. Во втором случае работа идет только с контекстом базы напрямую в слое ViewModel. Ок, не будем сейчас обсуждать недостатки данного конкретного метода, хочу обратить внимание на «репозиторий».

Опять же не отвлекаясь и на конкретную реализацию, можно увидеть, что общий подход основан на работе некоторого базового класса, которому необходимо явно передать тип данных, с которыми будет производиться работа. Т.е. для каждого используемого типа данных надо будет написать такую оболочку, не слишком ли жирно и скучно?  Про реализацию базового класса как-нибудь в другой раз, там тоже есть areas for improvement. В данный момент в исходном проекте сделано порядка 20 таких вот «репозиториев», что весьма удручает.

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

Основная цель: я хочу указать объект на сохранение, а дальше система уже должна сама разобраться, как определить новый он или нет, и сделать все необходимое для того, чтобы объект был готов к сохранению. Т.е. я не должен явно указывать тип на сохранение и говорить, новый он или нет.

Точкой входа для такого функционала может стать метод AddOrUpdate() вынесенный в заголовок статьи. Начнем для наглядности с конкретных типов и перейдем к шаблонной реализации.

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

Есть «Заказ», «Продукт» и «Строка заказа». Для некоторого усложнения и интереса, класс OrderLine содержит составной первичный ключ, на этом в целом простом примере будем тренироваться. Первой реализацией не шаблонного метода, может быть такой код:

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

  • Поиск нужной записи/строки/элемента в базе идет с помощью метода Find() c передачей набора первичных ключей.
  • Метод Find() возвращает null, если записи с указанными первичными ключами не найдено. Это в свою очередь означает, что необходимо переданный в метод объект OrderLine добавить в контекст с помощью метода Add().
  • Если метод Find() вернул объект, тогда он должен быть обновлен согласно переданному объекту OrderLine, для этого используется метод SetValues(). Важно, что этот метод обновляет только изменившиеся данные.
  • Сам по себе метод не сохраняет данные в базу данных, что в целом логично с учетом производительности и возможного отката данных. Так что сохранение данных SaveChanges() должно быть осуществлено позднее.
  • Наш метод возвращает объект из контекста, что может быть полезно при использовании общего fluent подхода к построению интерфейсов и для возможной дальнейшей композиции.

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

Понятно, что класс OrderLine меняем на шаблонный, скажем TEntity. В свою очередь свойство OrderLines может быть заменено на Set<TEntity>(). Самый сложный и хитрый момент получить набор первичных ключей для метода Find() у условно произвольного класса. Метод, который будет получать набор первичных ключей можно назвать KeyValuesFor(). После описанных изменений, метод примет такой вид:

Перед тем как рассказать о реализации метода KeyValuesFor(), необходимо решить как получать свойства, представляющие собой первичные ключи, так как зная свойства, взять значения уже дело техники будет по большей части. Так как  мы используем EF5, то нам потребуются классы ObjectContext и MetadataWorkspace. Метод назовем KeysFor() и это должен быть метод расширения.

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

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

  • ObjectItemCollection несет метаданные обо всех o-space типах в контексте, для которого выполняется запрос. O-space является жаргонным обозначением для object-space, что обозначает метаданные о CLR типах.
  • O-Space метаданные получаются для запрошенного типа.
  • Далее метаданные фильтруются по o-space CLR типу. Так как мы используем EF5 (и можно вообще начиная с EF4.1 как оказалось), то можно резонно ожидать, что совпадений будет ноль или одно.
  • Для работы с прокси классами используется вызов GetObjectType() для определения истинного класса.

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

Данный метод уже возвращает набор значений, который требуется на вход методу Find().

После таких преобразований получаем единственный метод AddOrUpdate(), который работает со всеми типами данных, которые мапятся на базу данных, участвуют в построении контекста базы.

Таким образом, если сделать некий общий репозиторий без специальных методов или же фасад к базе, то метод в начале статьи сократился бы до такого вида:

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

Надеюсь что было интересно, и это сподвигнет вас на более детальное ковыряние с ObjectContext и MetadataWorkspace, там содержится много интересного по всей видимости. Ну и будьте ленивы, не пишите тупой код!

По мотивам статей Артура Викерса.

 

Hard’n’Heavy!

 

Violet Tape

 

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