Перейти к основному содержанию
`Ctrl`+`K`

# Практика Avi

## Про `selectStatement` и `onRefreshExt`

Изначально результат выборки формируется операцией `onRefresh` или `onRefreshItem` одним из двух способов: реляционный или объектный запрос.

### Реляционный запрос

В этом случае в теле `onRefresh` вызывается метод `selectStatement` или `prepareSelectStatement` (вызывает тот же `selectStatement`, но с добавлением фильтров, описанных в `avm`). Иначе говоря, результатом операции `onRefresh` является текстовое значение, внутри которого реляционный запрос.

Примечание

onRefreshExt в случае формирования выборки реляционным запросом не вызывается!

Так формируется по умолчанию отображение `List` (кроме коллекций). Не учитываются значения хранящиеся в кэше, учитываются значения только из БД.

Пример `selectStatement` для отображения `List`:

    override protected def selectStatement: String = {
      s"""SELECT
        t.id
        ,t.idClass
        ,t.idState
        ,t1.sHeadLine_dz as idStateHL
        ,t.idStateMC
        ,t.idObjectType
        ,t2.sHeadLine_dz as idObjectTypeHL
        ,t.idDepOwner
        ,t3.sHeadLine_dz as idDepOwnerHL
        ,t.gidSrc
        ,t.idResourceHolder
        ,t4.sHeadLine_dz as idResourceHolderHL
        ,t.sRegNum
        ,t.dReg
        ,t.idPeriod
        ,t5.sHeadLine_dz as idPeriodHL
        ,t.sDescription
        ,t.gid
        ,t.sRegNum_dz
        ,t.sRegNumBMs_dz
        ,t.sRegNumidVer_dz
        FROM Oil_DemandMov t
          LEFT JOIN  Btk_ClassState t1 on t.idState = t1.id
          LEFT JOIN  Btk_ObjectType t2 on t.idObjectType = t2.id
          LEFT JOIN  Bs_DepOwner t3 on t.idDepOwner = t3.id
          LEFT JOIN  Bs_Contras t4 on t.idResourceHolder = t4.id
          LEFT JOIN  Bs_Period t5 on t.idPeriod = t5.id
      """
    }

### Коммит в БД при выполнении реляционного запроса и @FlushBefore

Важно понимать, что при реляционном запросе сервер приложения делает коммит в БД (особенность работы сервера приложения). Поэтому на выборках, где вводятся значения в поля и поддерживается возможность пользователю отменить изменения, нельзя использовать формирование выборки на реляционном запросе. Иначе данные запишутся в БД без разрешения пользователя (без нажатия пользователем на операцию сохранения (дискетка)).

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

В таком случае перед `onRefresh` необходимо добавить аннотоцию `@FlushBefore(mode = FlushBeforeMode.Disabled)`:

    trait Lookup extends Default with super.Lookup {

      @FlushBefore(mode = FlushBeforeMode.Disabled)
      override protected def onRefresh: Recs = {
        s"""SELECT
            t.id
            ,t.sHeadLine_dz as sHeadLine
            ,t.sMnemoCode_dz as sMnemoCode
            ,coalesce(t.sMnemoCode_dz, '') || ' ' || coalesce(t.sHeadLine_dz, '') as sMnemoCodeHeadLine
          from AsfEqp_IntoOperExt t
          order by upper(t.sHeadLine_dz)
        """
      }
    }

Иначе при открытии выпадающего списка данные будут записываться в БД.

## Объектный запрос

В этом случае onRefresh формируется из экземпляров объектов `SRop` (инструменты `refreshByParent`, `load`, `TxIndex`, `OQuery`) или `case class`’ов.

В этом случае учитываются значения, хранящиеся в кэше.

Примеры:

По умолчанию отображение `List` у коллекций формируется с помощью `refreshByParent`:

    trait List_idPlanFactoryShip extends Default with super.List_Master {

      override protected def onRefresh: Recs = {
        Oil_PlanFactShipSegrGroupApi().refreshByParent(getIdMaster)
      }
    }

По умолчанию отображение `Card` у коллекций формируется с помощью `load`:

    trait Card extends Default with super.Card {

      override protected def onRefreshItem: Recs = {
        Oil_PlanFactShipSegrGroupApi().load(getVar(getPKFieldName).asNLong)
      }
    }

### onRefreshExt

