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

на примере 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. Рассмотрим его подробнее.

Отображение

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

  • общий абстрактный предок для всех классов 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 as O join dbo.V_Object as 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 as O join dbo.V_Object as SO on O.OID = SO.OID
           inner join Company as 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 (http://www.osp.ru/dbms/1998/04-05/07.htm)
  4. Обсуждение вертикальной модели хранения атрибутов в fido.su.dbms
  5. Проблемы оптимизации в ООБД (дискуссия в fido.su.oop)
  6. Проблемы persistent layers (обсуждение в fido.su.dbms)

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

AttachmentSize
Package icon A2KernelDemo.zip58.84 KB