Не мог не поделиться с сообществом этой публикацией — вольным перевод с английского статьи одного из соавторов (Oren Eini) известного фреймворка персистентности NHibernate "25 Reasons Not To Write Your Own Object Relation Mapper"; оригинальный текст статьи находится здесь (http://ayende.com/Blog/archive/2006/05/12/25ReasonsNotToWriteYourOwnObjectRelationalMapper.aspx).
По причине того, что Oren Eini aka Ayende пишет достаточно специфическим языком, ибо хорошо знаком с «изнанкой» OR-мапперов, перевод на русский был дополнен необходимыми пояснениями, которых нет в оригинальном тексте.
Итак, читаем
25 причин за то, чтобы не начать писать свой OR/Mapper, или Прочти всякий раз при разработке Data Access Layer
Не так давно меня попросили выбрать один из вариантов технологии построения слоя доступа к данным для кое-какого приложения. На выбор были предложены следующие варианты:
Закодить / сгенерировать по шаблонам DAL (Data Access Layer);
Использовать имеющийся OR-маппер;
Написать свой OR-маппер, специфичный для конкретного приложения.
Датасеты (DataSets) из ADO.Net оказались не очень подходящими для того приложения, поэтому первый и последний вариант — по сути одно и то же. Кроме того, схема БД — это не такая штука, которая только на основе ее позволит автомаГически сгенерировать код для доступа к данным, поэтому выбор свелся к тому, чтобы написать свой ORM или взять уже существующий.
Лично я рекомендую (а я присоединяюсь — прим. перевод.) NHibernate (ну, конечно! — прим. перевод.); тем не менее, первое, что приходит в голову - построить простую прослойку доступа к данным поверх хранимых процедур БД.
Такая прослойка является очень простой в плане кодирования, и посему написать ее самостоятельно несложно. Трудность этого подхода состоит в том, что для все более и более сложного DAL вы будете просто неспособны обеспечить весь функционал, который потребуется. Также этот подход весьма и весьма ориентирован на процедурный стиль SQL, и требует от вас отслеживать еще дополнительно кучу вещей, относящихся к данным, в коде самого приложения.
В общем, я сел за клавиатуру и составил список фич, которые мне понадобились от ORM в моем последнем проекте, использующем NHibernate внутри DAL. Некоторые из пунктов списка являются базовыми любого ORM, некоторые — просто очевидны, некоторые — просто особые приемы, которые я использую. Да, некоторые из них перекрываются и/или требуют наличия других пунктов, а некоторые — просто основа для других интересных трюков. Этот список основан на тех возможностях, что реализованы в NHibernate; и он в любом случае не полон. Конечно, я привык использовать некоторые другие фичи, о которых счас трудно вспомнить сразу. В мире полно ORM, которые не поддерживают весь список целиком; кроме того, есть несколько действительно крутых приемов, которые не реализованы и в NHibernate.
В списке так же подразумевается и паттерн Unit Of Work (далее UoW), который лежит в основе работы OR/M (таких, как NHibernate, DLinq и т.п.). Конечно, их работу можно организовать и по-другому (ActiveRecord, отсоединенные объекты), но я нахожу весьма привлекательным использование именно стиля UoW, который избавляет меня от необходимости ручного контроля изменений, примененных к объектам. Он так же очень близко отвечает той схеме, по которой обычно работают web-приложения (запрос на web-сервер — это, как правило, и есть UoW)
Короче говоря, вот они, эти пункты, безо всякого порядка :)
1. Транзакции.
Это очевидное требование. Транзакции обязательно должны быть реализованы для выполнения любых операций. Используются при обновлении нескольких таблиц/строк/объектов в согласованной манере и при организации работы нескольких пользователей, оперирующих с одними и теми же данными.
2. Контроль конфликтов одновременного доступа (версионирование, исключение потерянного обновления, оптимистическая блокировка).
Обязательно должен быть реализован для выполнения любого рода операций с данными. Так же обязаны быть реализованными уведомления (обычно в виде бросания исключений) при попытке сохранить данные в ситуации, когда эти данные уже были отредактированы другим пользователем с момента их последнего чтения. Я предпочитаю реализацию, при которой версия объекта хранится в его специальном поле. Предпочтительно использовать тип TIMESTAMP при работе с MS Sql Server, и что-нибудь похожее при работе с другими базами данных.
Еще один способ отслеживать конфликты — проверять все поля колонки в таблице, чтобы узнать, не изменилось ли чего-нибудь с момента последнего чтения. Это способ несколько хуже, потому что он может вызывать проблемы точного определения изменений (например, тип DateTime может трансформироваться, когда путешествует между сервером и клиентом (прим. перевод.: web-службы до версии .net 2.0 некорректно обрабатывали локальное время)), также возникают проблемы с производительностью (прим. перевод.: из-за того, что приходится предварительно вычитывать данные перед каждой операцией записи). Еще один момент, на который следует обратить внимание при использовании такого подхода — обработка NULL-значений; он также требует наличия информации об исходном состоянии объекта.
3. Встроенное кэширование (на каждый UoW и в целом на приложение).
Кеширование значительно увеличивает производительность приложения. Кеширование на каждый UoW означает, что если отдельный запрос требует некоторую порцию информации дважды в одном и том же UoW, ORM должен быть достаточно «умен» для того, чтобы вернуть информацию, сохраненную в памяти, вместо того, чтобы снова нагружать БД. Кэширование уровня приложения означает, что если одна и та же порция информации требуется в двух разных UoW, только запрос от первого UoW пройдет в БД, следующий будет обслужен из кэша. В идеальном случае это должно быть "прозрачно" для кода приложения. Такой кэш уровня приложения должен уметь корректно обрабатывать ситуации обновления/удаления/вставки записей, чтобы явно не пришлось писать обработку этих ситуаций в коде.
4. Поддержание идентичности (identity) объектов.
Запрос некоторого объекта дважды в одном и том же UoW должен возвращать нам один и тот же экземпляр объекта (в терминах языка программирования — прим. перев.). Это очень важно так как означает, что все изменения, сделанные в этом объекте внутри соответствующего UoW гарантированно останутся. В противном случае можно получить конфликты одновременного доступа к объекту.
Полезный сторонний эффект этой фичи в том, что внутри одного и того же UoW для проверки на равенство можно производить обычное сравнение ссылок на объекты, принятое в языке. В отсутствии этой фичи сравнение на равенство пришлось бы заменять сравнением первичных ключей объектов и, возможно, полей версии.
5. Политика очистки кэша.
Предполагая, что кэш уровня приложения пристуствует, необходимо реализовать возможность явно сбрасывать кэш, если БД была изменена сторонним приложением или процессом.
6. Поддержка запросов.
Позволяет запрашивать отдельный объект или набор объектов по их свойствам. Предпочтительно — с объекто-ориентированным API. Эта фича необходима, чтобы обеспечивать такую вещь, как поиск объектов по заданным условиям, которые обычно очень сложно сделать самому. Так же эта возможность очень удобна при заполнении отчетов и некоторых других манимуляциях с данными.
7. Связи между данными.
В реальном мире данные в БД не ограничены одной строкой. Между различными таблицами обычно существуют связи. Эти связи тоже должны быть отражены на уровне объектов. Предпочтительно так же иметь поддержку обобщенных коллекций - как минимум наборов (Sets) и словарей (Maps).
Связи, которые нужно уметь моделировать (с примером в скобках):
"Один-ко-многим" ("Покупатель" — "Адрес");
"Многие-ко-многим" ("Пользователь" — "Роли");
«Многие-к-одному" ("Адрес" — "Покупатель") (прим. перев.: разница с первым - в том, где хранится и кто управляет связью).
"Многие-к-любому" и "Любой-к-многим" (вариация "многие-к-одному") — для поддержки коллекций объектов, реализующих интерфейс.
«Один-к-одному» ("Покупатель" — "Пользователь")
8. Группирование запросов.
Эта фича очень важна для увеличения производительности. Если у вас есть возможность послать несколько запросов к БД «за один заход», это значительно снижает количество обращений по сети, которые необходимо выполнить.
9. Полиморфные запросы.
Запросы, применимые не только к одному классу, но и к его наследникам. Как пример: получить все правила, относящиеся к заданному объекту, если эти классы правил организованы в сложную ОО-иерархию.
10. Проверка на модификацию.
Использование паттерна UoW предполагает, что мне в своем коде не нужно следить за тем, какие объекты я модифицировал. Если они ассоцированы с текущим UoW (получены с его помощью или были отсоединены от одного и присоединены к другому UoW), то он сам следит за их обновлением и сохраняет их в тот момент, когда я завершаю UoW.
11. Возможность отмены изменений объекта.
Это - возможность установить объект в состояние, которое присутствует сейчас в БД (и отменить все изменения, которые были внесены в него, но не сохранены — прим. перев.)
12. Отложенная загрузка.
Обязательна к реализации, если вы используете связи между объектами. Загружает только текущий объект, без принадлежащих ему коллекций других объектов. Фича позволяет загружать все зависимые объекты в момент первого обращения к ним.
13. Ручное управление отложенной загрузкой.
Позволяет явно отменить поведение отложенной загрузки для коллекций, когда это необходимо. Решает проблему «N+1 запроса» (прим. перевод.: проблема состоит в том, что для объекта с «ленивой» коллекцией из N элементов отложенная загрузка «в лоб» вызовет выполнение N+1 запроса к БД - 1 запрос, чтобы загрузить объект и N отдельных запросов для каждого элемента коллекции, когда к нему обратятся - например, при итерации. В NHibernate, раз он принят как образец реализации, эта проблема решается либо явным указанием ранней загрузки всей коллекции вместе с объектом, либо группированием отложенных операций загрузки элементов коллекции, таким образом эффективно сокращая число «походов» на сервер БД).
14. Каскадные обновления.
Изменения в объекте-родителе (на главном конце связи) могут требовать соответствующее изменение в дочернем объекте (в том, который находится на другом конце этой связи). Это изменение может подразумевать удаление (может быть обработанно в БД как каскадное удаление) или обновление объекта (может быть реализовано средствами БД как триггер), однако в любом случае необходимо иметь возможность пройти по связи между объектами, чтобы найти новые объекты, еще не сохраненные в БД, чтобы вставить их в таблицу.
15. Возможность отладки.
Хорошо, когда все работает с первого раза, но когда (не если:) что-то идет не так, как хотелось, должен существовать способ отследить, что происходит до того момента, пока не станет понятно, в чем дело. Эта фича позволяет исключить некоторые «финты ушами» (ну, например, когда-нибудь имели удовольствие отлаживать сгенеренный на лету код?). Конечно, для этого можно использовать логи, и обычно так и делается. Я настаиваю на этом, потому что я насмотрелся на код, от которого волосы встают дыбом, и который никогда не захотелось бы отлаживать.
16. Безопасное использование в многопоточном режиме.
ORM должен обеспечивать возможность использовать его в многопоточном режиме. Как именно это сделано, неважно; но это означает, что я могу не обращать внимание на потоковую модель и жить счастливо, потому что ASP.NET, например, может перемещать исполнение вашего кода с одного потока на другой (и делает это, если считает нужным).
17. События жизненого цикла объектов.
Позволяет объектам приложения выполнять некоторые действия в ответ на события соответствующего типа, исходящие в ORM (обычно, как реакция на сохранение/обновление/удаление/загрузку записей в БД). Эта фича очень полезна для своевременного «подтягивания» некоторых данных из разных источников (таких как Active Directory, Web-службы и т.д.).
18. Стратегия обработки исключительных ситуаций.
Необходима развитая проработанная стратегия для обработки ситуаций, когда возникает исключение («какие исключения считать неисправимыми?» «что должно происходить при выбросе исключения типа Foo?» и т.п.).
19. Загрузка данных без загрузки объекта.
Нужна для того, чтобы вытащить некоторые данные или свойства объекта, но не сам объект. Это очень полезно, если вы хотите загружать только необходимые данные без загрузки тяжелого объекта целиком - например, вынуть из БД только свойство «Описание» и первичный ключ записи для показа в выпадающем списке.
20. Составные первичные ключи.
Эта фича не относится к тем, которые мне нравятся, но возможность определить такие ключи бывают нужна достаточно часто.
21. CRUD-операции над сущностями.
Ну, тут и сказать особенно нечего.
22. Упорядочивание зависимостей.
Когда я добавляю две новых сущности, и одна из них содержит ссылку на первичный ключ другой, они должны быть вставлены в определенном порядке, чтобы избежать нарушения ссылочной целостности.
23. Поддержка разбиения результатов запроса на страницы.
И мне, и вам такая фича очень понадобится, и я бы предпочел делать ее силами БД, а не самого приложения.
24. Поддержка произвольных типов данных, в том числе сложных.
ORM должен позволять выполнять маппиг как для простых, так и для сложных типов. Простейший пример - маппинг класса XmlDocument на колонку в БД типа XML.
25. Поддержка аггрегатных запросов.
Эта фича в OR/M позволяет исполнять запросы, содержащие такие вещи, как count(), sum(), avg() и т.д. Они нужны достаточно часто, и OR/M должен позволять использовать их.
Все 25 причин, которые я привел, должны натолкнуть вас на мысль, что написание своего высококлассного движка ORM - не такое дело, которое можно выполнить с легкостью. Он требует тщательного обдумывания каждого шага; и далеко не все пункты, которые я привел, решаются тривиально. Заставить их работать совместно оказывается очень непростой задачей. По моему мнению, это также не то, что можно перепоручить начинающему программисту.
Здесь есть очень много мест, где нужно в точности знать, что происходит. Опять же, пункты, приведенные выше - только частичный список того, что я использовал в последнем проекте. Среди них есть основополагающие принципы — транзакции, одновременный доступ, разбиение на страницы, аггрегирующие запросы, что-то более-менее очевидное (отмена изменений и загрузка свойств), что-то необходимо только для увеличения производительности (кэширование, отложенная загрузка).
Если вы планируете вести разработку, опираясь на UoW, обратите внимание на этот список и представьте, сколько всего вам понадобится для слоя DAL приложения, а затем сделайте вывод, хотите ли вы делать это сами, или лучше привлечь сторонние утилиты, которые смогут сделать это за вас.
P.S. Лично я еще и ненавижу дублирование существующего функционала только по причине синдрома NIH (Not Invented Here, «придумано не нами» или изобретение «велосипеда»).

