Классика баз данных - статьи

       

Проблема объектно-табличного отображения


Одна из первых и наиболее очевидных проблем при использовании объектов в качестве внешнего компонента реляционного хранилища данных состоит в том, как следует отображать классы в таблицы. По началу это кажется довольно простым упражнением – таблицы соответствуют типам, столбцы – полям. Даже для типов полей подыскиваются типы столбцов таблиц, которые соответствуют им в довольно сильной степени: типы VARCHAR соответствуют типу String, тип INTEGER – типу int и т.д. Так что имеет смысл для каждого класса, определенного в объектной системе, определить таблицу с тем же или схожим именем. Или, возможно, если объектный код пишется при уже существующей схеме базы данных, то класс отображается на существующую таблицу.

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

Вероятно, наиболее понятным является подход с отдельной таблицей для каждого класса, поскольку он направлен на нахождение минимального «расстояния» между объектной и реляционной моделями. Каждому классу в иерархии наследования соответствует отдельная таблица, и объекты порожденных типов «сшиваются» путем соединения различных таблиц, основанных на наследовании. Например, если в объектной модели имеется базовый класс Person, из которого порождается класс Student, являющийся, в свою очередь, предком класса GraduateState, то потребуются три таблицы PERSON, STUDENT и GRADUATESTUDENT, каждая из которых соответствует одноименному классу.
Однако для связывания этих таблиц потребуется наличие у каждой из них независимого первичного ключа (такого, значения которого реально не хранится в сущности объекта), чтобы у каждого порожденного класса могла иметься связь по внешнему ключу с таблицей, соответствующей его суперклассу. Причина этого понятна: объект GraduateStudent, поскольку у него имеется связь IS-A с объектами Student и Person, является коллекцией трех наборов состояния, и ко времени создания объекта этого типа различие между этими классами в значительной степени теряется. Например, в Java и .NET объект представляет собой участок памяти, в котором сохраняются поля экземпляра, определенные для класса и всех суперклассов, а также таблица методов, определяемых той же иерархией. Это означает, что при расспрашивании конкретного экземпляра на реляционном уровне для сборки состояния объекта в рабочую память объектной программы потребуются, по меньшей мере, три соединения.

На самом деле, дело обстоит еще хуже. Если иерархия объектов продолжает расти, включая, например, классы Professor, Staff, Undergrad (наследует от класса Student) и всю иерархию AdjunctEmployees (наследуемую от Staff), и программа хочет найти всех людей по фамилии Smith, то соединения должны быть выполнены для всех порожденных классов системы, поскольку семантика «найти всех людей» означает, что запрос должен произвести поиск данных в таблице PERSON, а затем выполнить набор дорогостоящих операций соединения для импорта из базы данных оставшихся данных. В этих операциях должны участвовать таблицы PROFESSOR, UNDERGRAD, ADJUCTEMPLOYEE, STAFF и все другие таблицы, соответствующие классам, которые порождены от PERSON. Если учесть, что запросы с соединениями относятся к числу наиболее дорогостоящих запросов, поддерживаемых РСУБД, то становится очевидной невозможность быстрого выполнения требуемых действий.

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


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

Проблема не исчерпывается отображением наследования: совершенно по-разному обрабатываются ассоциации между объектами, типичные ассоциации 1:n и m:n, обычно используемые и в SQL, и в UML. В объектных системах ассоциации являются однонаправленными, ведущими от ассоциирующего объекта к ассоциируемому объекту (это означает, что ассоциируемый объект в действительности ничего не знает об ассоциации, если явно не установлена двунаправленная ассоциация). В то же время в реляционных системах ассоциация на самом деле является инверсной, она ведет и от ассоциируемого объекта к ассоциирующему объекту (через столбцы внешних ключей). Это оказывается поразительно важным, поскольку означает, что для поддержки ассоциаций m:n необходимо использовать третью таблицу для хранения реальной взаимосвязи ассоциирующего и ассоциируемого объектов. Даже в более простом случае связей 1:n у ассоциатора отсутствует внутреннее знание отношений, с которыми он ассоциируется – для обнаружения этих данных требуется выполнить соединение с какой-либо или со всеми ассоциированными таблицами. (Вопрос, когда следует реально выбирать эти данные, заслуживает отдельного обсуждения, см.ниже разд. «Парадокс загрузки».)


Содержание раздела