Практика код#

Вычисление суммы без использования буфера#

Пример, где подсчитывается сумма по записям коллекции Oil_PlanFactoryShipPos:

//получаем все записи коллекции по родителю (объект rop)
Oil_PlanFactoryShipPosApi().byParent(rop)
//получаем только значение поля nQtyLoad
//в случае незаполненности поля необходимо иметь значение 0, иначе в дальнейшем сумма с null = null
.map(_.get(_.nQtyLoad).nvl(0.nn))
//сложение, результат которого будет обернут безопасной конструкцией Option
.reduceOption(_ + _)
//распаковка Option
//если внутри был null или в коллекции не было записей, то вернётся указанное значение 0.nn
.getOrElse(0.nn)

Группировка объектов с использованием null-типов#

При группировке объектов коллекций, в которых используется null-типы нужно учитывать, что метод groupBy считает хэши группируемых объектов, поэтому перед группировкой, нужно убедиться, что параметр, используемый для группировки, не равен null.

Пример, с NPE:

List(
  asd(123.nl, "asd".ns),
  asd(None.nl, "asd".ns), // У NLong с underlying = null хэш не посчитается
).groupBy(_.id).foreach { case (_, name) =>
  println(name)
}

Перед использованием groupBy необходимо отфильтровать значения или использовать метод nvl.

Сравнение диапазона дат#

Допустим документы имеют поля dBeginDoc и dEndDoc.

Фильтр имеет 2 поля dBeginFilter и dEndFilter.

Чтобы найти все документы, которые началом или окончанием входят в диапазон, указанный в фильтре, нужно использовать следующее условие:

dBeginDoc <= dEndFilter && dEndDoc >= dBeginFilter

.distinct или .toSet для scala-коллекций и особенности применения#

Если необходимо в scala-коллекции держать только уникальные объекты, можно использовать методы:

  • .distinct

  • .toSet

Перед использованием метода .distinct scala-коллекцию необходимо подготовить: убрать значения null. Иначе в ходе выполнения программы выпадет ошибка java.lang.NullPointerException.

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

  test("distinctOrToSet") {

    val data: Seq[NString] = Seq("h".ns, "i".ns, "i".ns, None.ns, None.ns)

    println("Результат работы .distinct с null внутри scala-коллекции:")
    try {
    println(data.distinct)
    } catch {
      case e: Throwable => println("Ошибка:\n" + "java.lang.NullPointerException")
    } finally {

      println("\nРезультат работы .filter(_.isNotNull).distinct:")
      println(data.filter(_.isNotNull).distinct)

      try {
        println("\nРезультат работы .toSet с null внутри scala-коллекции:")
        println(data.toSet)
      } catch {
        case e: Throwable => println("Ошибка:\n" + "java.lang.NullPointerException\n")
      }
    }
  }

Результат:

Примечание

Результат работы .distinct с null внутри scala-коллекции: Ошибка: java.lang.NullPointerException

Результат работы .filter(_.isNotNull).distinct: List(h, i)

Результат работы .toSet с null внутри scala-коллекции: Set(h, i, Null)

immutable.Map.builder вместо mutable.Map#

Если по результату сформированной Map она больше не изменяется, то для формирования лучше использовать конструктор immutable.Map.builder, чем mutable.Map.

Пример:

val map = Map.newBuilder[NString, NString]
map ++= Map("1" -> "11")
map += "2" -> "22"
map.result() //Map("1" -> "11", "2" -> "22")

Признак наличия модуля на проекте#

val isInstallProModule = session.sbtClassLoader.getModuleMap.containsKey("pro")

Вернёт true, если такой модуль есть в проекте, иначе false.

Предупреждение

Поддержка этого метода не гарантируется.

Применение ASQL/ ASelect/ OQuery/ TxIndex/ refreshByParent и byParent#

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

ASQL#

Выполнение реляционного запроса на чтение.

ASQL

Внимание

Вызывает транзакцию в БД, не учитывая значения в кэше.

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

    val idaWagonByTrain = ASQL"""
      select string_agg(cast(rwt.idWagon as varchar), ', ')
      from Rzd_TrainWagon rwt
      where rwt.idTrain = $idpTask
    """
    //берется 1ый столбец результата запроса, как NString
    //если результат запроса вернул несколько строк, то будет взята первая в конструкции Option,
    // но предполагается, что результат вернёт максимум 1 строку
    .as(nStr(1).singleOpt)
    //если Option пустой, т.е. в результате запроса не было строк, то вернётся None.ns
    .getOrElse(None.ns)

