Реализация сервера объектного представления средствами реляционной СУБД

| рубрика: Проектирование | автор: st
Метки: , ,

на примере MS SQL Server 2000 и CASE ERwin

Зачем это нужно?

Объектный подход, используемый для разработки программных систем доказал на практике свои преимущества. К сожалению, кроме приятных возможностей он привнёс и новые проблемы. В классической парадигме "программа = алгоритмы + данные" месту объекта не находится, поскольку объект есть и данные и процедуры, как говорится, "в одном флаконе". Это привело в частности к тому, что до сих пор не существует общих решений для самого широкого класса ПО - приложений баз данных. Данная ситуация и некоторые способы её решения хорошо описана, например, в [1].

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

Исходя из старого, но верного правила "чем дальше источник данных от мест их обработки, тем обработка медленнее" [2], я придерживаюсь точки зрения, что методика "пассивный объект в БД - активный объект на клиенте БД" является узким местом в информационной системе. Очевидно, что использование объектов в адресном пространстве СУБД более эффективно с точки зрения производительности, чем в адресном пространстве программы - клиента СУБД. Клиентом может быть и сервер приложений, суть не меняется: затраты на сериализацию/десериализацию и маршаллинг всегда будут присутствовать в любой подобной схеме. Примеров таких решений масса: ECO (Bold), Hibernate (NHibernate), XPO, ObjectSpaces, InstantObjects и многие другие OPF (Objects Persistence Framework).

ORM - это, по большому счету, просто заменитель традиционной технологии работы через DataSet, а не система управления объектами (СУОб, по аналогии с СУБД). Вместо работы со строками и полями DataSet, выборками на стандартном SQL, используется работа с объектами и их свойствами, выборками коллекций, в общем случае, на нестандартном языке. Отсюда вытекают все плюсы и минусы. Итак, известные недостатки существующих решений проекций объектов на реляционную структуру:

  • Высокие накладные расходы (overhead) для манипуляций с объектами вне адресного пространстве сервера (отсутствует in-process)
  • Невозможность эффективно разделять логику между приложениями, работающими с одной БД (объекты "живут" в адресном пространстве клиентов)
  • Проблемы оптимизации и тонкой настройки взаимодействия с БД (ярким примером является требования разработчиков о необходимости поддержки отображения на хранимые процедуры, которое вводится, например в нынешней 3-й версии Hibernate)

Даже для частичного решения этих проблем необходимо создавать собственный сервер приложений, что является совсем не тривиальной технической и совсем не очевидной экономической задачей (необходимость в системных разработчиках, повышение TCO).

Приведу одну простую иллюстрацию. Наверняка, вы знаете, что реляционная СУБД хранит свои данные отнюдь не в виде таблиц или тем более реляционных отношений - множеств кортежей, которыми оперирует программист. СУБД использует внутренние эффективные оптимизированные структуры, в том числе древовидные, размещаемые в оперативной и долговременной памяти компьютера. Это подсистема хранения. Подсистема выполнения запросов к данным на языке SQL интерпретирует запрос и транслирует его в последовательность низкоуровневых операций поиска и чтения по этим внутренним нереляционным структурам. Получается, что сторонники разделения сред хранения состояний и использования объектов практически предлагают разделить среды хранения данных и выполнения SQL-запросов в СУБД. Более того, разрешается прямой (т.е. хакерский) доступ к таким структурам, минуя интерфейс языка запросов. Подобная ситуация наблюдалась с настольными СУБД типа FoxPro/dBase/Paradox, использовавшимися 10-15 лет назад в многопользовательском варианте на основе файл-сервера.

Являясь сторонником развития технологии баз данных в сторону объектных СУБД, я надеюсь в обозримом будущем сделать проект автоматизации с использованием промышленной версии таковой. А пока же у нас есть промышленные реляционные СУБД, есть математический аппарат в основе используемой модели данных, международные стандарты и развитая инфраструктура для разработки приложений и эксплуатации [3].

Как нам обустроить РСУБД?

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

Другой подход состоит в максимальном использовании стандартных средств самой СУБД, в частности механизма представлений (view) и языка запросов SQL. Рассмотрим его подробнее.

