Нехранимые строки в коллекциях#
Концепция нехранимых строк#
Здесь будет описан пример реализации пустых предзаполненных строк в списке, как частный случай, в коллекции. Такие строки не хранятся в БД, а существуют визуально в интерфейсе (иными словами нехранимые строки). Запись в БД создаётся при вводе пользователем какого-либо поля.
Пример в проекте 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()в тексте запроса.