Объектная модель и объектно-реляционная проекция

Объектная модель и проекцияИнформацию  об объектной модели я намеренно вынес из статьи "Другие подходы и модели" в эту отдельную заметку блога. Причина проста: объектной модели данных не существует, как не существует, например, функциональной или событийной модели данных. Соответствующие модели, точнее говоря, подходы к разработке приложений, разумеется, существуют, но без уточняющего слова «данных», важного для нас в рамках сюжета этой статьи.

Тем не менее, доминирующая на сегодняшний день в программировании объектная модель и соответствующие подходы оперируют какими-то данными. Причём данные эти инкапсулированы, то есть встроены и сокрыты в объектах. Как ими управлять: извлекать согласно критериям запросов, хранить, модифицировать и т. д.?

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

Кнута или Никлауса Вирта можно найти немало математики, делающей синтез и оптимизацию структур данных осмысленным занятием для применяемых алгоритмов.

С объектами сложнее. Математической базы за объектно­-ориентированным подходом (ООП) не стоит, это детище инженерных экспериментов 1960-х годов (см. например язык Симула-67).

Стандартизация объектной модели проводится «задним числом» консорциумом OMG, что не мешает объектным средам иметь свои особые понятия, не имеющие полных аналогов ни в одной версии UML. С начала массового распространения ООП прошло более 30 лет, но стандартизация внутренней реализации объекта, позволяющая оперировать ими в разных средах — недостижимая на практике утопия до сих пор. Все убожество интероперабельности современных систем живёт на шлюзах, ежесекундно перепаковывающих тучи рассекающих сетевое пространство слепков извлечённых из объектов данных в виде рассмотренных выше XML, JSON и иже с ними.

Поэтому в объектной модели с точки зрения данных нас должна интересовать сугубо структурная часть, являющаяся основой для процедур упаковки (serialization) и восстановления (deserialization). Действительно, если «оголить» класс, убрав из него все функции (методы) и рассматривать только свойства, привязанные к внутренним переменным класса (полям), считая их детерминированными, то есть, не меняющими своё значение после его считывания, то получается знакомая всем программистам структура. Информационный скелет класса.


Небольшое отступление. Не следует забывать принцип инкапсуляции в ООП, ведь совершенно необязательно, чтобы «скелет» соответствовал наружному виду, фасаду. Но в большинстве случаев структурное сходство объекта с тем, что мы наблюдаем извне (интерфейс) имеется. Интересно также, что данный постулат является одной из основ научного мировоззрения, состоягцего из следующих допущений:

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

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

Реализации отображения данных реляционной СУБД на объекты приложения называется Объектно-Реляционной Проекцией (ОРП), соответственно, Object Relational Mapping (ORM) в англоязычных источниках.

Первым в аббревиатуре ОРП идёт слово «объектно», что означает условный примат схемы объектной модели, по которой генерируется схема реляционной БД. Но поскольку обе модели являются логическими и имеют сравнимый уровень абстракции, то возможен и обратный механизм: генерация классов объектов по реляционной схеме. В этом случае проектор называется уже РОП (ROM — Relational-Object Mapping). На практике чаще под словом ORM (ОРП) имеют в виду оба подхода, не уточняя, являются они объектно- или датацентричными.

Прохождение запроса к СУБД через ОРП

Рис. 1. Прохождение запроса к СУБД через ОРП

На первый взгляд может показаться, что запрос к ОРП короче SQL. Это не вполне верно, потому что язык запросов ОРП представляет собой некоторую надстройку, сравнивать которую нужно с соответствующим аналогом. На уровне СУБД для наиболее часто используемых запросов создаются виды (view), который выполняют в том числе ту же функцию — сокращение кода запросов в прикладных программах за счёт сокрытия соединений. Другая важная функция видов — абстрагирование от структуры таблиц, позволяющее менять схему данных не нарушая работу приложения.

Так, если создать в СУБД следующий вид:

 
CREATE VIEW v_product_orders AS
SELECT p.*
FROM products p
	INNER JOIN order_items oi ON p.id = oi.id_prod
	INNER JOIN orders o ON o.id = oi.id_order

 

то в прикладной программе можно будет писать даже короче, чем в примере с ОРП:

 
SELECT *
FROM v_product_orders
WHERE number = 'O123'

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

