СТАТЬЯ
26.04.02

База данных — хранилище объектов (Часть 1)

© А. Тенцер
Статья была опубликована на сайте "КомпьютерПресс"

Классической методикой проектирования баз данных (БД) является создание отдельной таблицы для каждой описываемой моделью данных сущности, затем — в процессе нормализации — выделение отдельных таблиц для хранения атрибутов сущности (таблицы-справочники). Такой подход хорошо работает для БД с относительно небольшим количеством описываемых объектов (десятки) и при несложных и статичных связях между ними. Однако любое изменение структуры хранимых данных приводит к внесению изменений в структуру таблиц, эти данные отображающих. Несложная на этапе разработки, эта операция становится крайне проблематичной при больших объемах данных и при отсутствии у разработчика непосредственного доступа к БД (например, если она находится у заказчика). Многим, наверное, знакомы громоздкие, медленные и требующие огромного дискового пространства операции по конвертированию БД при переходе на новую версию продукта. Не менее неприятна работа с БД, исторически разросшейся до сотен таблиц, структуру которой сложно даже изобразить в читабельном виде. В связи с этим встает вопрос: а нельзя ли создать структуру данных, не требующую переделок при появлении новых сущностей и позволяющую хранить произвольную и при этом достаточно простую и эффективную информацию? Чудес, увы, не бывает. Любая универсальная система менее эффективна, чем специализированная. Однако возможно создание решения, в котором бы приемлемая производительность и простота сочетались с достаточной степенью универсальности.

Базовые тезисы

Прежде всего необходимо сформулировать основные принципы, на которых будет строиться проектируемая БД.

  1. Каждая сущность, информация о которой хранится в БД, — это объект.
  2. Каждый объект уникален в пределах БД и имеет уникальный идентификатор.
  3. Объект имеет свойства (строковые, числовые, временные, перечислимые), которые описывают атрибуты сущности.
  4. Объекты могут быть связаны между собой произвольным образом. Связь характеризуется связанными объектами и типом связи. Например, сотрудник фирмы может быть связан с отделом, в котором он работает, связью типа «сотрудник в отделе» и т.п. Связь в определенном смысле аналогична понятию ссылки на таблицу-справочник в традиционной модели БД.
  5. Объект может быть хранилищем. В этом случае допускается хранение в нем других объектов (например, товара на складе).
  6. Такая БД не привязана ни к какой бизнес-модели и позволяет реализовать «над собой» практически любую бизнес-логику. Логика выделяется в отдельный программный слой и, как правило, реализуется на сервере приложений, где по запросу клиента создаются объекты, загружающие информацию о себе из БД и реализующие «поведение» объектов реального мира. В то же время, в силу однообразности модели хранения, эти объекты довольно легко создаются на основе базовых классов, инкапсулирующих функциональность по загрузке и сохранению свойств и связей в БД.

Объекты

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

ObjType
Id INTEGER Первичный ключ
Code CHAR(10) NOT NULL UNIQUE Краткое название типа. Используется в программе для поиска объектов данного типа
ItemName CHAR(30) Полное название типа, используется только для интерфейса

Типами объектов могут быть, например, «Фирма», «Сотрудник», «Товар».

Для упрощения не будем реализовывать наследования — отметим лишь, что для этого необходимо ввести в ObjType поле ParentId, ссылающееся на Id наследника.

Сами объекты хранятся в таблице:

Objects
Id INTEGER

Первичный ключ

TypeId INTEGER REFERENCES ObjType(Id)

Ссылка на тип объекта

ItemName CHAR(50)

Название объекта

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

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

SELECT O.Id, O.ItemName, OT.ItemName

FROM Objects O

INNER JOIN ObjType OT ON O.TypeId = OT.Id

WHERE OT.Code = ‘EMPLOYEE’

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

Свойства

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

Величина же может выражаться данными различного типа. Будем хранить в БД атрибуты следующих типов:

Строковые атрибуты

Для описания атрибутов заведем таблицу