В случае объектного запроса сервер сам вызывает метод `onRefreshExt`, в который можно дописать получение нехранимых полей на синтаксисе `SQL`, НЕ учитывая значения из кэша:

    override protected def onRefreshExt: String = {
      s"""with t as ( select
       :id as id
      ,:idPlanFactoryShip as idPlanFactoryShip
      ,:idSegregationGroup as idSegregationGroup
    )
    SELECT
       t.id
      ,t1.sHeadLine_dz as idPlanFactoryShipHL
      ,t2.sHeadLine_dz as idSegregationGroupHL
      ,t2.sMnemoCode_dz as idSegregationGroupMC
    FROM t
      LEFT JOIN  Oil_PlanFactoryShip t1 on t.idPlanFactoryShip = t1.id
      LEFT JOIN  Oil_SegregationGroup t2 on t.idSegregationGroup = t2.id
    """
    }

Примечание

Также существует другой способ реализовать нехранимые поля (через case class AdditionalInfo), который будет описан дальше. Данный способ формирует значения с учётом кэша.

## Добавление нехранимых полей

Здесь будет рассмотрено 2 случая, как реализовать нехранимые поля в выборке, реализованные реляционным запросом и объектным.

Для формирования нехранимого поля нужно:

- Получить поле в запросе под заданным псевдонимом.

- Описать поле в разметке `avm` под тем же псевдонимом.

### Формировать реляционно или объектно

Если нехранимый атрибут вычисляется динамически от изменений на выборке, это значит, что работа идёт с значениями из кэша, т.е. с значениями, которые ещё не закоммичены в БД. Это означает, что нехранимое поле реализовывать нужно на объектном запросе.

Например: в карточке документа нужно посчитать сумму по всем записям в коллекции. Если поле суммы реализовать реляционно, то оно будет формироваться только из согласованных в БД значений. Это значит, для того чтобы учитывались новые записи в коллекции, их нужно закоммитить (сохранить) в БД. Если же поле реализовать объектно, то в нём будут учитываться данные из кэша, т.е. те самые созданные новые записи в коллекции, которые ещё не закоммичены в БД.

### Нехранимые поля в реляционном запросе

Примечание

Не учитывает значения из кэша, которые не закоммичены в БД.

Чтобы добавить не хранимый атрибут реляционно, нужно иметь значение атрибута в выборке запроса `selectStatement`.

Примером может служить результат кодогенератора ссылочного поля для отображения `List`, где описывается получение значения хэдлайна в `Dvi` и описание атрибута в `dvm`.

Dvi:

        override protected def selectStatement: String = {
      s"""SELECT
        t.id
        ,t.idClass
        ,t.idObjectType
        ,t1.sHeadLine_dz as idObjectTypeHL   --нехранимое поле
        ,t.dReg
        ,t.sRegNum
        ,t.idState
        ,t2.sHeadLine_dz as idStateHL    --нехранимое поле
        FROM Rzd_Train t
          LEFT JOIN  Btk_ObjectType t1 on t.idObjectType = t1.id
          LEFT JOIN  Btk_ClassState t2 on t.idState = t2.id
        """
    }

dvm:

    <representation name="Default">
        <attributes>
            <attr name="idObjectType" caption="Тип документа" isVisible="false" editorType="edit" order="10" isRequired="true"/>
            <attr name="idObjectTypeHL" caption="Тип документа" order="10.2" isRequired="true">
                <editor>
                    <buttonsEdit canEditText="true" changeableAttr="idObjectType">
                        <buttons lookup="true" openCard="true" reset="true"/>
                    </buttonsEdit>
                </editor>
                <ref class="Btk_ObjectType"/>
            </attr>
            <attr name="idState" caption="Состояние" isVisible="false" editorType="edit" order="40" isRequired="true"/>
            <attr name="idStateHL" caption="Состояние" order="40.2" isRequired="true">
                <editor>
                    <lookup changeableAttr="idState" isLookupLazyLoad="true" lookupKeyAttr="id" lookupListAttr="sHeadLine" lookupQuery="gtk-ru.bitec.app.btk.Btk_ClassStateAvi#Lookup_Class"/>
                </editor>
                <ref class="Btk_ClassState"/>
                <grid columnWidth="12"/>
            </attr>
        </attributes>
    </representation>

Примечание

Напомню, что Dvi и dvm - это результат работы кодогенератора по данным описанным в разметке odm. Разработчик работает в соответствующих файлах Avi и avm, переопределяя содержимое Dvi и dvm.

ВНИМАНИЕ! Изменять Dvi и dvm нельзя.

А точнее бесполезно, потому что изменения будут перетёрты при следующем запуске кодогенератора.

### Нехранимые поля в объектном запросе

Примечание

Учитывает значения из кэша, которые не закоммичены в БД.

#### onRefreshExt

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