Отображение (Mapping)

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

  • общий абстрактный предок для всех классов Object. Каждый объект имеет уникальный в пределах информационной системы генерируемый ключ OID
  • подклассу соответствует отдельная таблица; имя таблицы совпадает с именем класса; связь с таблицей суперкласса "один к одному" по ключу OID
  • строки таблиц соответствуют объектам
  • атрибуты классов соответствуют столбцам таблицы; атрибуты суперкласса, за исключением OID, не дублируются в таблице подклассов
  • каждому классу в системе соответствует одна проекция (view), с именем V_ИмяКласса, содержащая все атрибуты как самого класса, так и суперклассов

Метаданные

Важной частью информационной системы являются метаданные. В приложении к объектам корректнее использовать термин "метаинформация", но в рамках статьи будем использовать традиционный. Не рассматривая вопросы необходимости метаданных для пользователя, определимся, зачем они будут нужны нам. Итак:

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

Структура для хранения метаданных может иметь следующий вид:

Итак, таблица Object будет содержать атрибуты корневого суперкласса. Мы ввели в нее атрибут Deleted, который служит для моделирования "мусорной корзины": логического удаления объектов с возможностью восстановления.

Таблица Classes содержит характеристики класса: имя (является ключом), пользовательское название, ссылка на суперкласс (пустая для корневого), признак иерархической организации объектов (влияет на генерацию таблиц, например в создаваемой таблице может быть создано поле для ссылки на родителя), признак "собственная проекция" (не генерируется автоматически, создается программистом).

Таблица Attributes содержит характеристики атрибутов класса: имя, пользовательское название, тип (например, одно из значений: целое, строка, дата и время, вещественное, деньги, ссылка на объект), класс объектов (для типа атрибутов "объектная ссылка"), является ли альтернативным ключом для поиска и однозначной идентификации объекта данного класса (таковых атрибутов может быть несколько, в этом случае они образуют составной уникальный ключ).

Таблица Links содержит характеристики связей между классами: главный класс и атрибут, починенный класс и атрибут, пользовательское название связи, признак "обязательная связь" (подчиненный не может быть создан без главного).

Для декларации классов, атрибутов и связей создадим простой интерфейс в виде хранимых процедур. Например, если мы захотим объявить классы Компания (Company) и Контактное лицо (Person), связанные между собой отношением "один ко многим", то декларация может иметь вид:

exec Class_Declare
    @Class = 'Company',
    @Superclass = 'Object',
    @Caption = 'Компания';

exec Attribute_Declare
    @Class = 'Company',
    @Name = 'Name',
    @Caption = 'Name',
    @Type = 'TString',
    @IsAltKey = 1;

exec Class_Declare
    @Class = 'Person',
    @Superclass = 'Object',
    @Caption = 'Контактное лицо';

exec Attribute_Declare
    @Class = 'Person',
    @Name = 'FirstName',
    @Caption = 'Имя',
    @Type = 'TPersonName',
    @IsAltKey = 1;

exec Attribute_Declare
    @Class = 'Person',
    @Name = 'LastName',
    @Caption = 'Фамилия',
    @Type = 'TPersonName',
    @IsAltKey = 1;

exec Attribute_Declare
    @Class = 'Person',
    @Name = 'CompanyID',
    @Caption = 'Компания',
    @Type = 'TOID',
    @ObjClass = 'Company';

exec Link_Declare
    @Class = 'Company',
    @AttrName = 'OID',
    @LinkedClass = 'Person',
    @LinkedAttrName = 'CompanyID',
    @Caption = 'является сотрудником компании',
    @Mandatory = 1;

Автогенерация структур

Согласно принятым положениям о проекции классов на таблицы, мы должны создать для объявленных нами классов Company и Person, производных от Object, таблицы.

CREATE TABLE Company (
 OID  TOID,
 Name TString128 NOT NULL,
 CONSTRAINT PK_Company PRIMARY KEY (OID),
 CONSTRAINT FK_Company_Object FOREIGN KEY (OID) REFERENCES Object,
 CONSTRAINT AK_Company UNIQUE (Name)
)
CREATE TABLE Person (
 OID       TOID,
 FirstName TPersonName NOT NULL,
 LastName  TPersonName NOT NULL,
 CompanyID TOID,
 CONSTRAINT PK_Person PRIMARY KEY NONCLUSTERED (OID),
 CONSTRAINT FK_Person_Object FOREIGN KEY (OID) REFERENCES Object,
 CONSTRAINT FK_Person_Company FOREIGN KEY (CompanyID) REFERENCES Company,
 CONSTRAINT AK_Person UNIQUE (CompanyID, FirstName, LastName)
)

