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

Концепция нехранимых строк#

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

Пример в проекте 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’а.

Регистрация записи при редактировании пустой строки#

Необходимо переопределить операцию 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().

Иначе load() внутри afterEdit() будет вызван по некорректного 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() в тексте запроса.