С помощью `onRefreshExt` в отображении `Card` получены хэдлайны в `Dvi`:

        override protected def onRefreshExt: String = {
          s"""with t as ( select
           :id as id
          ,:idObjectType as idObjectType
          ,:idState as idState
          ,:gidSrc/*@NString*/ as gidSrc
        )
        SELECT
           t.id
          ,t1.sHeadLine_dz as idObjectTypeHL
          ,t2.sHeadLine_dz as idStateHL
          ,t3.sHeadLine as gidSrcHL
        FROM t
          LEFT JOIN  Btk_ObjectType t1 on t.idObjectType = t1.id
          LEFT JOIN  Btk_ClassState t2 on t.idState = t2.id
          LEFT JOIN  Btk_Object t3 on t.gidSrc = t3.gidRef
        """
        }

Здесь используется синтаксис postgresql.

Применяется конструкция with (ссылка).

Через `":"` подставляются текущие значения атрибутов с выборки (в том числе из кэша). Так, например, `":id as id"` означает, что с выборки будет получено текущее значение атрибута `id` и подставлено в таблицу `t` под псевдонимом поля `id`. Теперь обращение в основном запросе `t.id` будет возвращать текущее значеине `id`.

Примечание

Под фразой «текущее значение» понимается значение на момент обновления выборки и выполнения метода onRefreshExt.

Примечание

Описание в dvm не меняется в зависимости от метода получения значения нехранимого поля (реляционного или объектного) и останется таким же, как в случае формирования нехранимого поля в selectStatement.

#### case class AdditionalInfo

Нехранимое поле можно сформировать, переопределив `onRefresh` и `onRefreshItem`.

В этом случае объект представляют как кортеж (`SRop`, экземпляр `case class’а`). Тем самым объект имеет поля записи, описанных в БД (хранимые поля), и поля `case class’а` (нехранимые).

В этом случае принято называть `case class AdditionalInfo`, а заполнение описывать в методе `getAdditionalInfo`.

Нехранимое поле в отображении `Card`:

    case class AdditionalInfo(
                               var nQtyLoadRecievActs: NNumber,
                               var nQtyLoadTransferCertificate: NNumber,
                               var nQtyLoadTransferCertificateAccounted: NNumber,
                               var nQtyRemains: NNumber,
                               var nQtyReserv: NNumber
                             )

    protected def getAdditionalInfo(rop: Oil_ExternalMovApi#ApiRop): AdditionalInfo = {
      AdditionalInfo(
        nQtyLoadRecievActs = None.nn
        , nQtyLoadTransferCertificate = None.nn
        , nQtyLoadTransferCertificateAccounted = None.nn
        , nQtyRemains = None.nn
        , nQtyReserv = None.nn
      )
    }

    override protected def onRefresh: Recs = {
      val rop = thisApi().load(getVar(CardRep.IdItemSharp).asNLong)
      (rop, getAdditionalInfo(rop))
    }

    override protected def onRefreshItem: Recs = {
      val rop = thisApi().load(getVar(getPKFieldName).asNLong)
      (rop, getAdditionalInfo(rop))
    }

Примечание

Переменные внутри case class необходимо указывать var для Avi

Нехранимое поле в отображении `List_idBrigade` коллекции:

    trait List_idBrigade extends Default with super.List_idBrigade {

      case class AdditionalInfo(var sEmpId: NString = None.ns,
                                var sPosition: NString = None.ns)

      def getAdditionalInfo(rop: Bs_BrigadeStaffApi#ApiRop): AdditionalInfo = {
        if (rop.get(_.idEmployee).isNotNull) {
          val avEmploee = Bs_EmployeeApi().load(rop.get(_.idEmployee)).copyAro()
          AdditionalInfo(
            sEmpId = avEmploee.sEmpId,
            sPosition = avEmploee.sPosition)
        } else AdditionalInfo()
      }

      override protected def onRefresh: Recs = {
        Bs_BrigadeStaffApi().refreshByParent(getVarWithDep("super$id").asNLong)
          .map(rop => {
            (rop, getAdditionalInfo(rop))
          })
      }
    }

Такой способ самый универсальный.

Например, с помощью `onRefreshExt` не получится посчитать сумму по всем записям коллекции с учётом кэша (для этого необходимо присоединить таблицу коллекции по условию `idparent = t.id`, что обеспечит учёт только тех записей и их значений, которые закоммичены в БД, т.е. без учёта кэша).

В рассматриваемой реализации, можно получить все записи коллекции с учётом кэша с помощью метода `refreshByParent(rop.get(_.id))` и далее в обходчике сложить необходимые значения полей.

## Нехранимые строки