Для того, чтобы работать с объектами средствами SQL нам понадобятся представления V_ИмяКласса. Например, для объявленных нами классов Company и Person они будут такими:

CREATE VIEW V_Object AS
SELECT OID, Class, Deleted
FROM dbo.Object O
WHERE (Deleted = 0)
;
CREATE VIEW dbo.V_Company WITH VIEW_METADATA AS
SELECT SO.*, O.Name AS [Name]
FROM Company O JOIN dbo.V_Object SO ON O.OID = SO.OID
;
CREATE VIEW dbo.V_Person WITH VIEW_METADATA AS
SELECT
    SO.*,
    O.FirstName AS [FirstName],
    O.MiddleName AS [MiddleName],
    O.LastName AS [LastName],  /* отображаем ключевой атрибут связанного объекта */
    O.CompanyID AS [CompanyID],
    L1.Name AS [Company]
FROM Person O JOIN dbo.V_Object SO ON O.OID = SO.OID
    INNER JOIN Company L1 ON O.CompanyID = L1.OID

Очевидно, что процедура создания таблиц (или скрипта для их создания) и генерация таких представлений на основе метаданных может быть поручена программе. Для этих целей были написаны две несложные процедуры Metadata_GenerateTable и Metadata_GenerateView, принимающие параметр: имя класса. Теперь сценарий создания в среде нового класса выглядит так:

  • Декларация класса, атрибутов и связей (с использованием процедур Class_Declare, Attribute_Declare и Link_Declare)
  • Генерация таблиц или скрипта для их создания (Metadata_GenerateTable)
  • Генерация проекции (Metadata_GenerateView)

После этого прикладной программист получает в свое распоряжение проекцию V_ИмяКласса, с которой он может работать точно так же, как и с простой таблицей: делать выборки, используя стандартный SQL. Кроме этого, конечно, нужно иметь возможность создавать, модифицировать и удалять объекты, чем мы сейчас и займемся.

Автогенерация кода методов

Средства MSSQL предоставляют возможность создавать для представлений триггеры, обрабатывающие события вставки, модификации и удаления данных. Очевидно, что метаданных нам хватит для генерации таких триггеров при помощи созданной для этой цели процедуры Metadata_GenerateProcs с параметром "имя класса". Например, для класса Company триггер на вставку будет выглядеть следующим образом:

CREATE TRIGGER V_Company_Create ON V_Company
INSTEAD OF INSERT
AS
BEGIN
   SET NOCOUNT ON
   DECLARE @Ret INT
   DECLARE @OID TString, @Name TString
   SELECT TOP 1 @OID = convert(VARCHAR(16), I.OID), @Name = O.Name
   FROM V_Company O, inserted I
   WHERE O.OID <> I.OID AND O.[Name] = I.[Name]
   IF @@rowcount > 0
   BEGIN
      ROLLBACK TRAN
      RAISERROR ('Компания уже существует: OID: %s Name: %s', 11, 1, 'Company', @OID, @Name)
      RETURN
   END
   INSERT INTO OBJECT ([OID], [Class], [Deleted])
   SELECT [OID], 'Company', 0
   FROM inserted
   IF @@error <> 0
   BEGIN
      ROLLBACK TRANSACTION
      RETURN
   END
   INSERT INTO Company ([OID], [Name])
   SELECT [OID], [Name]
   IF @@error <> 0
   BEGIN
      ROLLBACK TRANSACTION
      RETURN
   END
END

Аналогичным образом генерируются триггеры для модификации и удаления данных. Триггер на delete, в соответствии с логикой "корзины", не удаляет записи, а просто меняет атрибут Object.Deleted = 1. С этого момента запись перестает быть видимой в представлении.

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

INSERT INTO V_Company (OID, Name) VALUES (12345, 'ТОО Рога и копыта')

Наращиваем возможности среды

