Динамические столбцы#

Введение#

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

Когда использовать динамические столбцы#

Динамические столбцы нужны, когда:

  • количество колонок определяется во время выполнения;

  • колонки строятся на основе набора дат, атрибутов, периодов, объектов;

  • структуру списка нельзя заранее описать статически;

  • нужно показать несколько однотипных значений в отдельных колонках.

Примеры:

  • колонка на каждую дату;

  • колонка на каждый атрибут объекта;

  • колонка на каждый найденный период;

  • колонка, имя и заголовок которой рассчитываются динамически.

Подходы к реализации#

Существует несколько основных способов реализации динамических столбцов:

  • 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 работа обычно строится в два этапа:

  1. описать метаданные колонки;

  2. заполнить значения по строкам.

Базовая схема:

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