Работа с объектами и коллекциями#
В разделе описаны бизнес-объекты, навигация и массовая загрузка объектов, провайдеры строк, оптимистическая блокировка и работа с коллекциями.
Бизнес-объект#
Бизнес-объект (БО) — это объединение нескольких классов и их коллекций в группу для работы с ними в кеше и конфигурировании вспомогательных сервисов.
Бизнес-объект позволяет:
массово загружать данные в транзакционный кеш.
Для бизнес-объекта можно указать стратегию загрузки данных, существенно уменьшающую количество запросов в базу данных, так как запросы выполняются не для каждого объекта, а для каждого класса бизнес-объекта.настраивать права доступа.
Для бизнес-объекта создаётся административный объект, на котором можно массово выдать привилегии для всех классов бизнес-объекта.управлять электронной подписью.
Можно настроить правила подписи всего бизнес-объекта, включая не только шапку, но и все вложенные коллекции.настраивать интеграцию и репликацию.
Навигацией является последовательное посещение элементов бизнес-объекта сверху вниз. При навигации перемещение между объектами выполняется по кешу. При этом обеспечивается автоматическая дозагрузка объектов в кеш по необходимости.
В процессе навигации объекты не блокируются и могут при необходимости быть вытолкнуты из кеша, что вызовет автоматическую дозагрузку (обновление).
Примеры навигации:
val empApi = EmployeeApi()
empApi.load(7452) :/ { id =>
println(id.id)
for(idDes <- AddressApi.byParent(id)){
println(s"address: city=${ idDes.get(_.city)}")
}
}
Коллекции#
Коллекция — это сущность, объекты которой не могут существовать без ссылки на объект-владелец. Классы коллекций объявляются в ODM сущности-владельца.
В бизнес-объекте предполагается использование ленивых коллекций. Элементы коллекции загружаются по необходимости. Удаление и добавление происходит в фоновых коллекциях и не требует немедленного запроса к базе данных.
Связывание сущностей владельца и коллекции
производится путём объявления элемента collection в секции
collections:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<class xmlns="http://www.global-system.ru/xsd/global3-class-1.0"
name="Bs_Brigade" caption="Бригада"
cardEditor.representation="Card"
listEditor.representation="List"
viewOptions.openCardType="mdi"
supertype="reference">
<attributes>
<attr name="id"
attribute-type="Long"
caption="Идентификатор"
order="-1"
type="basic"
isVisible="false"/>
<attr name="idClass"
attribute-type="Long"
caption="idClass"
order="-2"
type="basic"
isVisible="false"/>
<attr name="gid"
attribute-type="Varchar"
isVisible="false"/>
<attr name="sCode"
attribute-type="Varchar"
caption="Код"
order="10"
isMnemoCode="true"
type="basic"
isVisible="true">
<autonum id="1">
<dimension name="Dim1"/>
</autonum>
</attr>
<attr name="sCaption"
attribute-type="Varchar"
caption="Наименование"
order="20"
isHeadLine="true"
type="basic"
isVisible="true"/>
<attr name="idDepartment"
attribute-type="Long"
caption="Подразделение"
order="30"
type="refObject"
ref.class="Bs_Department"/>
<attr name="idForeman"
attribute-type="Long"
caption="Бригадир"
order="40"
type="refObject"
ref.class="Bs_Employee"/>
<attr name="idMaster"
attribute-type="Long"
caption="Мастер"
order="50"
type="refObject"
ref.class="Bs_Employee"/>
</attributes>
<collections>
<collection name="Bs_BrigadeStaff"
ref.attr="idBrigade"
cascadeOnDelete="true"/>
</collections>
<dbData>
<script name="dataInstall" version="1">
<install>Bs_BrigadeApi.dataInstall()</install>
</script>
</dbData>
</class>
Для класса коллекции указывается супертип collection. Это гарантирует
наследование Scala-классов от необходимых системных классов.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<class xmlns="http://www.global-system.ru/xsd/global3-class-1.0"
name="Bs_BrigadeStaff"
caption="Состав бригады"
cardEditor.representation="Card"
listEditor.representation="List"
viewOptions.openCardType="mdi"
supertype="collection">
<attributes>
<attr name="id"
attribute-type="Long"
caption="Идентификатор"
order="-1"
type="basic"
isVisible="false"/>
<attr name="idClass"
attribute-type="Long"
caption="idClass"
order="-2"
type="basic"
isVisible="false"/>
<attr name="gid"
attribute-type="Varchar"
isVisible="false"/>
<attr name="idEmployee"
attribute-type="Long"
caption="Сотрудник"
order="10"
type="refObject"
ref.class="Bs_Employee"
isMnemoCode="true"/>
<attr name="idBrigade"
attribute-type="Long"
caption="Бригада"
order="20"
type="refObject"
ref.class="Bs_Brigade"
genListCollectionRep="true"/>
<attr name="dStart"
attribute-type="Date"
caption="Дата начала"
order="30"
type="basic"
editorType="datePick"/>
<attr name="dEnd"
attribute-type="Date"
caption="Дата окончания"
order="40"
type="basic"
editorType="datePick"/>
<attr name="bisForeman"
attribute-type="Number"
caption="Является бригадиром"
order="50"
type="basic"
editorType="check"/>
</attributes>
</class>
Формирование кода#
При формировании кода сущности-владельца пересоздаётся код
всех коллекций. Dpi коллекции наследуется от ChildApi:
trait Xxx_XxxxDpi[T] extends ChildApi[java.lang.Long, ARO, API]
Dvi наследуется от CollectionAvi:
trait Xxx_XxxxDvi extends CollectionAvi
Основные методы для работы:
//создание по владельцу, возвращает rop созданного объекта
Api().insertByParent(ropMaster)
//загрузка всей коллекции по владельцу, возвращает обходчик записей отфильтрованных по rop предка
Api().byParent(ropMaster)
Api().byParent(idMaster)
//удаление по rop объекта
Api().delete(rop)
Отображения-детали#
Для формирования отображений выборки, данные которых ограничены по
значению ссылочного поля, необходимо в ODM-файле для соответствующего
ссылочного атрибута указать свойство genListCollectionRep="true". Будет
сформировано отображение List_{attr}.
<attr name="idBrigade"
attribute-type="Long"
caption="Бригада"
order="20"
type="refObject"
ref.class="Bs_Brigade"
genListCollectionRep="true"/>
Переменная ссылочность#
Переменная ссылочность позволяет хранить ссылку на объекты разных классов в одном атрибуте и указывать, какой объект какого класса он представляет.
Примечание
У классов, на объекты которых может ссылаться коллекция
с переменной ссылочностью, обязательно должен существовать атрибут gid.
Рекомендуется индексировать это поле.
Преобразование коллекции в самостоятельный тип#
Если объекты коллекции должны существовать независимо от объекта-родителя, использоваться несколькими объектами-родителями или иметь собственный жизненный цикл, класс с супертипом collection или vcollection может быть преобразован в самостоятельный тип, например reference, journal или document.
Такое преобразование изменяет не только описание класса, но и структуру бизнес-объекта, поэтому требует обновления модели данных и миграции существующих записей.
Изменение модели#
При преобразовании коллекции необходимо:
Изменить супертип класса с
collectionилиvcollectionна требуемый самостоятельный тип.Удалить объявление коллекции из Odm-файла класса объекта-родителя.
Удалить из преобразованного класса ссылку на объект-родитель, если прямая связь с ним больше не используется.
Если связь между объектами должна сохраниться в другом виде, создать отдельный класс связи. Например, для отношения many-to-many класс связи содержит ссылки на преобразованный объект и объект-родитель.
Обновить ORM-описания, API, выборки, пользовательские интерфейсы и другую прикладную бизнес-логику, использующую прежнюю структуру данных.
После преобразования объект перестает быть элементом коллекции и становится самостоятельной сущностью. Дальнейшая работа с ним определяется супертипом, выбранным при преобразовании.
Миграция данных#
Необходимость миграции и ее состав зависят от того, как после преобразования должна измениться связь с объектом-родителем.
Если преобразование не изменяет структуру хранения и существующие ссылки остаются действительными, отдельная миграция данных может не потребоваться. Например, класс может перестать быть коллекцией, но продолжить использоваться как таблица связи между двумя бизнес-объектами.
Если прямая ссылка на бывший объект-родитель сохраняется, существующие записи могут быть оставлены без изменения. В этом случае требуется только проверить, что новая модель корректно использует эту ссылку.
Если вместо прямой ссылки вводится отдельный класс связи, необходимо перенести существующие отношения в новую таблицу связи.
Отдельный сценарий возникает, когда преобразование выполняется для устранения дублирующихся записей. В этом случае миграция может включать:
определение записей, которые должны представлять один самостоятельный объект;
выбор записи, которая сохраняется в базе данных;
перенос связей с объектами-родителями на сохраненную запись через прямую ссылку или отдельный класс связи;
перенос других ссылок с удаляемых дублей на сохраненную запись;
удаление дублирующихся записей.
Критерии определения дублей и выбора сохраняемой записи задаются в соответствии с прикладной моделью.
Пересчет структуры бизнес-объекта#
Если при преобразовании изменяется положение класса в структуре бизнес-объекта, необходимо пересчитать значение gidRoot_dz.
Для пересчета используется метод recalcGidRoot. В качестве параметра передается системное имя класса, положение которого было изменено.
Например:
Btk_ClassUtilsPkg.recalcGidRoot("Vci_GitProject")
При изменении положения класса ранее рассчитанный путь внутри бизнес-объекта автоматически не обновляется. Без пересчета аудит может продолжить записывать изменения в журнал прежнего корневого объекта. Некорректно могут работать и другие механизмы, использующие gidRoot_dz для определения корня или пути объекта внутри бизнес-объекта.
Пересчет включается в релиз модуля, содержащего изменения структуры бизнес-объекта, в виде обычного или отложенного миграционного скрипта. Если пересчет затрагивает небольшой объем данных, используется обычный скрипт. При значительном объеме данных предпочтительно использовать отложенный скрипт, чтобы не увеличивать время установки обновления.
Подробнее о разработке релизов, обычных и отложенных миграционных скриптах см. в разделе Релизы.
Преобразование коллекции в переменную коллекцию#
Если одна коллекция должна использоваться для объектов-родителей разных классов, класс с супертипом collection можно преобразовать в переменную коллекцию (vcollection).
При преобразовании к прямой ссылке на объект-родитель добавляется переменная ссылка, в которой хранится GID объекта-родителя.
Изменение модели#
При преобразовании коллекции необходимо:
Изменить супертип класса с
collectionнаvcollection.Добавить атрибут переменной ссылочности для хранения GID объекта-родителя.
Например, при преобразовании класса Btk_AccessPeriodByUser в vcollection был добавлен атрибут gidObj:
<attr name="gidObj"
attribute-type="Varchar"
caption="Источник"
order="30"
type="refAnyObject"
isRequired="true"
genListCollectionRep="true"/>
У классов, на объекты которых может ссылаться переменная коллекция, должен существовать атрибут gid.
Если класс уже использовался в прикладной логике, прежнюю прямую ссылку на объект-родитель рекомендуется сохранять для обратной совместимости.
Если в коллекции уже существуют записи, их связь с объектом-родителем необходимо перенести на новую переменную ссылку. Например, при преобразовании Btk_AccessPeriodByUser существующие связи через idAccessByPeriod были перенесены на gidObj миграционным скриптом.
После преобразования необходимо обновить API, выборки, пользовательские интерфейсы и другую прикладную логику, использующую прежнюю схему работы с коллекцией.
Каскадное удаление#
Свойство «Каскадное удаление» у коллекции включено по умолчанию. Если оно включено:
<collections>
<collection cascadeOnDelete="true"
name="Bs_PersonIdentityDoc"
ref.attr="idPerson"/>
<collection cascadeOnDelete="true"
name="Bs_PersonProf"
ref.attr="idPerson"/>
</collections>
В Dpi мастера формируется вызов метода delete для коллекций.
Для подключённых vcollection (переменных коллекций) код удаления
необходимо писать вручную в Api:
override def delete(rop: ApiRop): Unit = {
for (crop <- Bs_BankAccApi().byParent(rop)) {
Bs_BankAccApi().delete(crop)
}
for (crop <- Bs_DefSettlerAddressApi().byParent(rop)) {
Bs_DefSettlerAddressApi().delete(crop)
}
for (crop <- Bs_SettlerAddressApi().byParent(rop)) {
Bs_SettlerAddressApi().delete(crop)
}
super.delete(rop)
}
Навигация в рабочем пространстве#
Объекты загружаются из базы данных или из кросс-сессионного кеша. При загрузке происходит пессимистическая либо оптимистическая блокировка. Объекты находятся в рабочем пространстве до момента коммита. В момент коммита рабочее пространство очищается.
При навигации можно модифицировать объекты. Api гарантирует
корректную навигацию по мастер-деталям без необходимости выполнять flush и clean
кеша или немедленно загружать коллекции.
Массовая загрузка объектов#
При массовой загрузке объектов сокращается количество обращений к базе
данных. При обходе в обычном режиме трёхуровневого
бизнес-объекта выполняется n + 2 запроса, где n — количество деталей второго
уровня: один запрос выполняется для мастер-объекта, а два — для коллекций
второго уровня. Если объект запросить с помощью массового запроса,
при его обходе выполняется всего три запроса. Это может ускорить
навигацию по объектам более чем в 10 раз.
Пример массового запроса:
for (rv <- new OQuery(Stk_WarrantAta.Type){
where (t.id in idap)
batchAll()
}){}
Примечание
Объектные запросы активно расходуют оперативную память. Это накладывает ограничение на использование их в процедурах бизнес-логики. Обычно объектные запросы используются для организации пользовательского интерфейса (редактирование одного объекта с коллекциями, справочники, документы и т.д.), а для программирования внутренних процедур бизнес-логики используются SQL-запросы в базу данных.
Работа с провайдерами строк#
Провайдер строки Rop используется для работы со строкой данных (Aro),
загруженной в рабочее пространство, и гарантирует, что при
доступе к строке данная строка будет находиться в рабочем пространстве.
Метод получения Rop:
thisApi().load(идентификатор.asNLong)
Примеры сеттеров в файле выборки:
val rop = thisRop
thisApi().setidContras(rop,getVar("super$id").asNLong)
Работа с Rop в API:
for (ropGrade <- new OQuery(entityAta.Type){
where (t.idGdsGrade === idpGdsGrade)
}) {
setidGdsGrade(ropGrade, None.nl)
}
Работа с AnyRop (Rop неизвестного типа):
anyRop match {
case Btk_GroupApi(ropGroup) =>
ropGroup.get(_.sCaption)
case Btk_ClassApi(ropClass) =>
ropClass.get(_.sName)
case _ => throw AppException("Ожидали роп группы или класса")
}
// получить из списка только ропы определенного класса
ropaAny
.collect {
case Btk_GroupApi(ropGroup) => ropGroup
}
Оптимистическая блокировка#
Так как система Global Postgres ориентирована на работу с короткими транзакциями, фреймворк по умолчанию включает для классов оптимистическую блокировку.
Принцип работы оптимистической блокировки:
При загрузке строки в кеш запоминается версия изменения.
Если строка изменяется, версия изменения увеличивается.
При сохранении в базу данных проверяется, соответствует ли старая версия изменения версии изменения в базе данных. Если версии не совпадают, выдаётся ошибка применения изменений.
Для отключения оптимистической блокировки в классе необходимо указать свойство:
<class useOptimisticLocking="false"/>
Примечание
Для хранения версии изменений используется служебное
поле nVersion_dz.