В прошлом уроке мы познакомились с возможностями PostgreSQL по работе с JSON-объектами, когда сохраняли заказ и клиента через API-запросы. Платформа Workflow Technology так же поддерживает работу с JSON-объектами, что позволят собирать на форме всю информацию и единым запросом передавать ее на сервер. Это избавляет нас от необходимости создавать черновую запись для работы с сущностями со сложной иерархической структурой (например, сущность заказа).
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.
При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.
Инструкция по подключению шаблонов находится по ссылке.
Работа с JSON на форме
Работу с JSON будем рассматривать на примере карточки заказа, так как заказ имеет вложенные сущности и потенциально может иметь больше уровней вложенности. Ранее на примере заказа мы рассматривали паттерн Add/Edit. Теперь будет интересно увидеть, как изменится форма с использованием JSON-объекта.
Построение объекта
С формы заказа на сервер должен уходить JSON-объект подобного вида:
Так как JSON-объект представляет собой набор пар "ключ-значение", то нам необходимо создать структуру <Structure> типа Dictionary, которую присвоим в объект Variable:
Для полей с массивами объектов используем конструкцию <Array> с преобразованием массива строк из таблицы в массив словарей.
У тэгов <Object> и <Parameter> появился атрибут Refresh, который определяет, будет ли обновляться значение у тэгов <Key> и <Array>, если изменится значение источника. Таким образом, значение объекта OrderDictionaryVariable не будет пересчитываться каждый раз, когда измениться какой-либо источник. Для ручного пересчета OrderDictionaryVariable нужно использовать команду ValueSetCommand для вызова set-проперти Refresh у объекта Variable:
Команда SerializeToJsonCommand при формировании JSON-объекта преобразует все даты со временем к UTC относительно пользовательских настроек временной зоны.
Добавим вызов обеих команд в SaveSequentialCommand.
Переделаем соединение с данными OrderUpdateSetDataConnection, убрав все параметры и добавив один параметр Model, в который будет передаваться результат команды сериализации объекта в JSON. Переименуем OrderUpdateSetDataConnection в OrderSaveSetDataConnection:
Также переименуем команду OrderUpdateSaveCommand в OrderSaveCommand.
Перейдем в серверный xml-файл (Template.xml). Скорректируем текст запроса OrderUpdateSqlQuery и переименуем его в OrderSaveSqlQuery:
Template.xml
<SqlQueryName="OrderSaveSqlQuery"> <Events> <EventName="ChangedNumberOfOrders"> <Parameters> <ParameterName="NumberOfOrders"> SELECT count(*) FROM template.order O WHERE O.added AND NOT O.deleted; </Parameter> </Parameters> </Event> </Events> <Text> SELECT template.order_save({Model}::json); </Text></SqlQuery>
Самостоятельно
Удаление логики черновых записей
Удалите всю логику, связанную с черновыми записями заказов на главной форме и в карточке заказа.
Удалите колонку added из таблицы template.order и всех запросов, которые ее проверяют или обновляют.
Правка сохранения изменений
Удалите запросы OrderPositionInsertSqlQuery и OrderPositionUpdateSqlQuery и связанные с ними команды. В карточке позиции заказа создайте параметры формы MaterialId, Quantity и UnitPrice. Через эти параметры будем передавать значения между родительской формой и карточкой позиции. Пока можете не реализовывать добавление значений в таблицу на родительской форме - в следующем разделе мы сделаем это вместе.
Удалите запрос OrderPositionByIdSelectSqlQuery и OrderPositionPrimaryGetDataConnection. Объекты MaterialComboBox, QuantityNumericBox и UnitPriceNumericBox заполняйте значениями из параметров формы.
Удаление записей также рассмотрим дальше в уроке.
Аналогично сделайте для оплат в заказе.
Сохранение в базу
Реализуйте функцию template.order_save(json). Функция должна возвращать order_id, который необходимо отлавливать на форме TemplateOrderEdit.xml и писать в параметр OrderId.
Не забывайте про работу с датой со временем и временные зоны. В JSON-строке на сервер даты со временем придут в UTC. Для приведения времени во временную зону сервера используйте функцию public.convert_date_json(timestamp without time zone).
Хранение данных на форме
Удаление записей
Вернемся в файл TemplateOrderEdit.xml и на примере позиций заказа рассмотрим, как реализовать удаление через JSON-объект.
Удалим запрос OrderPositionDeleteSqlQuery и связанные с ним соединение с данными OrderPositionDeleteDataConnection и команду.
С его помощью мы дополняем исходный набор данных из соединения OrderPositionPrimaryGetDataConnection колонкой Deleted со значением False по умолчанию. С OrderPositionConvertDataConnection мы можем работать так же, как с любым другим загружающим DataConnection. Например, мы можем использовать его в качестве источника данных для объектов или другого DataConnection.
Создадим SecondaryGetDataConnection, чтобы отфильтровать записи по колонке Deleted:
И укажем OrderPositionSecondaryGetDataConnection в качестве источника данных для таблицы OrderPositionDatabaseTable. Таким образом, в соединении с данными OrderPositionConvertDataConnection будут храниться все записи, полученные из базы данных, а в таблице будут отображаться только неудаленные.
Теперь займемся удалением записей из OrderPositionConvertDataConnection. Важно то, что в нем могут быть записи из таблицы в базе данных (у таких записей будет значение в поле OrderPositionId) и новые записи, добавленные на форме. Для первых мы должны обновлять значение в поле Deleted, а для вторых будем использовать set-проперти DeleteRowsByIndices.
Первым делом создадим условие IsNullCondition для проверки OrderPositionId:
Аналогичным образом измените логику работы с оплатами в заказе. Для хранения данных об оплатах в карточке заказа создайте преобразующее загружающее соединение с данными OrderPaymentConvertDataConnection.
Скорректируйте функцию template.order_save(json) так, чтобы в ней обрабатывалось поле deleted у объектов из order_position и order_payment.
Добавление и изменение записей
Добавление и изменение записей будем рассматривать с заделом на несколько уровней вложенности. Отдельную сложность представляют сущности третьего и более уровней вложенности.
В таких случаях на формах необходимо использовать временные идентификаторы, по которым при сохранении в базу данных будут восстанавливаться отношения сущностей между собой.
Временный идентификатор
Для таких целей можно использовать объект типа CounterVariable, который является счетчиком положительных целых чисел. При обращении счетчик автоматически увеличивает значение на один и возвращает это значение:
В OrderPositionConvertDataConnection и OrderPaymentConvertDataConnection добавляем поле ID, которое будет хранить временный идентификатор, и укажем в качестве значения объект CounterVariable:
После загрузки формы и получении данных в PrimaryGetDataConnection оба ConvertDataConnection построят свои внутренние таблицы, и в каждой строчке в поле ID будут проставлены уникальные значения.
В таблицы OrderPositionDatabaseTable и OrderPaymentDatabaseTable добавим колонки ID.
Создадим команды ValueSetCommand для добавления и изменения записей в OrderPositionConvertDataConnection. Для этого будем использовать set-проперти AddRow и UpdateRow у DataConnection.
Добавление записи в ConvertDataConnection
В команде на добавление записей будем обращаться к CounterCopyVariable для заполненияколонки ID уникальным временным идентификатором:
Так как временный идентификатор является единственным уникальным ключом, по которому мы можем получить конкретную запись, то по нему и будем выделять строку в таблице. В команде используем конструкцию <Input>, что позволит передавать нужное значение в момент вызова команды. Так мы сможем использовать значение переменной CounterCopyVariable, если создавали новую позицию заказа. А при редактировании позиции заказа будем использовать значение временного идентификатора редактируемой записи.
Скорректируем Execution, который отрабатывает результат выполнения команды OrderPositionAddFormShowCommand:
Обновление данных в строке происходит по ее индексу, который мы можем получить с помощью get-проперти RowIndexOf по уникальному значению, которым является временный идентификатор ID. Значение временного идентификатора берем из выделенной строки в таблице OrderPositionDatabaseTable, так как карточка позиции заказа открывается в модальном режиме. В противном случае, нам пришлось бы передавать на дочернюю форму временный идентификатор и после получать его значение через параметр команды OrderPositionEditFormShowCommand.
Скорректируем Execution, который отрабатывает результат выполнения команды OrderPositionEditFormShowCommand:
Здесь так же можно было добавить команду выделения редактируемой строки, если бы команда OrderPositionEditFormShowCommand открывала окно в немодальном режиме.
Аналогичным образом измените логику добавления и редактирования оплат в заказе.
Обновление данных
После внесенных изменение команды OrderPositionDataConnectionRefreshCommand и OrderPaymentDataConnectionRefreshCommand не используются - их можно удалить. Но обновлять соединения с данными для позиций заказа и оплат необходимо, чтобы подтягивать из базы данных на форму идентификаторы новых записей. Так как после сохранения форма остается открытой, и пользователь может редактировать новые записи, то важно иметь оригинальные идентификаторы, чтобы новые изменения правильно сохранялись в базу данных.
Создадим новую команду типа DataConnectionRefreshCommand:
Раньше в параметре OrderId всегда было значение, так как при открытии формы на создание заказа, на родительской форме создавался черновик заказа, идентификатор которого передавали в параметр. Теперь мы удалили логику черновиков, и параметр OrderId будет иметь пустое значение при создании заказа. А так как после сохранения изменений форма остается открытой, то повторное сохранение изменений будет приводить к дублированию записей в базе данных. Чтобы этого не происходило, мы и будем сохранять идентификатор нового заказа.
Добавим новые команды в команду SaveSequentialCommand:
В соединение с данными OrderPrimaryGetDataConnection добавим атрибут RefreshQuery="False" на параметр OrderId, чтобы DataConnection не обновлялся автоматически при изменении параметра формы OrderId. Такие же изменения внесем и в OrderPositionPrimaryGetDataConnection и OrderPaymentPrimaryGetDataConnection - эти соединения обновляем вручную.
Запустите приложение и проверьте работу формы заказа.
Вложенные сущности третьего уровня
Если у позиции заказа будут вложенные сущности, то для их хранения будет использоваться отдельный ConvertDataConnection, например:
В поле OrderPositionID типа Substitution будет подставляться значение из поля ID в OrderPositionConvertDataConnection на основе полей OrderPositionId в обоих соединениях.
Обратите внимание, что соединение с данными PositionItemConvertDataConnection имеет вложенный тэг <ManualRefresh> со значением True. При этом источник данных (PositionItemPrimaryGetDataConnection) может не иметь вложенный тэг <ManualLoad> со значением True. Следовательно, необходимо создавать команду DataConnectionRefreshCommand для его обновления.
Команда добавление записей в PositionItemConvertDataConnection может иметь вид:
Объект PositionItemArrayToAddVariable будет содержать массив объектов (точнее, матрицу значений), которые описывают вложенные сущности позиции заказа. Такой массив будет возвращаться с дочерней формы (карточки позиции заказа). С помощью конструкции <Array> дополним матрицу новыми значениями для полей ID, OrderPositionID и Deleted. Все строки в матрице будут иметь уникальные значения в колонке ID.
В момент вызова функции в качестве значения <Input Name="OrderPositionID"> будем передавать либо значение CounterCopyVariable, которое сохранили перед добавлением новой позиции заказа, либо значение параметра OrderPositionID из команды OrderPositionEditFormShowCommand.
Итоги
В этом уроке мы рассмотрели альтернативу паттерну Add/Edit, который строится на создании черновой записи и работе с этим черновиком в карточке сущности. Плюсом работа с JSON является возможность передавать данные сущности со сложной иерархической структурой единым запросом, что сохраняет целостность информации в базе данных. Минусом - такой подход усложняет работу с данными на формах, так как необходимо передавать их с родительской формы на дочернюю и обратно, а также следить за связанностью данных при хранении их на форме. И сам SQL-запрос на сохранение в базу данных становится громоздким и сложным.
Ответы
В архиве присутствуют xml-файлы форм и серверный xml-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.