StrDesc
Id INTEGER Первичный ключ
TypeId INTEGER REFERENCES ObjType(Id) Ссылка на тип объекта
Code CHAR(10) Краткое название параметра
ItemName CHAR(30) Полное название параметра, используется только для интерфейса

При настройке БД для каждого типа объектов задается набор свойств данного типа. Так, например, объект типа «EMPLOYEE» (Сотрудник) может иметь строковые свойства:

«FAMILY» — фамилия
«FIRSTNAME» — имя
«LASTNAME» — отчество
«ADDRESS» — адрес
«EMAIL» — адрес электронной почты

Поле Objects.ItemName для сотрудников заполняется сервером приложений как комбинация из атрибутов FAMILY + FIRSTNAME + LASTNAME.

Сами атрибуты хранятся в таблице:

Strings
Id INTEGER Первичный ключ
TypeId INTEGER REFERENCES StrDesc(Id) Ссылка на тип параметра
ObjectId INTEGER REFERENCES Objects(Id) Ссылка на объект
Value VARCHAR(255) Значение параметра

Чтобы получить список сотрудников, имеющих E-mail, можно использовать запрос:

SELECT O.ItemName, S.Value

FROM Objects O

INNER JOIN ObjType OT ON O.TypeId = OT.Id
INNER JOIN Strings S ON O.Id = S.ObjectId
INNER JOIN StrDesc SD ON S.TypeId = SD.Id

WHERE OT.Code = ‘EMPLOYEE’

AND SD.TypeId = OT.Id
AND SD.Code = ‘EMAIL’

Для получения списка всех сотрудников с указанием адресов электронной почты для тех, у кого они есть, запрос надо модифицировать:

SELECT O.ItemName, S.Value

FROM Objects O

INNER JOIN ObjType OT ON O.TypeId = OT.Id
LEFT JOIN Strings S ON O.Id = S.ObjectId
LEFT JOIN StrDesc SD ON S.TypeId = SD.Id

WHERE OT.Code = ‘EMPLOYEE’

AND SD.TypeId = OT.Id
AND SD.Code = ‘EMAIL’

Интересный эффект можно получить, создав у различных типов объектов атрибуты с одинаковыми значениями поля Code. Предположим, что у объектов типов EMPLOYEE (сотрудник) и FIRM (организация) есть строковые атрибуты с одинаковым именем ‘EMAIL’. Тогда можно одним запросом получить адресную книгу по всем известным в БД почтовым адресам.

SELECT O.ItemName, OT.ItemName, S.Value

FROM Objects O

INNER JOIN ObjType OT ON O.TypeId = OT.Id
INNER JOIN Strings S ON O.Id = S.ObjectId
INNER JOIN StrDesc SD ON S.TypeId = SD.Id

WHERE SD.Code = ‘EMAIL’
ORDER BY OT.ItemName, O.ItemName

Если впоследствии добавится новый объект с аналогичным атрибутом, его наличие будет немедленно учтено и он появится в списке без каких-либо модификаций БД и запросов. Добиться подобного результата в БД с традиционной структурой будет как минимум сложно.

Числовые атрибуты

Для простоты будем предполагать, что для хранения любых числовых атрибутов достаточно одного типа данных, например DECIMAL(20,4). В серверах, поддерживающих тип MONEY, удобно реализовывать хранение на базе этого типа. Если же логика БД требует выделения отдельного целого либо другого типа данных — это можно сделать путем выделения для него отдельной группы таблиц. В остальном хранение значений этого типа аналогично строковым. Описание атрибутов хранится в таблице:

PropDesc
Id INTEGER Первичный ключ
TypeId INTEGER REFERENCES ObjType(Id) Ссылка на тип объекта
Code CHAR(10) Краткое название параметра
ItemName CHAR(30) Полное название параметра, используется только для интерфейса

А сами атрибуты — в таблице:

