Динамические столбцы#
Введение#
Динамические столбцы - это колонки, которые формируются на этапе отображения данных, не меняя физическую структуру таблицы в базе данных. Они используются, когда набор колонок заранее неизвестен и зависит от данных, настроек пользователя или бизнес-логики.
Когда использовать динамические столбцы#
Динамические столбцы нужны, когда:
количество колонок определяется во время выполнения;
колонки строятся на основе набора дат, атрибутов, периодов, объектов;
структуру списка нельзя заранее описать статически;
нужно показать несколько однотипных значений в отдельных колонках.
Примеры:
колонка на каждую дату;
колонка на каждый атрибут объекта;
колонка на каждый найденный период;
колонка, имя и заголовок которой рассчитываются динамически.
Подходы к реализации#
Существует несколько основных способов реализации динамических столбцов:
SQL-подзапросы - Данные только на чтение, простая логика, независимость от кэша
DynMetaBuilder+DynRecBuilder- Интерактивные интерфейсы, редактируемые данные, сложная бизнес-логика, работа с кэшемonRefreshExtendMeta+onRefreshExtendValuesвCard_ObjectAttr- Упрощённый сценарий для карточки объектных характеристик, когда динамические атрибуты относятся к текущей записи карточки
Примечание
Если колонка должна вести себя как обычный атрибут представления, чаще всего правильнее использовать DynMetaBuilder + DynRecBuilder.
Базовое правило именования#
Ключевой механизм динамических столбцов — разделение имени на базовую часть и индекс.
Общий формат:
baseName[idx]
Примеры:
date[0]date[1]nStartYear[13]sStyle[3]
Настройки по базовому имени применяются ко всем однотипным колонкам. Через базовое имя удобно централизованно задавать общие настройки отображения. Если для колонки используются отдельные style- или editor-атрибуты, они тоже должны быть динамическими и строиться по той же схеме с индексом.
Примечание
Символ | в caption разделяет подпись колонки и имя бенд-группы.
Пример:
builder.add(s"nStartYear[$idx]", classOf[NNumber], "Год начала|Период")
В интерфейсе:
подпись колонки:
Год начала;группа:
Период.
DynMetaBuilder и DynRecBuilder#
При использовании DynMetaBuilder и DynRecBuilder работа обычно строится в два этапа:
описать метаданные колонки;
заполнить значения по строкам.
Базовая схема:
override protected def onRefresh: Recs = {
val data = loadData()
data
.extend(buildDynMeta().build())
.foreach((rop, builder) => {
fillDynamicValues(rop, builder)
(rop, builder.build)
})
}
Где:
buildDynMeta()объявляет новые колонки;fillDynamicValues(...)записывает значения в уже объявленные имена.
Внимание
Имя в builder.set(...) должно полностью совпадать с именем, которое было зарегистрировано через builder.add(...).
Пример: динамические колонки по датам#
Регистрация колонок#
def getObjAttrDynMetaBuilder(dapDates: Iterable[(NDate, Int)]): DynMetaBuilder = {
val builder = DynMetaBuilder()
dapDates.foreach { case (date, idx) =>
builder.add(s"date[$idx]", classOf[NNumber], date.toString("dd.MM.yyyy"))
}
builder
}
Что здесь происходит:
для каждой даты создаётся отдельная колонка;
техническое имя хранит индекс;
заголовок колонки строится из даты.
Заполнение значений#
def getDynamicObjAttr(
currentRop: Rop[JLong, Btktst_DynamicColumnsAro],
data: scala.List[Btktst_DynamicColumnsApi#ApiRop],
dapDates: Map[NDate, Int],
builder: DynRecBuilder
): DynRecBuilder = {
builder.set(
s"date[${dapDates(currentRop.get(_.dDate))}]",
currentRop.get(_.nQuantity)
)
builder
}
Здесь:
date[idx]уже объявлен на этапе метаданных;теперь в него записывается конкретное значение;
индекс определяется по карте
date -> idx.
Настройка отображения#
После регистрации динамической колонки её можно настраивать так же, как обычный атрибут.
Через AVM#
Если настройка должна применяться ко всем однотипным колонкам, используется базовое имя:
<attr name="date">
<grid columnWidth="5"/>
</attr>
Внимание
В AVM для общей настройки динамических колонок указывается базовое имя без индекса.
Реализация через SQL#
Этот вариант используется, когда значение колонки можно полностью получить на стороне SQL.
Когда использовать#
отчёт только для чтения;
значение не нужно редактировать;
не требуется динамический редактор;
логика хорошо выражается SQL-подзапросом.
Пример#
override protected def selectStatement: String = {
val data = new OQuery(Btktst_DynamicColumnsAta.Type).toList
val davZipDates = data.view.map(_.get(_.dDate)).distinct.zipWithIndex.toMap
val dynColumnsSql = davZipDates.map { case (date, idx) =>
s"""
,(SELECT t_inner.nQuantity
FROM Btktst_DynamicColumns t_inner
WHERE t_inner.id = t.id
AND t_inner.dDate = DATE '${date.toString("yyyy-MM-dd")}'
LIMIT 1) as "${date.toString("dd.MM.yyyy")}"
"""
}.mkString("")
s"""
SELECT
t.id
,t.sCode
${dynColumnsSql}
&defDynAttrsMacro
FROM Btktst_DynamicColumns t
&defDynAttrsFromMacro
"""
}
Особенности SQL-подхода#
колонка формируется сразу в выборке;
DynMetaBuilderне используется;этот вариант удобен для read-only сценариев;
если нужен editor-атрибут, style-атрибут или дополнительная метаинформация, чаще лучше перейти на
DynMetaBuilder.
Примечание
В проектных примерах SQL-колонка может формироваться как технический атрибут вида name[idx], а может сразу иметь человекочитаемый alias. Эти сценарии стоит различать.
Реализация через DynMetaBuilder и DynRecBuilder#
Это основной вариант для полноценных динамических атрибутов представления.
Общий шаблон#
override protected def onRefresh: Recs = {
val data = thisApi().refreshByParent(getIdMaster)
data
.extend(getObjAttrDynMetaBuilder().build())
.foreach { (rop, builder) =>
val dynamicInfo = getDynamicObjAttr(rop, builder)
(rop, dynamicInfo.build)
}
}
Пример 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)
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
}
Этот пример показывает, что вместе с основной колонкой можно зарегистрировать:
дополнительное HL-поле;
скрытый editor-атрибут;
служебные атрибуты для отображения.
Пример getDynamicObjAttr#
def getDynamicObjAttr(rop: Rop[_, _], builder: DynRecBuilder): DynRecBuilder = {
val svLetterInRuleCaption = thisApi().getLetterInProcessRule(rop).sCaption
builder.set(A.sLetterInProcessRule.name + "HL", svLetterInRuleCaption.get)
builder
}
Идея та же:
метаданные описываются отдельно;
значения записываются отдельно;
каждая служебная колонка тоже должна быть зарегистрирована заранее.
Дополнительные данные строки через extend2(...)#
Иногда нужно вернуть не только динамические колонки, но и дополнительный объект с вычисленными данными строки.
Для этого используется extend2(...).
thisApi().refreshByParent(getIdMaster)
.extend2(classOf[AdditionalInfo], dynMetaBuilder.build())
.foreach { (rop, builder) =>
val dynamicInfo = getDynamicObjAttr(rop, builder)
(rop, getAdditionalInfo(rop.asInstanceOf[SomeApi#ApiRop]), dynamicInfo.build)
}
Когда это полезно:
нужны дополнительные вычисленные данные для строки;
логика динамических колонок тесно связана с
AdditionalInfo;не хочется дублировать вычисления в нескольких местах.
Динамические стили#
Для динамической колонки можно зарегистрировать отдельный style-атрибут. Такой атрибут тоже должен быть динамическим и использовать тот же индекс, что и value-атрибут.
Регистрация метаданных#
def getObjAttrDynMetaBuilder(dapDates: Iterable[(NDate, Int)]): DynMetaBuilder = {
val builder = DynMetaBuilder()
dapDates.foreach { case (date, idx) =>
builder.add(s"date[$idx]", classOf[NNumber], date.toString("dd.MM.yyyy"))
builder.add(s"sStyle[$idx]", classOf[NString], "стиль")
}
builder
}
Заполнение значений#
builder.set(s"date[$idx]", currentRop.get(_.nQuantity))
builder.set(s"sStyle[$idx]", "FontColor=clCream;Color=clRed")
Настройка в AVM#
<attr name="date">
<style attr="sStyle"/>
<grid columnWidth="5"/>
</attr>
<attr name="sStyle" isVisible="false"/>
Важное правило#
Если значение хранится в date[idx], то стиль для этой же ячейки должен храниться в sStyle[idx].
Внимание
Если style задаётся через отдельный атрибут, этот атрибут должен быть зарегистрирован как динамический. У value-атрибута и style-атрибута должен совпадать индекс. Иначе стиль будет применён некорректно.
Динамические редакторы#
Редактируемая динамическая колонка обычно требует отдельного скрытого атрибута, в котором хранится конфигурация редактора. Такой атрибут тоже должен быть динамическим: если значение живёт в test[idx], то конфигурация редактора должна жить, например, в sEditor[idx].
Пример: List_EditorAvm.
Регистрация колонок#
val columnName = s"test[$idx]".ns
val editorName = s"sEditor[$idx]".ns
builder.add(columnName, classOf[NNumber], date.toString("dd.MM.yyyy"))
builder.add(editorName, classOf[String], "Editor")
Построение редактора#
val sEditor = DynamicEditorBuilder
.currency()
.build()
builder.set(columnName, currentRop.get(_.nQuantity))
builder.set(editorName, sEditor)
Настройка в AVM#
<attr name="test">
<editor editorTypeAttr="sEditor"/>
<grid columnWidth="50"/>
</attr>
<attr name="sEditor" isVisible="false"/>
Как это работает:
test[idx]хранит значение;sEditor[idx]хранит описание редактора;editorTypeAttr="sEditor"связывает пользовательскую колонку и служебный атрибут.
Внимание
Если редактор задаётся через отдельный атрибут, этот атрибут должен быть динамическим. Статический sEditor не подойдёт для набора динамических колонок test[idx].
Упрощённая реализация в карточках объектных характеристик#
Для карточек объектных характеристик доступен более короткий сценарий через специальные хуки.
Используются методы:
onRefreshExtendMeta(dynMetaBuilder: DynMetaBuilder)onRefreshExtendValues(dynRecBuilder: DynRecBuilder)
Они позволяют не писать вручную цепочку extend(...).foreach(...).
Пример#
override def onRefreshExtendMeta(dynMetaBuilder: DynMetaBuilder): Unit = {
super.onRefreshExtendMeta(dynMetaBuilder)
val basicDataGroup = Btk_ObjectAttrGroupByObjectTypeApi()
.findByMnemoCode(Btk_ObjectTypeApi().load(getCurIdObjectType), "Attr_groupBasicData")
val sGroupAttr = if (basicDataGroup.isNotNull) {
Btk_ObjectAttrGroupByObjectTypeApi().load(basicDataGroup).get(_.sCaption)
} else "Основные данные"
dynMetaBuilder.add("dManufYearDisplay", classOf[String], s"Год выпуска|$sGroupAttr", 3.nn)
}
override def onRefreshExtendValues(dynRecBuilder: DynRecBuilder): Unit = {
super.onRefreshExtendValues(dynRecBuilder)
val convertedManufYear = if (thisRop().get(_.dManufYear).isNotNull) {
thisRop().get(_.dManufYear).toNLocalDateTime.get.getYear.toString
} else ""
dynRecBuilder.set("dManufYearDisplay", convertedManufYear)
}
Когда использовать этот вариант#
если вы работаете именно с карточкой объектных характеристик;
если динамические колонки относятся к текущей записи карточки;
Как настраивать динамические колонки#
Правило простое:
в
add(...)иset(...)используется полное имя;в AVM для общей настройки используется базовое имя.
Пример настройки через AVM#
<attr name="dManufYearDisplay">
<style attr="sAvailStyle"/>
<grid columnWidth="5" isColumnWidthFixed="true"/>
</attr>
Вывод#
DynMetaBuilderописывает динамические колонки;DynRecBuilderзаполняет их значениями;полное имя используется при регистрации и заполнении;
базовое имя используется для общей настройки;
стили и редакторы через отдельные атрибуты тоже должны быть динамическими и использовать тот же индекс;
для карточек объектных характеристик удобнее использовать
onRefreshExtendMetaиonRefreshExtendValues;SQL-подход хорош для read-only сценариев, а
DynMetaBuilder/DynRecBuilder— для полноценных динамических атрибутов представления.