Здесь будет описан пример реализации пустых предзаполненных строк в списке, как частный случай, в коллекции. Такие строки не хранятся в БД, а существуют визуально в интерфейсе (иными словами нехранимые строки). Запись в БД создаётся при вводе пользователем какого-либо поля.

Пример в проекте pgDev: `ru.bitec.app.oil.Oil_RecievTaskDetAvi.List_idRecievTaskByFlyOverWay`.

### `case class Row`

По сколку поля строки редактируемые, то не нужно использовать реляционный запрос для формирования выборки. Выборка будет формироваться экземплярами `case class`’а. Таким образом onRefresh вернёт список экземпляров `case class`’а.

В примере в коллекции «Подача» имеет столько строк, сколько вагонов указано в записи «Путь эстакады», на который ссылается мастер-документ.

`Case class` должен иметь такие же поля, как поля класса (таблицы), описанные в odm и нехранимые, формируемые не реляционно.

При добавлении нового хранимого поля, необходимо это поле добавить в `case class`.

    /**
     * Представление строки, поля соответствуют хранимым полям строки
     *
     * Полем id выступает idFlyOverPos, т.к. для работы некоторых инструментов (например, onRefreshExt) необходимо
     * наличие уникального индификатора
     */
    case class Row(
                    var id: NLong //Oil_RecievTaskDet.idFlyOverPos
                    , var gid: NGid = None.ng
                    , var idRecievTask: NLong
                    , var nRow: NNumber
                    , var idWagon: NLong = None.nl
                    , var idRailwayInvoice: NLong = None.nl
                    , var bUnloaded: NNumber = None.nn
                    , var bReFeed: NNumber = None.nn
                    , var idReFeedType: NLong = None.nl
                    , var bWagonRemoved: NNumber = None.nn
                    , var bRequiredMeasure: NNumber = None.nn
                    , var nQtyMeasure: NNumber = None.nn
                    , var idStorageTank: NLong = None.nl
                    , var sCertificateList: NString = None.ns
                    , var idRecievAct: NLong = None.nl
                    , var bRepeatedFeed: NNumber = None.nn
                    , var bCommAct: NNumber = None.nn
                    , var bRecievActCreated: NNumber = None.nn
                    , var idFlyOverPos: NLong = None.nl
                    , var idLockDeviceOut: NLong = None.nl
                    , var idRecievTaskDet: NLong = None.nl //Oil_RecievTaskDet.id
                    , var idTrain: NLong = None.nl
                    , var bRecievByMeasure: NNumber = None.nn
                  )

### `onRefresh`

`ru.bitec.app.oil.Oil_RecievTaskDetAvi.List_idRecievTaskByFlyOverWay#onRefresh`.

Описание алгоритма:

- Получаем все записи данной коллекции по мастеру (т.е. те, что хранятся в БД или в кэше).

      RecievTaskDetApi().refreshByParent(getIdMaster)

- Получаем все вагоны выбранной у мастера «Пути эстакады».

      val ropParent = Oil_RecievTaskApi().load(selection.master.getSelfVar("id").asNLong)
      Oil_FlyOverPosApi().txidFlyOverWay.refreshByKey(ropParent.get(_.idFlyOverWay))

  Примечание

  В примере в коллекции «Подача» имеет столько строк, сколько вагонов указано в записи «Путь эстакады», на который ссылается мастер-документ.

- Реализуем обходчик по каждому вагону и заполняем экземпляры `case class`’а.

  - Если на данный вагон есть запись из таблицы БД, то заполняем данными из БД.

  - Иначе устанавливаем предзаполненные значения необходимых полей или `None`.

      ropaFOP.map(ropFOP => {
        //поиск ропы в БД
        val ropOpt = ropa.find(_.get(_.idFlyOverPos) === ropFOP.get(_.id))
        getRowByRop(ropOpt, ropFOP, nvRow)
      })

      protected def getRowByRop(ropOpt: Option[SRop[_ <: JLong, _ <: Oil_RecievTaskDetAro]]
                                , ropFOP: Oil_FlyOverPosApi#ApiRop
                                , npRow: NNumber = None.nn
                              ): Row = {
        Row(
      idPlacement = ropOpt.map(_.get(_.idPlacement)).nl
        .nvl(thisApi().getidPlacementByFlyOverPos(ropFOP.get(_.id)))
      )
      }

  При добавлении нового хранимого атрибута, необходимо в этом методе заполнения описать правило заполнения нового поля.

  На данном этапе предположим, что `id` заполняется от `ropOpt` (реальной записи), в случае отсутствия `None.nl`. Т.е. пустая строка имеет `id = None.nl`, что дальше будет использоваться, как признак пустой строки.

  Примечание

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

  `Row – case class`, представление строки, поля соответствуют хранимым полям записи.