$idpTask это подстановка значения из переменной scala idpTask в запрос связанной переменной (binding).

Внимание

Нельзя использовать внутри цикла. Это приведёт к многочилсенным транзакциям в БД.

Вместо использования ASQL внутри цикла с множественными транзакциями в БД нужно перед циклом одной транзакцией сформировать массив данных, который дальше будет использоваться внутри цикла.

ATSQL#

Выполнение реляционного запроса с изменением данных или блокировками.

ATSQL

Используется редко, в основном в ядровых процедурах, потому что минует серверную логику, записи в системные миксины, ведение аудитов и другое.

ASelect#

Выполнение реляционного запроса на чтение/запись.

ASelect

Внимание

Вызывает транзакцию в БД, не учитывая значения в кэше.

Используется в основном для чтения, потому что при изменении данных минует серверную логику, записи в системные миксины, ведение аудитов и другое.

Удобен для получения значений по нескольким столбцам результата запроса.

val mapPerson: Map[NLong, NString] = new ASelect {
  val sCode = asNString("sCode")
  val id = asNLong("id")
  SQL"""
    select p.id
          ,p.sCode
    from Bs_Person p
    where idObjectType = $idvObjectType
  """
}.map { rv => //rv представляет собой одну строку результата запроса
  rv.id() -> rv.sCode()
}.toMap

$idvObjectType это подстановка значения из переменной scala idvObjectType в запрос связанной переменной (binding).

Внимание

Нельзя использовать внутри цикла. Это приведёт к многочилсенным транзакциям в БД.

Вместо использования ASelect внутри цикла с множественными транзакциями в БД нужно перед циклом одной транзакцией сформировать массив данных, который дальше будет использоваться внутри цикла.

Когда использовать ASQL, а когда ASelect#

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

ASelect удобен для получения значений по нескольким столбцам результата запроса.

Подстановка связанных переменных (binding)#

Актуально для инструментов ASQL, ATSQL, ASelect.

Примечание

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

Инструмент ASQL"""<текст запроса>""" подставляет бинды с учётом типа данных переменной.

  • Если бы была подстановка NString, то ASQL сам обернул бы значение в одинарные кавычки, т.е. нет необходимости их указывать вручную.

  • Если NString подставляется в текст s"""<текст запроса>""", то одинарные кавычки необходимо указывать вручную.

Если текст запроса для ASQL"""<текст запроса>""" собирается динамически средствами scala, в том числе название таблицы для select формируется переменной, то его необходимо подставлять через #$bind, чтобы ASQL не обернул подставляемое значение в одинарные кавычки.

val sNameTb = {
  if (a = 1) "Bs_Goods".ns
  else "Bs_Person".ns
}

val ida: List[NLong] = ASQL"""
  select t.id 
  from #$sNameTb
  where idObjectType = $idvObjectType
""".as(nLong(1).*)

Существует иной формат binding, когда запрос формируется в переменной String/NString, где указываются ключи для подстановки значения, карта подстановки указывается в .on()

val svRequest =
 s"""
  select t.id 
  from Bs_Goods
  where idObjectType = {idObjectType}
"""
ASQL(svRequest).on("idObjectType" -> idvObjectType)

new ASelect {
 val sCode = asNString("sCode")
 val id = asNLong("id")
 SQL(s"""
    select p.id
          ,p.sCode
    from Bs_Person p
    where idObjectType = {idObjectType}
  """)
  .on("idObjectType" -> idvObjectType)
}.map { rv => //rv представляет собой одну строку результата запроса
 rv.id() -> rv.sCode()
}.toMap

Может быть полезно, если запрос собирается в переменной String/NString.

OQuery#

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

  • Синтаксис схож с реляционным запросом

  • Возвращает результат с учётом кэша

  • Результат - список Rop, что удобно для использования методов, которым требуется Rop

OQuery

Пример запроса:

new OQuery(Bs_GoodsAta.Type) {  
  where(t.sSystemName === spMnemoCode)  
}.toVector

Особенности, которые нужно знать про OQuery:

Условие применяется для запроса в БД#

Условие применяется для запроса в БД. Поэтому поиск записей идёт в БД без учёта кэша, а результат возвращается с учётом кэша. Пример:

