Разработка ядра информационной системы. Часть 3.

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

Небольшое отступление. Приведенные в заключительной статье цикла статье возможности ядра реализованы не полностью, поэтому исходные тексты пока не выложены для загрузки. Следует рассматривать статью скорее, как спецификацию того, что надо получить в итоге. См. также пример реализации на MS SQL Server. Дальнейшее развитие, включая разработку, мы предполагаем вести уже в вики-разделе сайта проекта.

Установка ядра

Прежде всего, необходимо установить на компьютере СУБД PostgreSQL версии 8.2 или выше. Дистрибутивы свободно доступны для загрузки на веб-сайте постгреса (http://www.postgresql.org/ftp/). В установочный комплект для Windows входит весь необходимый нам набор: сам сервер, консоль управления (pgAdmin), консоль для работы с SQL-запросами и скриптами, драйверы ODBC и клиентская библиотека для доступа. Установка обычно не вызывает трудностей, большинство параметров можно принять «по умолчанию».

После установки СУБД, запустите консоль управления (приложение PgAdmin III), соединитесь с сервером и создайте новую базу данных. Название непринципиально, пусть оно будет, например, meta4core. Обратите внимание, что кодировка новой базы должна быть UTF-8.


Создание БД из консоли управления

Созданная БД пока пуста. Следующим шагом будет установка нашего ядра. Архив с инсталлятором ядра необходимо скачать с вики-сайта проекта (http://meta4.arbinada.com), и распаковать. Он состоит пока только из одного файла с названием Install.Core.sql.

Далее, из консоли управления (меню Tools - Query tool) запустите SQL-консоль (приложение PgAdmin III Query), откройте в ней файл Install.Core.sql и запустите его на выполнение.


Запуск установки из SQL-консоли

Декларации

Начнем разработку примера с объявлений структурных сущностей (классов) и связей между ними. Нам потребуется создать в SQL-консоли новый файл, назовем его Sample.sql и сохраним где-нибудь на диске.


Пример в UML-нотации

Структура из примера отражает заказ (класс Order) клиентом (класс Customer) товара (класс Product). Заказ может содержать более одного товара требуемого количества по определенной цене (класс OrderItem).
Для объявления классов нам потребуется функция Class_Declare, для полей (атрибутов) и связей, соответственно, Field_Declare и Link_Declare. После объявления, класс необходимо явным образом cкомпилировать, используя функцию Class_Compile с параметром <Имя класса>. В итоге у нас должен получиться достаточно простой, на первый взгляд, код, который может быть легко сгенерирован из диаграммы классов средствами вашего любимого UML-редактора, если таковой имеется.

Декларация классов примера.

set search_path to Core;
/* Модуль определяет пространство имен */
select Module_Declare('Sample', 1, 0, 0);
 
/* Пользовательские типы */
select DataType_Declare('TProductCode', 'varchar(15)');
 
/* Классы */
select Class_Declare('Sample', 'Customer', 'Object');
select Field_Declare('Customer', 'Name', 'TShortString', not Nullable(), Identifier());
select Field_Declare('Customer', 'Address', 'TShortString', Nullable());
select Class_Compile('Customer');
 
select Class_Declare('Sample', 'Product', 'Object');
select Field_Declare('Product', 'Code', 'TProductCode', not Nullable(), Identifier());
select Field_Declare('Product', 'Description', 'TShortString', not Nullable());
select Field_Declare('Product', 'Status', 'int', not Nullable());
select Class_Compile('Product');
 
select Class_Declare('Sample', 'Order', 'Object');
select Field_Declare('Order', 'Number', 'int', not Nullable(), Identifier());
select Field_Declare('Order', 'OrderDate', 'datetime', not Nullable());
select Link_Declare('Customer', 'Order', 'Client', OneToMany(), Aggregation());
select Class_Compile('Order');
 
select Class_Declare('Sample', 'OrderItem', 'Object');
select Field_Declare('OrderItem', 'Number', 'int', not Nullable());
select Field_Declare('OrderItem', 'Amount', 'float', not Nullable());
select Field_Declare('OrderItem', 'Price', 'money', not Nullable());
select Link_Declare('Order', 'OrderItem', 'Order', OneToMany(), Composition());
select Link_Declare('Product', 'OrderItem', 'Product', OneToMany(), Aggregation());
select Class_Compile('OrderItem'); 
select Class_Compile('OrderItem');

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

Генерация

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

Создана схема (пространство имен) Sample. Имя схемы было задано нашим объявлением модуля.
На структурном уровне для всех классов созданы соответствующие таблицы с префиксом «C» внутри пространства имен Sample. Связи преобразованы в поля таблиц с именами, заданными нами при объявлении связи. Например, связь «Клиент - Заказ» сгенерировала поле ClientID типа TOID с обязательным заполнением (NOT NULL) в таблице COrder. Композитная связь «Заказ - Строка заказа», кроме того, создала в таблице COrderItems второй ключ по полям OrderID и Number.


Сгенерированная таблица класса Order, видимая из pgAdmin

На уровне интерфейсов для работы с классами созданы проекции (view), соответствующие их именам. Посмотрим, что получилось внутри проекций.

Сгенерированная проекция для класса Order.

CREATE OR REPLACE VIEW Sample.Order AS
SELECT 
  CObject.ObjectID,
  CObject.ClassID, Classes.ClassName,
  CObject.State,
  COrder.ClientID,
  CCustomer.Name AS Client /* Name идентифицирует клиента */,
  COrder.Number, COrder.OrderDate
FROM 
  COrder
  INNER JOIN CObject ON COrder.ObjectID = CObject.ObjectID
  INNER JOIN Classes ON CObject.ClassID = Classes.ClassID
  INNER JOIN CCustomer ON CCustomer.ObjectID = COrder.ClientID
WHERE
  CObject.State = 1 /* только активные объекты */;

Как вы помните, манипуляции данными производятся только через проекции или посредством явных вызовов функций (методов) классов. Поэтому для нашей проекции система также создала и правила (rules), позволяющие создавать модифицировать и удалять данные непосредственно в проекциях.

Правило и реализующая его функция для создания класса Order.

CREATE OR REPLACE FUNCTION Sample._RuleProc_COrder_Create(
  ObjectID TOID, 
  ClassName TSystemName,
  State int2,
  ClientID TOID, 
  Number int2, 
  OrderDate datetime
) RETURNS int AS $$
BEGIN
  if (ObjectID IS NULL) then
    ObjectID = Core.Object_NewID();
  end if;
  if (State IS NULL) then
      State = 1;
  end if;
  INSERT INTO Object (ObjectID, ClassName, State) 
    VALUES (ObjectID, ClassName, State);
  INSERT INTO COrder (ObjectID, ClientID, Number, OrderDate) 
    VALUES (ObjectID, ClientID, Number, OrderDate);  
  RETURN 0;
END;
$$ LANGUAGE plpgsql;
 
CREATE OR REPLACE RULE _Rule_Order_Create AS ON INSERT
  TO Sample.Order DO INSTEAD
SELECT Sample._RuleProc_COrder_Create(
  CAST(NEW.ObjectID AS TOID), 
  CAST(NEW.ClassName AS TSystemName),
  CAST(NEW.State AS int2),
  CAST(NEW.ClientID AS TOID),
  CAST(NEW.Number AS int2),
  CAST(NEW.OrderDate as datetime));

Манипулируем объектами

Для создания объекта проще всего вызвать конструктор Create соответствующего класса со всеми инициализирующими значениями. Однако есть не менее простой способ – выполнить оператор INSERT на проекции данного класса. В этом случае необязательные параметры можно не указывать.

Пример создания объекта класса Order.

INSERT INTO Sample.Order (ObjectID, ClientID, Number, OrderDate)
SELECT 
  Core.Object_NewID(), 
  1230000000000004567, 
  1, 
  current_date;

Аналогичным образом вы можете получать или изменять значения полей класса через методы типа Get<Имя поля> и Set<Имя поля>, либо воспользоваться оператором UPDATE на проекции. И только во втором случае вы имеете возможность:

  • изменять значения сразу нескольких полей
  • изменять значения сразу у нескольких объектов

Пример манипуляции объектами класса Customer.

UPDATE Customer
SET 
  Name = 'АО Мета4', 
  Address = 'meta4.arbinada.com'
WHERE 
  ObjectID = 1230000000000004568;

Наконец, удалять объект можно как прямым вызовом деструктора Destroy, так и оператором DELETE.

Немного императивного программирования

Чтобы убедиться в полезности механизма событий, попробуем решить простую задачку. При добавлении новой строки заказа генерировать ее уникальный в пределах документа номер, если тот не был задан.
Для этого нам потребуется написать обработчик события BeforeCreate класса OrderItem. Это событие происходит непосредственно перед созданием экземпляра класса, ядро вызывает все описанные в БД функции-обработчики со списком параметров, соответствующим полям класса. При этом программист может изменить значения всех полей, кроме ObjectID и ClassName. Обработчики вызываются в контексте транзакции, любая генерация ошибки приведет к ее откату: объект не будет создан.

Пример создания обработчика события.

CREATE OR REPLACE FUNCTION Sample.OrderItem_BeforeCreate_InitNumber(
  ObjectID TOID, 
  ClassName TSystemName, 
  inout State int2,
  inout OrderID TOID,
  inout Number int2, 
  inout ProductID TOID, 
  inout Amount real,
  inout Price money
) AS $$
BEGIN
  SELECT Number = (SELECT max(Number) FROM OrderItems WHERE OrderItems.OrderID = OrderID);
END;
$$ LANGUAGE plpgsql;

Каков механизм вызовов? После создания функции-обработчика необходимо вызвать перекомпиляцию класса. Ядро анализирует сигнатуры функций и определяет, например, что OrderItem_BeforeCreate_InitNumber – это обработчик события BeforeCreate класса OrderItem. Соответственно, в реализующей правило функции создания добавится вызов нашего обработчика. Вот так:

CREATE OR REPLACE FUNCTION Sample._RuleProc_COrderItem_Create(
  ObjectID TOID, 
  ClassName TSystemName,
  State int2,
  OrderID TOID,
  Number int2, 
  ProductID TOID, 
  Amount real,
  Price money
) RETURNS int AS $$
BEGIN
  if (ClassName IS NULL) then
    ClassName = 'OrderItem';
  end if;
  if (State IS NULL) then
      State = 1;
  end if;
  INSERT INTO Object (ObjectID, ClassName, State) 
    VALUES (ObjectID, ClassName, State);
  /* вызов обработчика перед созданием объекта */
  SELECT Sample.OrderItem_BeforeCreate_InitNumber(ObjectID, OrderID, Number, ProductID, Amount, Price);
  INSERT INTO COrderItem (ObjectID, OrderID, Number, ProductID, Amount, Price) 
    VALUES (ObjectID, OrderID, Number, ProductID, Amount, Price);  
  RETURN 0;
END;
$$ LANGUAGE plpgsql;

Число обработчиков событий не ограничено, в общем случае порядок их вызова не определен.

Итоги

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

Скептики и сторонники «чистоты» подходов могут возразить: «А нужно ли это в принципе?» Отставим теоретические споры о том, что «функция СУБД – выдать по запросу два поля, а сервера приложений – сложить их» в стороне. Практика показывает, что для многих типов задач использование выделенного сервера приложений является невыгодным с многих точек зрения: с технической (неоправданное усложнение архитектуры, накладные расходы на обработку данных), с экономической (удорожание разработки и поддержки) и с управленческой (новые риски). И дело далеко не в масштабе проекта: существует, например, система класса ERP «Монолит SQL», реализованная в подобной архитектуре и успешно функционирующая на нескольких тысячах рабочих мест.

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

Даже если описанная технология не показалась вам подходящей применительно к вашей текущей задаче, но, получив представление, вы станете рассматривать ее в качестве альтернативы, будем считать цель публикаций достигнутой.
Quot capita, tot sensus (лат., сколько голов, столько умов)

Сергей Тарасов, июль 2007

Статья также опубликована в журнале "Мир ПК" №9-2007 (анонс - в номере, текст - на прилагаемом компакт-диске).