- Возвращаем полученный список экземпляров `case class`’а.

### `insert` при вводе значения в нехранимую строку

Необходимо переопределить операцию `beforeEdit()`:

    override def beforeEdit(): Unit = {
      		if (getSelfVar("id").isNull) {
        			regRow()
      		}
    }

`beforeEdit()` вызывается при каждой попытке редактировать поле. Здесь необходимо инициализировать, что заполнение введётся в пустой нехранимой строке или в хранимой. Это можно сделать, проверив заполнен ли `id` (на данном этапе предполагается, что у пустой строки `id = None.nl`).

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

Переносить предустановленные значения можно двумя способами:

- Прописать, какой сеттер вызывать и какое значение из case class’а подставлять на каждое поле в отдельности. Такая реализация усложняет поддержание кода. При добавлении нового хранимого атрибута, в случае если оно имеет предустановленное значение, нужно добавлять код в этот фрагмент.

- Реализовать обходчик по всем имеющимся полям case class’a, отсечь поля, которые не имеют сеттеров (`gid`, например) и значение которых `null`, по оставшимся полям вызывать сеттеры и проставлять соответствующие значения:

    /**
     * insertByParent(ropParent) + сеттеры заполненных полей case class'а Row
     *
     * @param ropParent
     * @param row
     * @return
     */
    def insertByParentAndRow(ropParent: Oil_RecievTaskApi#ApiRop, row: Row): ApiRop = {
      insertByParent(ropParent) :/ { rop =>
        //setter'ы для переноса умолчательных значений из Row
        row.getClass.getDeclaredFields.map(_.getName.ns).zip(row.productIterator.to)
          .filterNot(field => saFieldsNotSetter.contains(field._1) || field._2.asInstanceOf[Nullable[_ <: Any, _]].isNull)
          .foreach(field => {
              setAttrValue(rop, field._1, field._2)
          })
        rop
      }
    }

Чтобы введенное пользователем значение относилось к только что созданной записи, необходимо принудительно после регистрации задать `id` на выборке:

    val rop = thisApi().registerByRow(thisRow())
    setVar("id", rop.get(_.id))

### `afterEdit()`

Также необходимо добавить проверку, является ли строка пустой, в `afterEdit()`.

Иначе `load()` внутри `afterEdit()` будет вызван по некорректного id пустой строки и будет вызвана ошибка.

### Поддержание уникального id в нехранимых полях.

Без уникального `id` ряд системных операций не будет работать или будет работать некорректно, в том числе для работы `onRefreshExt`.

Необходимо понять, что на выборке является уникальным, в случае примера это ссылка соответствующая вагону: `idFlyOverPos`.

Теперь параметр `id` на выборке будет иметь не значение `null` в случае пустой строки и значение записи коллекции из БД, а значение `idFlyOverPos`.

А сам `id` записи коллекции будет храниться в поле case class’a `idRecievTaskDet` Это нужно учесть в:

- структуре case class’а;

- в переносе значений из записи из БД в case class для формирования `onRefresh`;

- в условии по признаку пустая ли строка (теперь строка пустая, если `idRecievTaskDet === None.nl`):

    override def beforeEdit(): Unit = {
      if (getSelfVar("idRecievTaskDet").isNull) {
        regRow()
      }
    }

- При сеттере в поле пустой строки в методе переноса умолчательных значений:

      /**
      * insertByParent(ropParent) + сеттеры заполненных полей case class'а Row
      *
      * @param ropParent
      * @param row
      * @return
      */
      def insertByParentAndRow(ropParent: Oil_RecievTaskApi#ApiRop, row: Row): ApiRop = {
        insertByParent(ropParent) :/ { rop =>
          //setter'ы для переноса умолчательных значений из Row
          row.getClass.getDeclaredFields.map(_.getName.ns).zip(row.productIterator.to(scala.collection.immutable.IndexedSeq))
            .filterNot(field => saFieldsNotSetter.contains(field._1) || field._2.asInstanceOf[Nullable[_ <: Any, _]].isNull)
            .foreach(field => {
              if (field._1 == sid.ns) {
                setAttrValue(rop, sidFlyOverPos, field._2)
              } else {
                setAttrValue(rop, field._1, field._2)
              }
            })
          rop
        }
      }

- При сеттере в поле пустой строки после создания записи проставить параметр выборки `idRecievTaskDet`, заместо `id`, в `id` только что созданной записи:

      val rop = thisApi().registerByRow(thisRow())
      setVar("idRecievTaskDet", rop.get(_.id))