val idvGds = 121.nl 
val ropGds = Bs_GoodsApi().load(idvGds)
//ТМЦ переводится в состояние Отменено (0)
Bs_GoodsApi().setidState(ropGds, idvStateCancel)
//Поиск неотмененных ТМЦ с условием по категории
new OQuery(Bs_GoodsAta.Type) {  
  where(t.idCategory = idvGdsCategory 
          and t.idStateMC >> 0.nn)  
}.toVector

В результате OQuery будет запись Bs_Goods c id = 121, при чём с состоянием «Отменено» , потому что:

  • в БД эта запись не Отменена (t.idStateMC > 0) , а изменение по отмене записи находится в кэше и не синхронизировано с БД

  • результат OQuery возвращается с учётом кэша , поэтому на запись c id = 121 применён кэш - смена состояния

Решить можно 2 способами:

/** Вариант 1 - синхронизация с БД */
//синхронизация с БД
session.flush()
//Поиск неотмененных ТМЦ с условием по категории
new OQuery(Bs_GoodsAta.Type) {  
  where(t.idCategory = idvGdsCategory 
          and t.idStateMC >> 0.nn)  
}.toVector

/** Вариант 2 - вынос условия из OQuery, фильтрация результата OQuery */
//Поиск неотмененных ТМЦ с условием по категории
new OQuery(Bs_GoodsAta.Type) {
 where(t.idCategory = idvGdsCategory)
}
  //вынос условия из OQuery
  .filter(_.get(_.idStateMC) > 0.nn)

Предупреждение

  1. Вариант 1 - синхронизация с БД с помощью session.flush() имеет недостаток: у пользователя пропадает возможность отменить изменения на выборке.

  2. Вариант 2 - вынос условия из OQuery, фильтрация результата OQuery имеет недостаток: OQuery в память выгружает больше записей из БД, что может требовать приемов по работе с большими данными (см. раздел «Разработка под высокую нагрузку»).

Для предварительной прогрузки записей в кэш необходима инициализация OQuery#

Про предварительную прогрузку записей в кэш можно прочитать здесь.

Чтобы данные были загружены в кэш, необходима инициализация OQuery , которая происходит при использовании обходчика foreach или преобразовании OQuery к иной коллекции:

//Предварительная выгрузка записей в кэш
new OQuery(Bs_GoodsAta.Type) {  
  where(t.sSystemName === spMnemoCode
          and t.idStateMC >> 0.nn)
}
  //для инициализации OQuery
  .toVector

Транзакционный индекс#

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

  • Поиск по одному полю по условию равенства

  • Нет возможности искать по составному индексу из нескольких полей класса

  • Ищет и возвращает результат с учётом кэша

  • Результат - список Rop, что удобно для использования методов, которым требуется Rop

Транзакционный индекс.

Пример запроса:

//Объявляется обычно в Api класса соответствующего Ata (у примера Bs_GoodsApi)
lazy val idxCategory = TxIndex(Bs_GoodsAta.Type)(_.idCategory)

/** Поиск неотмененных ТМЦ по категории */
def byCategoryNotCanceled(): Iterable[ApiRop] = {
 idxCategory.byKey(idvGdsCategory)
  .filter(_.get(_.idStateMC) > 0.nn)
}

Если нужен запрос по нескольким условиям, то используйте индекс по полю, который вернёт меньшее кол-во записей в память, после чего примените остальные условия используя .filter().

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

refreshByParent и byParent#

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

  • Имеется только у коллекций

  • Поиск по ссылочному полю на мастера

  • Возвращает результат с учётом кэша

  • Результат - список Rop, что удобно для использования методов, которым требуется Rop

  • refreshByParent инвалидирует записи из БД и возвращает актуальные данные, гарантированно делает транзакцию в БД.

  • byParent возвращает данные из памяти, если они там были, иначе первично получает актуальные данные из БД, делая транзакцию.

  • Перед использованием byParent в цикле для предварительной прогрузки в кэш можно использовать OQuery (предварительная прогрузка.)

Про разницу между refreshByParent и byParent можно почитать здесь.

Какой инструмент использовать#

Загрузка множества объектов

Если нужны записи с учётом кэша, то используйте:

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

  • реляционный запрос с предварительным session.flush (не для построения выборок)

Предупреждение

  1. Синхронизация с БД с помощью session.flush() имеет недостаток: у пользователя пропадает возможность отменить изменения на выборке.

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