То, что у нас получилось на прошлом этапе - всего лишь реализация отображения (mapping), хотя и с дополнительными важными возможностями в виде метаданных и "корзины", что само по себе еще недостаточно для полноценной работы с ООСУБД. Необходимы, как минимум, сервис безопасности (разделение прав доступа, аудит) и хотя бы простейшая реализация обработчиков событий.

Ограничения доступа

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

  • Ограничения прав на создание объектов данного класса. Решается встроенными средствами СУБД: предоставление прав на insert для view V_ИмяКласса
  • Ограничение прав на модификацию и удаление данных. Решается проверкой соответствующих прав в триггере на update и delete.
  • Ограничение прав на чтение данных. Решается введением дополнительной проверки в условии where или введением соединения с таблицей (матрицей) прав только на уровне view V_Object (класс объекта известен на этом уровне).

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

Подробнее о моделировании прав доступа см. статью "Реализация ядра безопасности в информационной системе". Вам потребуется только добавить код в триггеры для реализации проверок.

Аудит

Часто встречающая задача: хранить историю операций пользователей с данным объектом. Решается не просто, а очень просто.

Добавляем два класса: "системный пользователь" (SysUser), который однозначно идентифицируется по имени регистрации пользователя (Login) SQL Server и "системное событие" (для простейшего случая нам потребуются 3 события: Create, Update, Delete). В таблицу SysLog будем заносить все операции, которые производит пользователь над объектами. Для этого в автоматически генерируемые триггеры нужно добавить всего несколько строчек кода. Например, для триггера insert вот такие:

INSERT INTO SysLog (UserID, ObjectID, EventID, [Date], Message)
SELECT
    (SELECT OID FROM SysUser WHERE Login = SYSTEM_USER),
    I.OID,
    (SELECT OID FROM V_SysEvent WHERE Name = 'Create'),
    getdate(), 'Комментарий'
FROM inserted I

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

Проверки ссылочной целостности

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

Обработчики событий

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

ИмяКласса_ИмяСобытия_ИмяОбработчика[_]

где - ИмяСобытия - Create, Update, Delete; - ИмяОбработчика - идентификатор, который назначает прикладной программист; - подчеркивание в конце или его отсутствие - признак пре- или пост-обработчика

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

Интеграция со средой моделирования

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

Пример использования

Имеется следующая информационная модель в ERwin:

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

/*** Classes declaration ***/
exec spDeclareClass @Class = 'PrKind', @Superclass = 'Object',
  @Caption = 'Product kind'

exec spDeclareAttribute @Class = 'PrKind', @Name = 'OID',
  @Caption = 'OID', @Type = 'TOID'
exec spDeclareAttribute @Class = 'PrKind', @Name = 'Code',
  @Caption = 'Code', @Type = 'TString16'
exec spDeclareAttribute @Class = 'PrKind', @Name = 'Name',
  @Caption = 'Name', @Type = 'TString128'
go

exec spDeclareClass @Class = 'PrList', @Superclass = 'Object',
  @Caption = 'Price list'

exec spDeclareAttribute @Class = 'PrList', @Name = 'OID',
  @Caption = 'OID', @Type = 'TOID'
exec spDeclareAttribute @Class = 'PrList', @Name = 'PriceDir',
  @Caption = 'Price direction', @Type = 'char(1)'
exec spDeclareAttribute @Class = 'PrList', @Name = 'CurrencyID',
  @Caption = 'Currency', @Type = 'TOID' , @ObjClass = 'Currency'
exec spDeclareAttribute @Class = 'PrList', @Name = 'QuantityB',
  @Caption = 'Quantity bottom', @Type = 'int'
exec spDeclareAttribute @Class = 'PrList', @Name = 'Price',
  @Caption = 'Price', @Type = 'money'
exec spDeclareAttribute @Class = 'PrList', @Name = 'VDate',
  @Caption = 'Value date', @Type = 'datetime'
exec spDeclareAttribute @Class = 'PrList', @Name = 'ProductID',
  @Caption = 'Product or service', @Type = 'TOID' , @ObjClass = 'Product'
exec spDeclareAttribute @Class = 'PrList', @Name = 'HCompanyID',
  @Caption = 'Holding company', @Type = 'TOID' , @ObjClass = 'HCompany'
