Работа с объектами и коллекциями#

В разделе описаны бизнес-объекты, навигация и массовая загрузка объектов, провайдеры строк, оптимистическая блокировка и работа с коллекциями.

Бизнес-объект#

Бизнес-объект (БО) — это объединение нескольких классов и их коллекций в группу для работы с ними в кеше и конфигурировании вспомогательных сервисов.

Бизнес-объект позволяет:

  • массово загружать данные в транзакционный кеш.
    Для бизнес-объекта можно указать стратегию загрузки данных, существенно уменьшающую количество запросов в базу данных, так как запросы выполняются не для каждого объекта, а для каждого класса бизнес-объекта.

  • настраивать права доступа.
    Для бизнес-объекта создаётся административный объект, на котором можно массово выдать привилегии для всех классов бизнес-объекта.

  • управлять электронной подписью.
    Можно настроить правила подписи всего бизнес-объекта, включая не только шапку, но и все вложенные коллекции.

  • настраивать интеграцию и репликацию.

Навигацией является последовательное посещение элементов бизнес-объекта сверху вниз. При навигации перемещение между объектами выполняется по кешу. При этом обеспечивается автоматическая дозагрузка объектов в кеш по необходимости.

В процессе навигации объекты не блокируются и могут при необходимости быть вытолкнуты из кеша, что вызовет автоматическую дозагрузку (обновление).

Примеры навигации:

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.

Такое преобразование изменяет не только описание класса, но и структуру бизнес-объекта, поэтому требует обновления модели данных и миграции существующих записей.

Изменение модели#

При преобразовании коллекции необходимо:

  1. Изменить супертип класса с collection или vcollection на требуемый самостоятельный тип.

  2. Удалить объявление коллекции из Odm-файла класса объекта-родителя.

  3. Удалить из преобразованного класса ссылку на объект-родитель, если прямая связь с ним больше не используется.

  4. Если связь между объектами должна сохраниться в другом виде, создать отдельный класс связи. Например, для отношения many-to-many класс связи содержит ссылки на преобразованный объект и объект-родитель.

  5. Обновить ORM-описания, API, выборки, пользовательские интерфейсы и другую прикладную бизнес-логику, использующую прежнюю структуру данных.

После преобразования объект перестает быть элементом коллекции и становится самостоятельной сущностью. Дальнейшая работа с ним определяется супертипом, выбранным при преобразовании.

Миграция данных#

Необходимость миграции и ее состав зависят от того, как после преобразования должна измениться связь с объектом-родителем.

Если преобразование не изменяет структуру хранения и существующие ссылки остаются действительными, отдельная миграция данных может не потребоваться. Например, класс может перестать быть коллекцией, но продолжить использоваться как таблица связи между двумя бизнес-объектами.

Если прямая ссылка на бывший объект-родитель сохраняется, существующие записи могут быть оставлены без изменения. В этом случае требуется только проверить, что новая модель корректно использует эту ссылку.

Если вместо прямой ссылки вводится отдельный класс связи, необходимо перенести существующие отношения в новую таблицу связи.

Отдельный сценарий возникает, когда преобразование выполняется для устранения дублирующихся записей. В этом случае миграция может включать:

  • определение записей, которые должны представлять один самостоятельный объект;

  • выбор записи, которая сохраняется в базе данных;

  • перенос связей с объектами-родителями на сохраненную запись через прямую ссылку или отдельный класс связи;

  • перенос других ссылок с удаляемых дублей на сохраненную запись;

  • удаление дублирующихся записей.

Критерии определения дублей и выбора сохраняемой записи задаются в соответствии с прикладной моделью.

Пересчет структуры бизнес-объекта#

Если при преобразовании изменяется положение класса в структуре бизнес-объекта, необходимо пересчитать значение gidRoot_dz.

Для пересчета используется метод recalcGidRoot. В качестве параметра передается системное имя класса, положение которого было изменено.

Например:

Btk_ClassUtilsPkg.recalcGidRoot("Vci_GitProject")

При изменении положения класса ранее рассчитанный путь внутри бизнес-объекта автоматически не обновляется. Без пересчета аудит может продолжить записывать изменения в журнал прежнего корневого объекта. Некорректно могут работать и другие механизмы, использующие gidRoot_dz для определения корня или пути объекта внутри бизнес-объекта.

Пересчет включается в релиз модуля, содержащего изменения структуры бизнес-объекта, в виде обычного или отложенного миграционного скрипта. Если пересчет затрагивает небольшой объем данных, используется обычный скрипт. При значительном объеме данных предпочтительно использовать отложенный скрипт, чтобы не увеличивать время установки обновления.

Подробнее о разработке релизов, обычных и отложенных миграционных скриптах см. в разделе Релизы.

Преобразование коллекции в переменную коллекцию#

Если одна коллекция должна использоваться для объектов-родителей разных классов, класс с супертипом collection можно преобразовать в переменную коллекцию (vcollection).

При преобразовании к прямой ссылке на объект-родитель добавляется переменная ссылка, в которой хранится GID объекта-родителя.

Изменение модели#

При преобразовании коллекции необходимо:

  1. Изменить супертип класса с collection на vcollection.

  2. Добавить атрибут переменной ссылочности для хранения 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.