<class name="Domain.Sales.Order" table="orders" schema="sales">
<id name="Id" access="property" column="id_order">
   <generator class="native">
      <param name="sequence">sales.saq_id_order</param>
   </generator>
</id>
<set name="OrderItems" table="sales.order_items" inverse="true" lazy="true">
   <key column="id_order" />
   <one-to-many class="Domain.Sales.OrderItem" />
</set>
<property name="Number" access="property">
   <column name="number" not-null="true" />
</property>
<property name="Created" access="property">
   <column name="created" not-null="true" />
</property>
<property name="Completed" access="property" type="YesNo">
   <column name="completed" not-null="true" />
</property>
</class>

Среды с развитыми механизмами метаданных, такие как .NET, позволяют также определять проекцию непосредственно в коде классов, например, в виде атрибутов.

[Serializable]
[Class(Schema = "sales", Table = "order_itemss")]
public class OrderItem : NhBase<ProgramRequirements>
{
   [Id(Name = "Id", Column = "id_order_item"), Generator(1, Class = "native")]
   public virtual int Id { get; set; }
   [ManyToOne(Column = "id_order", OuterJoin = OuterJoinStrategy.False)]
   public virtual Order Order { get; set; }
   [ManyToOne(Column = "id_prod", OuterJoin = OuterJoinStrategy.False)]
   public virtual Product Product { get; set; }
   [Property(Column = "price")]
   public virtual decimal Price { get; set; }
   [Property(Column = "qty")]
   public virtual decimal Quantity { get; set; }
}

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

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

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

  1. Хранение всех атрибутов (полей) в одной таблице.
  2. Группировка общих атрибутов в одной таблице и разнесение уникальных атрибутов подклассов по связанным таблицам.
  3. Представление каждого класса в виде отдельной таблицы с дублированием общих атрибутов.

Первый метод имеет весьма опосредованное отношение к реляционной модели и может рассматриваться разве что как временное решение или средство оптимизации в частном случае. Остальные методы используются автоматизированными инструментами проектирования базы данных, например ErWin или PowerDesigner, и обозначаются как «полный/неполный подтип» (complete/incomplete sub-typing) со схемой «атрибуты подкласса в новой таблице» (inherit only primary attributes, метод 2) и «наследуемые атрибуты добавляются к таблице подкласса» (inherit all attributes, метод 3).

В ОРП, поддерживающих выбор метода генерации схемы БД, например, в JDO, обозначения несколько иные. Первый метод называется «плоским отображением» (flat mapping), остальные методы реализуются с помощью либо «вертикального отображения» (метод 2, vertical mapping), либо «смешанного отображения» (методы 2 и 3, mixed mapping).

С точки зрения реляционной модели, только метод 2 приводит к созданию нормализованной схемы данных. Однако, если иерархия классов глубока, то каждый запрос будет сопровождаться соответствующим числом соединений таблиц, что неминуемо скажется на производительности. Поэтому в качестве компромисса возможно комбинирование с методами 1 и 3.

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

Следующей проблемой, уже технологического характера, является поддержание схемы проекции объектной модели на реляционную в актуальном состоянии. Приложение развивается в соответствии с изменениями требований к системе. Для простой операции добавления атрибута кроме собственно изменения таблицы и экранных форм потребуются дополнительные изменения на уровне ОРП: меняется описание класса и схемы отображения. Изменение связей и реструктурирование с разбиением таблиц вызовет цепные изменения во многих классах.

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

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

Основное преимущество проявляется в том, что слой ОРП позволяет программисту оперировать в приложении не SQL-запросами и табличными результатами, а объектами и их списками (коллекциями), выбираемыми по заданным критериям. Это делает программный код более единообразным, нежели изобилующий текстовыми константами SQL-запросов типичный код клиент-серверного приложения, работающего непосредственно с СУБД. То есть вместо одних операторов

 
Query1.Open("SELECT * FROM task_queue WHERE id_task IN (2, 3,
15) AND id_task_origin = 10");
foreach (DataRow row in Query1.Rows)
{
...
}

программист пишет другие:

 
IList queues = session.CreateCriteria()
     .Add(Expression.In("Task.Id", someTasks.ToArray()))
     .Add(Expression.Eq("TaskOrigin.Id", 10))
     .List();
foreach (TaskQueue queue in queues)
{
     ...
}