exec spDeclareAttribute @Class = 'PrList', @Name = 'PLClGroupID',
  @Caption = 'Client group', @Type = 'TOID' , @ObjClass = 'PLClGroup'
go

exec spDeclareClass @Class = 'Product', @Superclass = 'Object',
  @Caption = 'Product'

exec spDeclareAttribute @Class = 'Product', @Name = 'OID',
  @Caption = 'OID', @Type = 'TOID'
exec spDeclareAttribute @Class = 'Product', @Name = 'Code',
  @Caption = 'Code', @Type = 'TProductCode'
exec spDeclareAttribute @Class = 'Product', @Name = 'Name',
  @Caption = 'Product name', @Type = 'TCaption'
exec spDeclareAttribute @Class = 'Product', @Name = 'PrTypeID',
  @Caption = 'Product type', @Type = 'TOID' , @ObjClass = 'PrType'
exec spDeclareAttribute @Class = 'Product', @Name = 'PrStatusID',
  @Caption = 'Product status', @Type = 'TOID' , @ObjClass = 'PrStatus'
exec spDeclareAttribute @Class = 'Product', @Name = 'PrKindID',
  @Caption = 'Product kind', @Type = 'TOID' , @ObjClass = 'PrKind'
exec spDeclareAttribute @Class = 'Product', @Name = 'PrModelID',
  @Caption = 'Product model', @Type = 'TOID' , @ObjClass = 'PrModel'
exec spDeclareAttribute @Class = 'Product', @Name = 'DocProcTypeID',
  @Caption = 'Document processing type', @Type = 'TOID' , @ObjClass = 'DocProcType'
go

exec spDeclareClass @Class = 'PrStatus', @Superclass = 'Object',
  @Caption = 'Product status'

exec spDeclareAttribute @Class = 'PrStatus', @Name = 'OID',
  @Caption = 'OID', @Type = 'TOID'
exec spDeclareAttribute @Class = 'PrStatus', @Name = 'Code',
  @Caption = 'Code', @Type = 'TProductStatusCode'
exec spDeclareAttribute @Class = 'PrStatus', @Name = 'Caption',
  @Caption = 'Caption', @Type = 'TCaption'
go

exec spDeclareClass @Class = 'PrType', @Superclass = 'Object',
  @Caption = 'Product type'

exec spDeclareAttribute @Class = 'PrType', @Name = 'OID',
  @Caption = 'OID', @Type = 'TOID'
exec spDeclareAttribute @Class = 'PrType', @Name = 'Code',
  @Caption = 'Code', @Type = 'varchar(2)'
exec spDeclareAttribute @Class = 'PrType', @Name = 'Caption',
  @Caption = 'Caption', @Type = 'TCaption'
go

exec spDeclareClass @Class = 'Tax', @Superclass = 'Object',
  @Caption = 'Tax'

exec spDeclareAttribute @Class = 'Tax', @Name = 'OID',
  @Caption = 'OID', @Type = 'TOID'
exec spDeclareAttribute @Class = 'Tax', @Name = 'Code',
  @Caption = 'Code', @Type = 'TString16'
exec spDeclareAttribute @Class = 'Tax', @Name = 'Caption',
  @Caption = 'Caption', @Type = 'TCaption'
go

exec spDeclareClass @Class = 'TaxGroup', @Superclass = 'Object',
  @Caption = 'Product tax group'

exec spDeclareAttribute @Class = 'TaxGroup', @Name = 'OID',
  @Caption = 'OID', @Type = 'TOID'
exec spDeclareAttribute @Class = 'TaxGroup', @Name = 'Code',
  @Caption = 'Code', @Type = 'TString16'
exec spDeclareAttribute @Class = 'TaxGroup', @Name = 'Caption',
  @Caption = 'Tax group name', @Type = 'TCaption'
exec spDeclareAttribute @Class = 'TaxGroup', @Name = 'TaxID',
  @Caption = 'Tax', @Type = 'TOID' , @ObjClass = 'Tax'
go
/*** Declare links ***/
exec spDeclareLink @Class = 'PLClGroup', @AttrName = 'OID', @LinkedClass = 'PrList',
  @LinkedAttrName = 'PLClGroupID', @Caption = 'qualifies', @Mandatory = 1