- При взятии параметра у дочерних выборок по `super$id` -> `super$idRecievTaskDet`.

- В `CWA` управление свойством `isEnabled` у операций:

      selection.opers().setEnabled("Delete",selection.canDelete && selection.getSelfVar("idRecievTaskDet").notNull())

- Удаление:

      thisApi().delete(thisApi().load(getSelfVar("idRecievTaskDet").asNLong))

- `thisRop()`:

      thisApi().load(getSelfVar("idRecievTaskDet").asJLong)

- `onInvalidateItem()`:

      override protected def onInvalidateItem(): Unit = {
        if (!getSelfVar(thisApi().sidRecievTaskDet).isNull && thisRop() != null) {
          session.invalidateObject(thisRop())
        }
      }

- учесть в `onRefreshExt()` в тексте запроса.

## Динамическое присоединение столбцов

Если такое отображение только для чтения и результат не использует значения, которые могут быть в кэше, тогда можно реализовать на реляционном запросе, иначе необходимо использовать инструменты `DynMetaBuilder` и `DynRecBuilder`.

### Реляционная реализация

В этом случае необходимо собрать текст запроса `selectStatement`.

Ниже пример формирования одного из столбца:

    s"""
    ,(select string_agg(cast(t.id as varchar), ', ')
      from Oil_Task t
      where t.idResource  = ${rv.idResource()}
        and (t.dBegin < $sMainFromTab.dEndTime and t.dExec > $sMainFromTab.dBegTime)
        and t.idObjectType = :${Oil_TaskMonitorPkg().sfltidObjectType}
        and t.idStateMC >= 200
        and t.idDepOwner = :super$$idGlobalDepOwner
      group by t.idResource
      ) as \"$sNameFieldForGST[${rv.id()}]\"
    """

Пример текста после подстановки значения:

    (select string_agg(cast(t.id as varchar), ', ')
      from Oil_Task t
      where t.idResource  = 115
        and (t.dBegin < ‘2023-08-25 12:00:00’ and t.dExec > ‘2023-08-25 14:00:00’)
        and t.idObjectType = 225
        and t.idStateMC >= 200
        and t.idDepOwner = 336
      group by t.idResource
      ) as “idFlyOverWay[13]"

Мы получим столбец с названием `idFlyOverWay[13]`, результатом будет значение подзапроса.

Примечание

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

Пример задания параметров для всех атрибутов `idFlyOverWay`:

    <attr name="idFlyOverWay">
      <style attr="sAvailStyle"/>
      <grid columnWidth="6" isColumnWidthFixed="true"/>
    </attr>

Пример установки параметров атрибута по его индексу:

    for ((svCaption, nvIdx) <- Pro_ResLoadChartPkg().getPeriodCaptions(svExpandTypeMC, dvPeriodBegin, dvPeriodEnd, periods).zipWithIndex) {
      selection.attrs(s"""nWreQty[$nvIdx]""").caption = s"Плановая|${svCaption.get}"
      selection.attrs(s"""nWreQty[$nvIdx]""").order = 100 + 100 * nvIdx
      selection.attrs(s"""nResAvail[$nvIdx]""").isVisible = getVar("super$filter$Flt_bShowResourceAvailability").asNNumber.nvl(0.nn).toBoolean
    }

Примечание

В примере выше у атрибута задается бенд группа через символ „|“. Сначала идет наименование бенда „Плановая“, после наименование самого атрибута.

### `DynMetaBuilder` и `DynRecBuilder`

Пример использование `DynMetaBuilder` и `DynRecBuilder` является отображение `Card_ObjectAttr` для объектных характеристик, генерируемый кодогенератором в `Dvi`.

Структура формирования:

Примечание

Recs(<объекты, представляющие строку (список rop или case class)>) .extend(.build()) .foreach((row, builder) => (row, .build))

- Пример использования `.extend()` - метод расширяет исходный набор данных, добавляя к ним данные о динамических столбцах:

      val dynMetaBuilder = getObjAttrDynMetaBuilder()
      thisApi().refreshByParent(getIdMaster)
        .extend(dynMetaBuilder.build())
        .foreach{(rop, builder) =>
          val dynamicInfo = getDynamicObjAttr(rop, builder)
          (rop, dynamicInfo.build)
        }

  Стоит понимать, что в примере foreach не привычный обходчик по коллекции, который ничего не возвращает, а отдельный метод для динамического формирования столбцов, который должен вернуть результат выборки!

  В данном методе столбец получает построчно значения.

