Создание сущности
Каждый бизнес-объект в Apostol CRM -- это сущность: набор SQL-файлов, которые описывают её таблицу, представления, CRUD-функции, REST API, обработчики событий и рабочий процесс. Это руководство проведёт вас через создание полноценной сущности с нуля.
Конвенция 8 файлов
Каждая сущность располагается в собственной директории внутри entity/object/document/ (для бизнес-объектов) либо entity/object/reference/ (для справочных данных). В каждой директории находятся следующие файлы:
| Файл | Назначение | Выполняется при установке | Выполняется при обновлении |
|---|---|---|---|
table.sql | CREATE TABLE, индексы, триггеры | Да | Нет |
view.sql | 3 представления в схеме kernel (CREATE OR REPLACE) | Да | Да |
routine.sql | Функции Create/Edit/Get | Да | Да |
api.sql | Представление схемы API + 6 CRUD-обёрток | Да | Да |
rest.sql | Функция REST-диспетчера | Да | Да |
event.sql | 9 функций-обработчиков событий | Да | Да |
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:
Sensor-- базовое представление данных, объединяющее сущность с её родителемAccessSensor-- список ID с фильтрацией по доступу (какие объекты текущий пользователь может читать)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*:
- Проверить, что тип принадлежит нужной сущности
- Вызвать функцию создания родителя:
CreateDocument(...)илиCreateReference(...) - Вставить специализированную запись в
db.sensor - Выполнить метод
create, чтобы запустить рабочий процесс
Функция Edit* строится по схожему шаблону: вызвать функцию редактирования родителя, затем обновить специализированные столбцы, используя coalesce() для сохранения существующих значений.
Шаг 4. Написание слоя API
Слой API предоставляет 6 публичных функций:
| Функция | Назначение |
|---|---|
api.add_sensor | Создаёт новую запись, возвращает UUID |
api.update_sensor | Обновляет существующую запись |
api.set_sensor | Upsert (добавляет, если 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, чтобы обновить процедуры и представления после изменений.