exec spDeclareLink @Class = 'HCompany', @AttrName = 'OID', @LinkedClass = 'PrList',
  @LinkedAttrName = 'HCompanyID', @Caption = 'has', @Mandatory = 1
exec spDeclareLink @Class = 'Currency', @AttrName = 'OID', @LinkedClass = 'PrList',
  @LinkedAttrName = 'CurrencyID', @Caption = 'used in the', @Mandatory = 1
exec spDeclareLink @Class = 'Product', @AttrName = 'OID', @LinkedClass = 'PrList',
  @LinkedAttrName = 'ProductID', @Caption = 'has', @Mandatory = 1
exec spDeclareLink @Class = 'DocProcType', @AttrName = 'OID', @LinkedClass = 'Product',
  @LinkedAttrName = 'DocProcTypeID', @Caption = 'is attribute of', @Mandatory = 1
exec spDeclareLink @Class = 'PrModel', @AttrName = 'OID', @LinkedClass = 'Product',
  @LinkedAttrName = 'PrModelID', @Caption = 'is a base of', @Mandatory = 0
exec spDeclareLink @Class = 'PrKind', @AttrName = 'OID', @LinkedClass = 'Product',
  @LinkedAttrName = 'PrKindID', @Caption = 'qualifies', @Mandatory = 1
exec spDeclareLink @Class = 'PrStatus', @AttrName = 'OID', @LinkedClass = 'Product',
  @LinkedAttrName = 'PrStatusID', @Caption = 'determines', @Mandatory = 1
exec spDeclareLink @Class = 'PrType', @AttrName = 'OID', @LinkedClass = 'Product',
  @LinkedAttrName = 'PrTypeID', @Caption = 'qualifies', @Mandatory = 1
exec spDeclareLink @Class = 'Tax', @AttrName = 'OID', @LinkedClass = 'TaxGroup',
  @LinkedAttrName = 'TaxID', @Caption = 'groups in the', @Mandatory = 1
go

После прогона скрипта декларации создаем таблицы, проекции и триггеры:

exec Metadata_GenerateTable @Class = 'PrKind'
exec Metadata_GenerateTable @Class = 'PrList'
exec Metadata_GenerateTable @Class = 'Product'
exec Metadata_GenerateTable @Class = 'PrStatus'
exec Metadata_GenerateTable @Class = 'PrType'
exec Metadata_GenerateTable @Class = 'Tax'
exec Metadata_GenerateTable @Class = 'TaxGroup'
exec Metadata_GenerateView /* перегенерация всех представлений */
exec Metadata_GenerateProcs /* перегенерация всех триггеров */

Таблицы и представления для "Product taxes" и "Tax values" создаются в примере вручную, но это решение было принято из соображений оптимизации; ничто не мешает в общем случае представить связь "многие ко многим" связующим классом.

Заключение

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

В качестве примеров реализации я могу привести заказную CRM-систему с распределенной БД (репликация). Область использования такой архитектуры - корпоративные приложения, особенно в ситуации, когда с одной БД предполагается работа более одного приложения: решение позволяет прозрачно для прикладного программиста повторно использовать логику, реализованную на сервере БД. Вторая область - тиражируемые решения, когда заказчику нужен настраиваемый функционал, а тип СУБД его интересует в гораздо меньшей степени (зачастую, не интересует и вовсе). Примеры: КИС "БОСС-компания", с некоторыми натяжками "1С" и наш проект NEXUS.

В примере используется MS SQL Server, тем не менее, нет никаких препятствий реализации подобной архитектуры средствами Sybase, Oracle, Interbase.

Литература

  1. Корпоративные системы на основе CORBA. : Пер. с англ. - М. Издательский дом "Вильямс", 2000
  2. Бойко В.В., Савинков В.М. Проектирование информационных систем. – М.: Финансы и статистика. 1988
  3. Когаловский М.Р., Абстракции и модели в системах баз данных. - Журнал СУБД #04-05/98
  4. Обсуждение вертикальной модели хранения атрибутов в fido.su.dbms
  5. Проблемы оптимизации в ООБД (дискуссия в fido.su.oop)

Сергей Тарасов, октябрь 2003 С поправками, февраль 2005

[Package icon A2KernelDemo.zip