- Пример `getObjAttrDynMetaBuilder`

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

      def getObjAttrDynMetaBuilder(): DynMetaBuilder = {
        val dynMetaBuilder = DynMetaBuilder()
        thisApi().jsonAttrsWithPrefix.foreach { case (rvAttr, sAttrPrefix) =>
          val svAttr = getJsonAttrName(rvAttr.id, sAttrPrefix)
          val attrClass = Btk_AttributeApi().getAttrValueClassById(rvAttr.id)
          // Добавляем метаданные об атрибуте
          dynMetaBuilder.add(svAttr, attrClass, rvAttr.sCaption)
          // для ссылочных атрибутов отображаем HL поля
          if (rvAttr.sType === AttrTypes.RefObject.toString.ns) {
            dynMetaBuilder.add(s"idValueHL[${rvAttr.id}]", classOf[String], rvAttr.sCaption)
          }
          // И добавляем скрытый столбец для редактора
          dynMetaBuilder.add(Btk_AttributeLib().getEditorAttrName(rvAttr.id, rvAttr.id.isNull), classOf[String], s"Редактор ${rvAttr.sCaption}")
        }
        dynMetaBuilder
      }

- Пример `getDynamicObjAttr` - здесь формируются данные строк для каждого добавленного атрибута:

      def getDynamicObjAttr(rop: Rop[_, _], builder: DynRecBuilder): DynRecBuilder = {
        val svLetterInRuleCaption = thisApi().getLetterInProcessRule(rop).sCaption
        builder.set(A.sLetterInProcessRule.name + "HL", svLetterInRuleCaption.get)
        builder
      }

  В данном примере для атрибута `sLetterInProcessRule` будет выведен дополнительный столбец с заголовком.

В некоторых случаях требуется использовать методы `DynMetaBuilder` и `DynRecBuilder` вместе с классом `AdditionalInfo`. В таком случае вместо extend используется метод extend2.

