В этой статье рассматриваются способы сохранения сущности, имеющей, помимо скалярных полей, табличные данные. Например, поступление, которое имеет дату и номер, а также список товаров:
Данные из карточки позиции поступления передаются на родительскую форму и добавляются в таблицу, откуда отправляются на сервер или передаются в карточку позиции для редактирования.
В базе данных поступление представлено двумя таблицами:
Есть два способа сохранения таких сущностей:
последовательный вызов двух команд, которые отдельно сохраняют скалярные и табличные данные;
создание на форме единого JSON-объекта, который отправляется на сервер одним запросом.
Две команды SaveCommand
Первый способ заключается в использовании двух последовательных команд. Первая сохраняет скалярные данные и возвращает на форму идентификатор добавленной записи. Вторая сохраняет табличные данные, используя результат выполнения первой команды, в котором хранится идентификатор новой записи.
С необязательным предложением RETURNING команда INSERT возвращает значение колонки income_id для добавленной записи. Это значение вернется на форму и будет храниться в результате команды IncomeInsertSaveCommand.
Как вариант, вместо предложения RETURNING можно использовать функцию currval для получения текущего значения последовательности. Тогда запрос на сохранение скалярных данных будет иметь вид:
Или сразу использовать в DatabaseTableSetDataConnection для сохранения табличных данных, который вызывается во второй команде IncomeItemDatabaseTableSaveCommand:
В параметре IncomeIdобращаемся к результату выполнения команды, а не вызываем ее повторно.
Команда SaveCommand возвращает результат первого SqlQuery типа Insert первого SetDataConnection. Если SqlQuery типа Insert не указан в SetDataConnection, то будет возвращаться результат первого SqlQuery типа Update, если такой описан в сохраняющем соединении с данными. Иначе будет возвращаться результат первого SqlQuery типа Delete.
Недостаток такого варианта в том, что если команда сохранения табличных данных упадет с ошибкой, то изменения, внесенные в базу данных первой командой, все равно сохранятся. Таким образом, нарушится целостность данных.
JSON-объект
Второй способ заключается в создании на форме JSON-объекта и передаче его одним запросом на сервер.
Первое, что нужно сделать - создать объект Variable, в котором описать структуру Dictionary, которая позже сериализуется в JSON-объект:
Обратите внимание, что у тэга <MyObject> стоит атрибут ChangeForm со значением False, чтобы этот объект исключить из проверки свойства FormChanged самой формы.
Для списка позиций в поступлении используется конструкция <Array> с преобразованием массива строк из таблицы DatabaseTable в массив словарей.
У тэгов <Object> и <Parameter> используется атрибут Refresh, который определяет, будет ли обновляться значение у тэгов <Key> и <Array>, если изменится значение источника. Таким образом, значение объекта IncomeDictionaryVariable не будет обновляться каждый раз, когда изменяется какой-либо источник. Для ручного обновления IncomeDictionaryVariable нужно использовать команду ValueSetCommand для обращения к set-проперти Refresh у объекта Variable:
Команда SerializeToJsonCommand при формировании JSON-объекта преобразует все даты со временем к UTC относительно временной зоны из пользовательских настроек.
Для передачи JSON-объекта на сервер используется команда IncomeSaveCommand типа SaveCommand, которая вызывает сохраняющее соединение:
Шаблонный код функции, в котором показывается, как с помощью SQL разбирать скалярные поля и массивы в JSON-строке.
CREATE OR REPLACE FUNCTION template.income_save(in_model json)
RETURNS bigint AS
$BODY$
DECLARE
_income_id bigint = in_model -> 'common' ->> 'income_id';
_income_date timestamp = (in_model -> 'common' ->> 'income_date')::timestamp;
_income_number character varying = in_model -> 'common' ->> 'income_number';
_record record;
BEGIN
IF (_income_id IS NULL)
THEN
-- INSERT INTO template.income ... RETURNING income_id INTO _income_id;
ELSE
-- UPDATE template.income
END IF;
-- Сохранение позиций поступления
FOR _record IN (
SELECT *
FROM
json_to_recordset(in_model -> 'income_item_list') AS x(
income_item_id bigint,
material_id bigint,
quantity numeric,
unit_price numeric
)
) LOOP
IF (_record.income_item_id IS NULL) THEN
-- INSERT INTO template.income_item
ELSE
-- UPDATE template.income_item
END IF;
-- DELETE FROM template.income_item
END LOOP;
-- Сохранение позиций поступления
RETURN _income_id;
END;
$BODY$
LANGUAGE plpgsql;
Функция возвращает идентификатор поступления, который будет передаваться на форму в результат выполнения команды.
Результат запроса будет храниться в результате команды IncomeSaveCommand, который можно получить по имени команды и сохранить в параметр формы, чтобы использовать его дальше на форме:
Сначала обновляем переменную IncomeDictionaryVariable, а затем сериализуем ее в JSON-объект и передаем на сервер.
Преимущество этого варианта в том, что все данные отправляются на сервер одним запросом и обрабатываются в одной транзакции, что гарантирует целостность данных.