Создание сущности

Каждый бизнес-объект в Apostol CRM -- это сущность: набор SQL-файлов, которые описывают её таблицу, представления, CRUD-функции, REST API, обработчики событий и рабочий процесс. Это руководство проведёт вас через создание полноценной сущности с нуля.

Конвенция 8 файлов

Каждая сущность располагается в собственной директории внутри entity/object/document/ (для бизнес-объектов) либо entity/object/reference/ (для справочных данных). В каждой директории находятся следующие файлы:

ФайлНазначениеВыполняется при установкеВыполняется при обновлении
table.sqlCREATE TABLE, индексы, триггерыДаНет
view.sql3 представления в схеме kernel (CREATE OR REPLACE)ДаДа
routine.sqlФункции Create/Edit/GetДаДа
api.sqlПредставление схемы API + 6 CRUD-обёртокДаДа
rest.sqlФункция REST-диспетчераДаДа
event.sql9 функций-обработчиков событийДаДа
init.sqlКласс, тип, события, методы, переходыДаНет
create.psqlМастер-скрипт, подключающий все файлы----

Ключевое различие: table.sql и init.sql выполняются только при установке (--install). Все остальные файлы безопасно перезапускаются при обновлении (--update).

Выбор родительского класса

Каждая сущность наследуется от одного из двух классов:

  • Document -- для бизнес-объектов с жизненным циклом, доступом на основе областей, приоритетом и развитыми метаданными. Примеры: клиент, счёт, платёж, устройство, заказ.
  • Reference -- для справочных и каталожных данных с кодом, наименованием и описанием (с локализацией). Примеры: регион, валюта, страна, категория.

В этом руководстве создаётся сущность sensor (датчик) на основе Document.

Шаг 1. Создание таблицы

Таблица наследуется от родителя через внешний ключ. Триггер BEFORE INSERT копирует ID родителя в ID сущности, благодаря чему они используют один и тот же UUID.

CREATE TABLE db.sensor (
    id              uuid PRIMARY KEY,
    document        uuid NOT NULL REFERENCES db.document(id) ON DELETE CASCADE,
    code            text NOT NULL,
    value           numeric,
    unit            text,
    metadata        jsonb
);

COMMENT ON TABLE db.sensor IS 'Sensor.';

CREATE UNIQUE INDEX ON db.sensor (code);
CREATE INDEX ON db.sensor (document);

CREATE OR REPLACE FUNCTION db.ft_sensor_insert()
RETURNS trigger AS $$
BEGIN
  IF NEW.id IS NULL THEN
    SELECT NEW.document INTO NEW.id;
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql
   SECURITY DEFINER
   SET search_path = kernel, pg_temp;

CREATE TRIGGER t_sensor_insert
  BEFORE INSERT ON db.sensor
  FOR EACH ROW
  EXECUTE PROCEDURE db.ft_sensor_insert();

Основные правила:

  • Столбец id всегда является PRIMARY KEY и равен ID родителя
  • Внешний ключ на db.document(id) использует ON DELETE CASCADE
  • Триггер BEFORE INSERT копирует document в id
  • Добавляйте индексы для столбцов, по которым выполняется поиск

Шаг 2. Создание представлений

Каждой сущности нужны ровно 3 представления в схеме kernel:

  1. Sensor -- базовое представление данных, объединяющее сущность с её родителем
  2. AccessSensor -- список ID с фильтрацией по доступу (какие объекты текущий пользователь может читать)
  3. ObjectSensor -- полное представление объекта со всеми метаданными (сущность, класс, тип, состояние, владелец, область, область видимости)

Именно на представлении ObjectSensor базируется api.sensor. Оно объединяет всё: данные сущности, метаданные объекта, сведения о состоянии, владельца/оператора, область и область видимости.

Шаг 3. Написание CRUD-функций

Каждой сущности нужны как минимум функции Create*, Edit* и Get*.

CREATE OR REPLACE FUNCTION CreateSensor (
  pParent       uuid,
  pType         uuid,
  pCode         text,
  pLabel        text default null,
  pDescription  text default null,
  pValue        numeric default null,
  pUnit         text default null,
  pMetadata     jsonb default null
) RETURNS       uuid
AS $$
DECLARE
  uDocument     uuid;
  uClass        uuid;
  uMethod       uuid;
BEGIN
  SELECT class INTO uClass FROM db.type WHERE id = pType;

  IF GetEntityCode(uClass) <> 'sensor' THEN
    PERFORM IncorrectClassType();
  END IF;

  uDocument := CreateDocument(pParent, pType, pLabel, pDescription);

  INSERT INTO db.sensor (id, document, code, value, unit, metadata)
  VALUES (uDocument, uDocument, pCode, pValue, pUnit, pMetadata);

  uMethod := GetMethod(uClass, GetAction('create'));
  PERFORM ExecuteMethod(uDocument, uMethod);

  RETURN uDocument;
END;
$$ LANGUAGE plpgsql
   SECURITY DEFINER
   SET search_path = kernel, pg_temp;

Шаблон, общий для каждой функции Create*:

  1. Проверить, что тип принадлежит нужной сущности
  2. Вызвать функцию создания родителя: CreateDocument(...) или CreateReference(...)
  3. Вставить специализированную запись в db.sensor
  4. Выполнить метод create, чтобы запустить рабочий процесс

Функция Edit* строится по схожему шаблону: вызвать функцию редактирования родителя, затем обновить специализированные столбцы, используя coalesce() для сохранения существующих значений.

Шаг 4. Написание слоя API

Слой API предоставляет 6 публичных функций:

