Одна из ошибок, которую делают разработчики (и я когда-то в их числе) — это утверждение о том, что вы должны использовать ровно одну ORM-стратегию для создаваемого приложения. В общем случае это неверно. Вы можете (и должны) привязывать выбор стратегии к конкретному сценарию, и быть уверенным в том, что выбираете правильные инструменты для конкретного случая.
Сразу отмечу, что 99.9% времени вы не должны использовать ADO.NET напрямую. Если вы до сих пор используете dataReader.MoveNext
— остановитесь!
Множество людей не любят ORM как таковые. Послушав их аргументацию, я соглашусь с тем, что Мартин Фаулер написал в OrmHate:
Наибольшее разочарование от ORM заключается в завышенных ожиданиях.
Мы должны смириться с мыслью о том, что ORM являются плохими, уродливыми и перегруженными. ORM предназначены решать задачу и имеют множество разных подходов для этого. Но, перед тем как мы посмотрим на эти подходы, давайте изучим, какую же задачу мы пытаемся решить?
Преодоление разрыва
Если вы должны загружать или вставлять данные в SQL, вы должны отобразить («замапить») свои .NET типы данных в SQL. Для .NET это означает использование ADO.NET для отправки SQL команд к SQL-серверу. Затем нам надо отобразить SQL типы в .NET типы. И здесь есть нюансы — например, даты в SQL отличаются от дат в .NET.
ADO.NET помогает нам в этом, но оставляет нам работу по обработке сырых наоборов данных и созданию объектов .NET. В итоге мы получаем что хотим — мы работаем с объектами и типами .NET. А какой-то код транслирует это в SQL запросы и обратно.
ORM предназначены решать эту проблему, добавляя слои различных абстракций поверх ADO.NET. Но существует множество стратегий для этого. Давайте взглянем на каждую из них и посмотрим где какие лучше подходят.
Прямое отображение сущностей (Entity-based relational mapping)
В таком отображение почти всегда таблицы базы данных соотносятся 1:1 с сущностями в вашей системе. Когда вы добавляете свойство к объекту — добавляете и колонку к таблице. Использование такого способа строится вокруг загрузки сущности (или агрегата) по его идентификатору, управлению этим объектом и, возможно, связанными объектами, а затем сохранения этого объекта в базу данных посредством ORM.
ORM в этом случае предоставляет множество функционала, например:
- Слежение за изменениями
- Ленивая загрузка (lazy-loading)
- Предзагрузка (eager fetching)
- Каскадность
- Обеспечение уникальности объектов (Identity map)
- Работа с единицами работы (Unit of work)
Если я работаю с только одной сущностью или агрегатом одновременно, то такие ORM как NHibernate нам очень подходят. Они используют указанную конфигруацию для слежения за загруженными сущностями и автоматическим сохранением изменений во время коммита транзакции. И это приятно, потому что мы не должны таскать за собой свой слой работы с данными. NHibernate делает всю грязную работу за нас.
Пока мы загружаем объект по Id с единственной целью изменить его, всё это работает отлично. Это избавляет от большого количества кода, который бы мне потребовалось создать чтобы следить за добавлением объектов, их сохранением и т.д.
Обратная сторона такого подхода в том, что ORM не знает, собираетесь ли вы только читать обекты, или загружаете сущность, чтобы изменить её. Мы часто видим людей спотыкающихся, когда они не понимают, что трекинг изменений включен по умолчанию и как он работает.
Если вы хотите загрузить сущность чтобы её изменить и сохранить изменения (или создать новую сущность), этот подход обеспечивает большую гибкость от включения уровня доступа к данным в ваш инфраструктурный слой и позволяет вашим типам сущностй быть относительно независимыми от их метода сохранения. Эта независимость не означает, что моя модель C# и схема данных могут расходиться. Напротив, это означает, что слой доступа к данным не проникнет в мою объектную модель, которую вместо этого я бы скорее предпочёл нагрузить бизнес-правилами.
Отображение в наборы данных (Result-set-based relational mapping)
В большинстве приложений, требования к чтению данных существенно превосходят количество записей. Мы видели соотношение в 100:1 между SELECT и INSERT/UPDATE/DELETE в нашем недавнем приложении. Когда мы смотрим, в чём SQL действительно хорош, так это в работе с данными в сетах (наборах). Чтобы выбрать какой-то набор данных из SQL сервера, часто не имеет никакого смысла пытаться прямо отображать эти данные в сущности.
Но мы всё равно предпочитаем не работать напрямую с IDataReader или DataTable. Это плохо-типизированные объекты, тяжело переносимые в верхние слои приложения. Наоборот, мы часто строим объекты, приспособленные к данным. Эти объекты часто называется DTO (Data-Transfer Objects), или модели для чтения (Read Models). Такие DTO мы создаём для индивидуальных SQL выборок — и редко для того, чтобы повторно использовать их в других запросах.
Многие ORM имеют функционал, оптимизированный для таких сценариев. В NHibernate, вы можете использовать проекции чтобы выключить трекинг, и отобразить данные напрямую в DTO. Вы можете использовать SQL запросы чтобы сделать это и не нуждаетесь в конфигурации маппинга. Или вы можете использовать микро-ORM например PetaPoco.
Эти чтения также могут генерировать DTO объекты по мере их чтения. И NHibernate и несколько micro-ORMs позволяют получать индивидуальные DTO объекты последовательно один за одним во время чтения строк результатов запроса, тем самым минимизируя объем объектов содержащихся в памяти.
В наших приложения, мы до сих пор часто используем NHiberante для чтения, но не используем объекты сущностей, а вместо этого используем сырой SQL. Мы полагаемся на оптимизированные мапперы NHiberanate, чтобы просто подать тип DTO, а результат выборки отобразится автоматически.
Этот подход не очень хорошо работает, если нам надо применить бизнес правила и сохранить информацю обратно. Так как эти модели обычно отображаются в отдельные наборы данных, а не в таблицы базы данных.
Active Record – это другой пример сущностного отображения данных, в котором, функционал работы с данными включён в саму объектную модель.
Отображение DML-запросов (DML-based relational mapping)
Если вы знаете, какой SQL вам нужен для реализации CRUD, и предпочли бы создавать его вручную, то вы уже ищите что-то, чтобы эффективно аббстрагировать DML команды на уровень выше, чем ADO.NET.
И это арена микро-ORM. Такие фреймворки как PetaPoco, Dapper, Massive и другие созданы, чтобы помочь решить пробемы работы ADO.NET. Они обычно всё равно позволяют нам работать с объектами ADO.NET, но наше взаимодействие сильно упрощается. Нам только нужно соединение, и эти фреймворки могут позволить работать со всеми CRUD операциями в виде, который предлагает намного более простой код, чем сам ADO.NET.
В случая, когда у вас нет сущностей и нужды отображать их в таблицы и обратно, микро-ORM дадут гораздо более лёгкий подход. А так микро-ORM не требуют предварительной конфигурации, то они полагаются на ленивое-исполнение и оптимизинорованные техники кеширования, чтобы налету маппить SQL параметры и результаты запросов. Многие приложение могут начать с маппигом основанном на DML, переходя на полноценную ORM, как только отношения или сущности потребуют этого.
Инструменты массовой загрузки (Bulk loading tools)
Это то, что занимает особое место — иногда вы не хотите вставлять/загружать данные объектным способом. Вместо этого, вы бы предпочли работать с всеми наборами целиком. Такие инструменты, как SQL Bulk Copy, позволяют вам получать и выгружать данные в CSV или в табличных форматах,
Эти утилиты работают примерно как базука, вырывая все данные сразу туда и обратно, но не предоставляя ничего кроме этого. Вы не можете обновлять или удалять данные, но для того, чтобы получить большие объёмы данных из SQL, эти утилиты — то что вам нужно.
Во многих интеграционных сценариях, где вы предоставляете файлы с данными внешним партнёрам, или наоборот — эти загрузчики позволяют пользоваться файлами как таблицам и напрямую загружать их в базы данных.
Эти утилиты намного быстрее традиционных методов парсинга/загрузки данных. В некоторых из наших тестов, мы видели разницу в порядки по сравнению с построчной загрузкой. А в одном случае, мы видели разницу между несколькими часами и минутой. Обратная сторона всего этого, это то, что функционал ограничен только лишь INSERT и SELECT. Всё остальное требует других подходов.
Правильный инструмент для задачи
В одном из недавних проектов, я использовал каждый из представленных выше подходов в работе с одной базой данных и одним кодом. NHibernate для сущностного/агрегатного маппинга, выборки готовых резалт-сетов для чтения наборов данных (и далее подготовки сообщений/экспорта/представлений из результатов), DML-маппинги для простых таблиц и модулей, а также инструменты для bulk-load для загрузки файлов от партнёров с многими миллионами строчек.
Ключевым моментом является то, что у вас нет необходимости привязываете себя к определённому инструменту или подходу. Никакая ORM стратегия не работает во всех сценариях, и не должна этого делать. NHibernate может работать со многими другими сценариями (кроме непосредственного маппинга сущностей), но не делает всего на свете. Сложность часто возникает из-за попыток использовать один и тот же подход всегда.
Каждое приложение, написанно вне SQL сервера использует ORM. Или этого рукописный ADO.NET код, или NHibernate — вы должны преодолевать разрыв между .NET и SQL. Это преодоление — тяжёлая задача, и ничто не решает задачу полностью идеально. И не должно!
Выбирайте подход, который решает конкретную проблему. Не беспокойтесь, что у вас будет несколько ORM-стратегий в одном проекте. Это не означает, что бессистемные решения приемлемы. Но наоборот — применение выверенных решений, основанных на знании возможных вариантов — всегда хорошая идея.
Автор: bitmap