9 коммент.:
Спасибо за перевод.
Исправьте ошибочку плиз небольшую в 6 пункте написано "манимуляциях с данными" ну а надо бы "манипуляциях с данными"
Статья великолепная, перевод тоже хороший. Спасибо.
Здорово. Спасибо.
Немного позанудствую.
Пункт 12. В оригинальной статье говорится о ленивой загрузке, а не об отложенной. Не знаю, разделяются ли эти понятия в NHibernate, но в EF, например, это два разных понятия.
Пункт 13. Оригинальный заголовок - "Flexible Eager Fetch", перевод - "Ручное управление отложенной загрузкой". Eager Fetch - это ни разу не отложенная загрузка.
Пункт 23. По-моему пэйджинг - устоявшийся в девелоперском кругу термин. Прочитав "Поддержка разбиения результатов запроса на страницы", я подумал о чем-то глобальном, пока не прочитал "Paging suppor" в оригинале.
А вообще, статья отличная.
Я не так хорошо знаю EF, чтобы сказать, чем в нем отличается ленивая загрузка от отложенной. В данном переводе отложенная == ленивая.
Я долго думал, как перевести пункт 13 с его eager fetch, но ничего нормального не сообразил - пришлось пойти от противного. Я предупредил в начале статьи о вольности перевода.
По поводу пейджинга - возможно вы и правы, можно было оставить как есть. Не думаю, что перевод сильно пострадал от этого.
>> Я не так хорошо знаю EF, чтобы сказать, чем в нем отличается ленивая загрузка от отложенной.
Отложенной (deferred) загрузкой в EF называется ручная загрузка навигационных свойств (путем вызова метода Load).
А ленивой загрузки в EF v. 1 нет. Хотя ее можно достаточно просто прикрутить (можно загуглить "Entity Framework lazy loading").
>> Я долго думал, как перевести пункт 13 с его eager fetch, но ничего нормального не сообразил
Да, у меня тоже нет мыслей, как это можно перевести на русский :)
>> По поводу пейджинга - возможно вы и правы, можно было оставить как есть. Не думаю, что перевод сильно пострадал от этого.
Не сказалось. Отличный перевод :)
>Eager Fetch
В какой-то книге видел перевод: "энергичная загрузка". ))
Еще учесть на будущее вариант "жадная загрузка" - более-менее точно передает смысл.
Ну да ладно, пустое :)
Отправить комментарий