Properties
Id INTEGER Первичный ключ
TypeId INTEGER REFERENCES PropDesc(Id) Ссылка на тип параметра
ObjectId INTEGER REFERENCES Objects(Id) Ссылка на объект
Value DECIMAL(20,4) Значение параметра. На серверах, поддерживающих тип данных MONEY (аналог Currency в Delphi), удобно использовать этот тип

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

Исторические атрибуты

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

Status
Id INTEGER Первичный ключ
TypeId INTEGER REFERENCES ObjType(Id) Ссылка на тип объекта
Code CHAR(10) Краткое название состояния
ItemName CHAR(30) Полное название состояния, используется только для интерфейса

Например, для человека допустимые состояния могут включать:

«WASBORN» — Дата рождения
«EMPLOYEED» — Принят на работу
«DISMISS» — Уволен

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

Сама история состояний хранится в таблице:

History
Id INTEGER Первичный ключ
StatusId INTEGER REFERENCES Status(Id) Ссылка на тип состояния
ObjectId INTEGER REFERENCES Objects(Id) Ссылка на объект
ItemDate DATETIME Дата возникновения состояния

При изменении состояния объекта в History добавляется запись со ссылкой на это состояние и датой его возникновения. Например, при увольнении сотрудника в его историю состояний добавляется запись с датой увольнения и ссылкой на состояние «DISMISS». Если этот же человек снова принят на работу — достаточно добавить новую запись с датой приема и ссылкой на состояние «EMPLOYEED». Список сотрудников, числящихся на работе на любую интересующую дату, может быть получен запросом вида:

SELECT O.ItemName, H.ItemDate AS Employeed

FROM Objects O

INNER JOIN ObjType OT ON O.TypeId = OT.Id
INNER JOIN History H ON H.ObjectId = O.Id
INNER JOIN Status S ON H.StatusId = S.Id

WHERE OT.Code = ‘EMPLOYEE’

AND S.Code = ‘EMPLOYEED’
AND H.ItemDate = (SELECT MAX(H1.ItemDate)

FROM History H1

INNER JOIN Status S1 ON H1.StatusId = S1.Id

WHERE H1.ObjectId = O.Id

AND H.ItemDate <= :DateParam
AND S1.Code IN (‘EMPLOYEED’, ‘DISMISS’))

Перечислимые атрибуты

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

EnumDesc
Id INTEGER Первичный ключ
TypeId INTEGER REFERENCES ObjType(Id) Ссылка на тип объекта
Code CHAR(10) Краткое название состояния
ItemName CHAR(30) Полное название состояния, используется только для интерфейса

Вторая — определяет возможные значения для перечислимого типа:

EnumValues
Id INTEGER Первичный ключ
DescId INTEGER REFERENCES EnumDesc(Id) Ссылка на тип перечисления
Code CHAR(10) Краткое название значения
ItemName CHAR(30) Полное название значения, используется только для интерфейса

Третья — непосредственно хранит значения, связанные с объектом:

Enums
Id INTEGER Первичный ключ
ValueId INTEGER REFERENCES EnumValues(Id) Ссылка на значение
ObjectId INTEGER REFERENCES Objects(Id) Ссылка на объект

Типичным примером использования этого типа данных является форма собственности фирмы. Для ее определения необходимо вставить в EnumDesc строку:

TypeId
Code
ItemName
Ссылка на ObjType для типа «FIRM» OWNERFORM Форма собственности

И в EnumValues:

DescId
Code
ItemName
Ссылка на EnumDesc для
ИЧП
Индивидуальное частное предприятие
типа «OWNERFORM»
ООО
Общество с ограниченной ответственностью
  
...
  

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

Продолжение статьи

Дополнительную информацию Вы можете получить в компании Interface Ltd.

Обсудить на форуме
Отправить ссылку на страницу по e-mail


Interface Ltd.
Тel/Fax: +7(095) 105-0049 (многоканальный)
Отправить E-Mail
http://www.interface.ru
Ваши замечания и предложения отправляйте автору
По техническим вопросам обращайтесь к вебмастеру
Документ опубликован: 26.04.02