- Пример использования `.extend2()` - метод расширяет исходный набор данных, добавляя к ним данные о динамических столбцах и данные из класса `AdditionalInfo`:

      thisApi().refreshByParent(getIdMaster)
            .extend2(classOf[AdditionalInfo], dynMetaBuilder.build())
            .foreach{(rop, builder) =>
              val dynamicInfo = getDynamicObjAttr(rop, builder)
              (rop, getAdditionalInfo(rop.asInstanceOf[SomeApi#ApiRop]), dynamicInfo.build)
            }

  ,где `SomeApi` - какая-либо Api класса.

## Динамическое изменение свойств avm

Для этого необходимо использовать следующий метод:

`ru.bitec.app.gtk.gl.Rep#setMetaProp`

Описание

Пример использования:

    setMetaProp(attr.sSystemName.get //для какого поля меняется свойство
    //какое свойство
    //в данном случае свойство задает системное имя атрибута в выборке,
    //которое хранит настройки типа редактора
    , s"View.Representation.Attributes.Attribute.Editor.editorTypeAttr"меняется
    , thisPkg.getSEditorAttrName(attr) //значения свойства
    )

## `OnrefreshExt` и значение даты с выборки

В `onRefreshExt` нет приведения `/*NDate*/`, нужно писать через `/*NString*/`.

В `onRefreshExt` в конструкции with объявляются поля, которые нужно получить с выборки (из кэша). Иногда требуется указать тип данных. Тип данных указывается в `/*...*/`, но парсер на знает `NDate`, поэтому необходимо указать, как `NString` и далее использовать в запросе `cast(... as timestamp)` или `as date`.

    override protected def onRefreshExt: String = {
      s"""with t as ( select
           :id as id
          ,:idState as idState
          ,:gidSrc/*@NString*/ as gidSrc
          ,:idObjectType as idObjectType
          ,:idDepOwner as idDepOwner
          ,:idDischargePlace as idDischargePlace
          ,:idSegregationGroup as idSegregationGroup
          ,:idService as idService
          ,:idFlyOverWay as idFlyOverWay
          ,:idStateMC as idStateMC
          ,:dPlanEnd /*@NString*/ as dPlanEnd
          ,:dPlanBegin /*@NString*/ as dPlanBegin
        )
        SELECT
           t.id
          ,to_char(cast(t.dPlanEnd as timestamp) - cast(t.dPlanBegin as timestamp), 'dd д. hh24 ч.') as nPlanBusy
          ,t1.sHeadLine_dz as idStateHL
          ,t2.sHeadLine_dz as idObjectTypeHL
          ,t3.sHeadLine_dz as idDepOwnerHL
          ,t4.sHeadLine_dz as idDischargePlaceHL
          ,t5.sMnemoCode_dz as idSegregationGroupHL
          ,t6.sHeadLine_dz as idServiceHL
          ,t7.sHeadLine as gidSrcHL
          ,t8.sHeadLine_dz as idFlyOverWayHL
        FROM t
          LEFT JOIN  Btk_ClassState t1 on t.idState = t1.id
          LEFT JOIN  Btk_ObjectType t2 on t.idObjectType = t2.id
          LEFT JOIN  Bs_DepOwner t3 on t.idDepOwner = t3.id
          LEFT JOIN  Bs_Placement t4 on t.idDischargePlace = t4.id
          LEFT JOIN  Oil_SegregationGroup t5 on t.idSegregationGroup = t5.id
          LEFT JOIN  Gds_Service t6 on t.idService = t6.id
          LEFT JOIN  Btk_Object t7 on t.gidSrc = t7.gidRef
          LEFT JOIN  Oil_FlyOverWay t8 on t.idFlyOverWay = t8.id
      """
    }

## Поиск отображения на выборке

`selection.form.findSelection(...)`

Ссылка

Полезный метод, который позволяет найти отображение по системному имени среди видимых в сессии.

Пример использования:

    val sel = selection.form.findSelection(Bdg_ForecastAvi.card())

## Пользовательская блокировка

Пользовательская блокировка включается при взаимодействии пользователя с интерфейсом в методе `Dvi#beforeEdit`, который является результатом кодогенератора.

Принудительно можно включать блокировку с помощью вызова метода:

- `Btk_FormSessionApi().lockObject(gid)`

- `Btk_FormSessionApi().lockObjectMulti(gida)`

Больше про пользовательскую блокировку можно узнать здесь.

## Объект класса в процессе создания и другие состояния объекта rop

Иногда, есть необходимость в Avi знать, что запись находится в процессе создания, т.е. не имеет реализации, как запись в таблице БД.

Это можно узнать по объекту записи `rop`:

- используйте метод Avi `thisRop()` чтобы получить объект `rop`, чья карточка открыта или на котором стоит фокус, если отображение `list`.

- если `rop.ropMode == InsertRopMode`, то объект находится в процессе создания, т.е. есть в кэше приложения, но не имеет реализации в таблице БД.

Где поле объекта `rop.ropMode` хранит в себе информацию состояния объекта. Есть и иные состояния объекта:

- ReadRopMode.

- UpdateRopMode.

- DeleteRopMode.

- InsertRopMode.

Пример кода из проекта:

        //Документ должен быть закоммичен, чтобы были пройдены все соответсвующие проверки,
        // т.к. по закрытию card_ReadFromFile будет flush()
        if (rop.ropMode == InsertRopMode) {
          throw AppException("Для вызова операции необходимо, чтобы документ был заполенен и сохранён.")
        }

## Как узнать, что выборка является главным меню или главной выборкой формы

В системе есть 3 типа форм:

- главная;

- модальная;

- MDI (отображается в качестве закладки на главной форме) .

У каждой формы есть главная выборка. Флаг `selection.isMainOnForm` и указывает, что выборка - главная на форме.

Условие `application.mainSelection == selection` определит, является ли выборка главным меню.

## Фильтрация в деревовидном списке (отображение Tree классов-деревьев)

### Как выводить ветку дерева до объекта, который удовлетворяет условию фильтрации

Решение такой задачи в каждом случае может быть индивидуальным.

Примеры решений:

#### Через денормализированную таблицу

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

Что такое Денормализация классов-деревьев и как её создать можно узнать здесь.

Пример условия:

    t.id in (
      select denorm.idParent
      from Btk_TreeExampleDenormalization denorm
      join Btk_TreeExample ex on ex.id = denorm.idChild
      where (upper(ex.sCode) like upper('%'||:filter$flt_sCode||'%'))
      )

#### Постройка запроса recursive снизу вверх

Строить запрос с нижних объектов, которые необходимо отфильтровать, дальше строить ветки вверх.

    --рекурсия
    with recursive RecursiveTreeExample as (
      --нижнии объекты дерева, которые фильтруются
      select id, idParent, sCode
      from Btk_TreeExample
      --условие
      where upper(sCode) like upper('%' || :filter$flt_sCode || '%')
      --union на случай, если в одной ветке несколько объектов подходят под фильтр
      union
      --постройка ветки вверх - присоединение родителей idParent
      select e.id, e.idParent, e.sCode
      from Btk_TreeExample e
      --присоединение родителей idParent
      join RecursiveTreeExample rc on e.id = rc.idParent
    )
    select id, idParent, sCode
    from RecursiveTreeExample;

Примечание

PostgreSQL оптимизирует рекурсивные запросы, поэтому они могут быть достаточно эффективными, особенно для относительно небольших деревьев.