Во втором фрагменте, несколько более громоздком, в отличие от SQL- запроса в виде текстовой строки, часть кода проходит проверку компилятором, выявляя возможные ошибки на более ранней стадии. Работу по отображению объектно-ориентированного определения запроса в SQL целевой СУБД берет на себя слой ОРП.

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

String cityName = order.Customer.Address.City.Name;

Но перемещаясь по связям в самой программе, надо отдавать себе отчёт, что каждое обращение к свойству объекта, возвращающее другой объект, может вызвать запрос к СУБД для инициализации этого объекта. Извлечённые объекты сохраняются в кэше ОРП, поэтому возможна ситуация, когда большая часть БД окажется в оперативной памяти приложения. Возникает проблема синхронизации этого кэша с изменениями, совершаемыми другими приложениями.

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

Сложности нарастают с переходом к обработке данных за пределами CRUD-логики. Использовать в запросах непосредственно SQL остаётся технически возможным, но тогда теряется основной смысл ОРП. Чтобы избежать работы с SQL, программисту приходится извлекать списки объектов и обрабатывать их средствами приложения, тогда как в рамках СУБД аналогичная обработка выполнялась бы на порядок быстрее.

SQLвысокоуровневый декларативный специализированный язык четвёртого поколения, в отличие от того же Java или C#, по-прежнему относящихся к третьему поколению языков императивных. Единственный оператор SQL на три десятка строк, выполняющий нечто посложнее выборки по ключу, потребует для достижения того же результата в разы, если не на порядок, больше строк на C#.

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

Для простых транзакционных приложений, оперирующих в CRUD-логике, использование ОРП даёт ряд преимуществ: лучшая типизация, более простой код, лучшая переносимость между СУБД. Однако, любые выходы за рамки CRUD приводят программиста к следующим альтернативам:

  • продолжать использовать нестандартный и весьма ограниченный собственный язык запросов ОРП типа HQL;
  • использовать SQL, динамически отображая результат запросов на специально создаваемые для этих целей классы;
  • извлекать списки объектов более простыми запросами и обрабатывать их в приложении.

Все перечисленные способы имеют кроме преимуществ и серьёзные недостатки, анализировать которые следует заранее. Например, HQL мало пригоден для запросов аналитического характера и отчётности, прямое использование SQL рушит абстракцию и приводит к мысли о её изначальной ненужности, обработка объектов средствами приложения требует высокой квалификации программистов, как минимум понимающих зависимости типа O(n). В случаях, когда обработка нужна для предоставления пользователю табличных форм, например, отчётности, с чем прекрасно справляется созданный для этих целей SQL, ОРП является лишним звеном в системе, лишь увеличивающим время отклика.

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

  • современные реализации ОРП, такие как Hibernate, являются системами типа «вещь в себе»: они не поддерживают стандартов, привязаны к средам и фреймворкам, содержат множество параметров настройки, имеют изъяны в дизайне, ставшие особенностями (feature), исправлять которые уже никто не будет;
  • сложность наиболее развитых ОРП стала сравнима с СУБД. И не мудрено, ведь подобные реализации ОРП по сути — попытки создать встраиваемую в приложение объектную СУБД, используя реляционную лишь в качестве «интеллектуальной файловой системы»;
  • использование слоя домена затруднено или даже невозможно из приложений других сред (например, домен-сборка C#.NET из PHP- или C++-приложения), поэтому бизнес-правила могут быть обойдены или многократно реализованы заново. Для решения этой задачи требуется полноценная многоуровневая архитектура с сервером приложений;
  • создать систему с высокой производительностью, используя ОРП, невозможно без хорошего знания реляционных СУБД и привлечения в проект соответствующих экспертов.

Вас заинтересует / Intresting for you:

Реляционные базы данных: объяс...
Реляционные базы данных: объяс... 16273 просмотров Дэйзи ак-Макарова Sun, 30 May 2021, 17:53:20
Реляционная модель в сравнении...
Реляционная модель в сравнении... 4028 просмотров Дэн Tue, 05 Mar 2019, 07:15:14
Темпоральные модели базы данны...
Темпоральные модели базы данны... 1754 просмотров Денис Sat, 23 Mar 2019, 05:09:39
Основные модели данных: иерарх...
Основные модели данных: иерарх... 17101 просмотров Дэйзи ак-Макарова Sun, 09 Sep 2018, 10:28:33
Войдите чтобы комментировать