ФункцияНазначение
api.add_sensorСоздаёт новую запись, возвращает UUID
api.update_sensorОбновляет существующую запись
api.set_sensorUpsert (добавляет, если id равен NULL, иначе обновляет)
api.get_sensorВозвращает одну запись по ID с проверкой доступа
api.count_sensorВозвращает количество с поддержкой поиска и фильтрации
api.list_sensorВозвращает список с поиском, фильтрацией и постраничной выборкой

А также одно представление: api.sensor (на основе ObjectSensor).

CREATE OR REPLACE VIEW api.sensor
AS
  SELECT * FROM ObjectSensor;

CREATE OR REPLACE FUNCTION api.add_sensor (
  pParent       uuid,
  pType         uuid,
  pCode         text,
  pLabel        text default null,
  pDescription  text default null,
  pValue        numeric default null,
  pUnit         text default null,
  pMetadata     jsonb default null
) RETURNS       uuid
AS $$
BEGIN
  RETURN CreateSensor(pParent, coalesce(pType, GetType('default.sensor')),
    pCode, pLabel, pDescription, pValue, pUnit, pMetadata);
END;
$$ LANGUAGE plpgsql
   SECURITY DEFINER
   SET search_path = kernel, pg_temp;

Шаг 5. Написание REST-диспетчера

REST-диспетчер сопоставляет пути URL с функциями API. Подробности см. в руководстве REST-эндпоинты.

Каждая сущность получает 6 стандартных маршрутов плюс динамическое делегирование методов:

CREATE OR REPLACE FUNCTION rest.sensor (
  pPath       text,
  pPayload    jsonb default null
) RETURNS     SETOF json
AS $$
DECLARE
  r           record;
  e           record;
  arKeys      text[];
BEGIN
  IF pPath IS NULL THEN
    PERFORM RouteIsEmpty();
  END IF;

  IF current_session() IS NULL THEN
    PERFORM LoginFailed();
  END IF;

  CASE pPath
  WHEN '/sensor/type' THEN
    -- вернуть доступные типы для этой сущности
  WHEN '/sensor/method' THEN
    -- вернуть доступные методы для объекта
  WHEN '/sensor/count' THEN
    -- подсчёт объектов с поиском и фильтрацией
  WHEN '/sensor/set' THEN
    -- создать или обновить (upsert)
  WHEN '/sensor/get' THEN
    -- получить один объект по ID
  WHEN '/sensor/list' THEN
    -- список объектов с поиском, фильтрацией и постраничной выборкой
  ELSE
    RETURN NEXT ExecuteDynamicMethod(pPath, pPayload);
  END CASE;

  RETURN;
END;
$$ LANGUAGE plpgsql
   SECURITY DEFINER
   SET search_path = kernel, pg_temp;

Ветка ELSE автоматически обрабатывает действия рабочего процесса: /sensor/enable, /sensor/disable, /sensor/delete, /sensor/restore, а также любые пользовательские действия.

Шаг 6. Написание обработчиков событий

Каждой сущности нужны 9 обработчиков событий для стандартных действий жизненного цикла, а также обработчик drop для очистки:

CREATE OR REPLACE FUNCTION EventSensorCreate (
  pObject   uuid default context_object()
) RETURNS   void
AS $$
BEGIN
  PERFORM WriteToEventLog('M', 1000, 'create', 'Sensor created.', pObject);
END;
$$ LANGUAGE plpgsql;

Девять событий: Create, Open, Edit, Save, Enable, Disable, Delete, Restore, Drop. Каждое строится по одному шаблону -- записать действие в журнал и выполнить произвольную пользовательскую логику.

Обработчик Drop особый: он удаляет запись сущности из db.sensor до того, как каскад родителя удалит всё остальное.

Шаг 7. Регистрация сущности

Файл init.sql регистрирует сущность в иерархии классов Платформы:

CREATE OR REPLACE FUNCTION CreateEntitySensor (
  pParent       uuid
)
RETURNS         uuid
AS $$
DECLARE
  uEntity       uuid;
BEGIN
  -- Регистрация сущности
  uEntity := AddEntity('sensor', 'Sensor');

  -- Создание класса хотя бы с одним типом
  PERFORM CreateClassSensor(pParent, uEntity);

  -- Подключение REST-эндпоинта
  PERFORM RegisterRoute('sensor', AddEndpoint('SELECT * FROM rest.sensor($1, $2);'));

  RETURN uEntity;
END
$$ LANGUAGE plpgsql
   SECURITY DEFINER
   SET search_path = kernel, pg_temp;

Здесь выполняется:

  • AddEntity -- регистрирует имя сущности (должно быть уникальным)
  • AddClass -- создаёт класс в иерархии
  • AddType -- создаёт хотя бы один тип (формат: 'qualifier.entity')
  • AddDefaultMethods -- настраивает стандартный рабочий процесс из 4 состояний
  • RegisterRoute + AddEndpoint -- подключает REST-эндпоинт

Шаг 8. Подключение к родителю

Добавьте сущность в скрипты создания и обновления родителя.

В entity/object/document/create.psql:

\ir './sensor/create.psql'

В entity/object/document/update.psql:

\ir './sensor/update.psql'

В функции InitConfigurationEntity() конфигурации:

PERFORM CreateEntitySensor(GetClass('document'));

Контрольный список

После выполнения всех шагов ваша сущность будет иметь:

  • Таблицу базы данных с корректным наследованием
  • Три представления в схеме kernel для доступа к данным
  • CRUD-функции (Create, Edit, Get)
  • Шесть функций API (add, update, set, get, count, list)
  • REST-диспетчер с шестью стандартными маршрутами
  • Девять обработчиков событий для полного жизненного цикла
  • Интеграцию с рабочим процессом (состояния, методы, переходы)
  • Автоматическую REST-маршрутизацию через Платформу

Выполните ./runme.sh --install для первичной настройки или ./runme.sh --update, чтобы обновить процедуры и представления после изменений.