Перейти к основному содержанию
`Ctrl`+`K`

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

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

Пример, где подсчитывается сумма по записям коллекции `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`, либо группироваться по Option[?] использовав метод `option`.

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

- Коллекции с сортировкой для простых Nullable
  Пример:

      val avIdEiToRop = ropa.view.map(r => r.get(_.idExpenceItem) -> r).to(mutable.SortedMap)
      avIdEiToRop.get(None.nl).foreach { ropInDim => }

  При создании коллекций в scala с уникальным ключом возможны варианты использования:

  - отсортированной коллекции, имеющей обычно префикс Sorted или Tree, например, `collection.SortedMap`, `collection.immutable.TreeMap`

  - коллекции, для определения уникальности обращающейся к методу hashCode с сравнением результата для определения уникальности. Такие классы используются в часто вызываемых методах: фабричных (`mutable.Map.empty`, `mutable.HashMap`), группировки (`ropa.groupBy(_.get(_.gidObjCalc))`, `immutable.HashMap`), toMap, toSet.

    Взамен применения Hash коллекций, вызывающих `hashCode`, ведущий к NPE, возможно использование Sorted, для чего важно наличие способа упорядочивания для типа (Ordering, что сейчас отсутствует для NGid): `ropa.view.map(r => r.get(_.gidObjCalc)).to(collection.SortedSet) // compile error`

- Tuple с nvl и nullif

  Пример:

      for (((idvEI, gidvOC), ropa) <- ropa.groupBy(_ :> (aro => aro.idExpenceItem.nvl(0) -> aro.gidObjCalc.nvl("0".ng)))) {
        val idvUsedEI = idvEI.nullif(0)
        val idvUsedOC = gidvOC.nullif("0".ng)
      }

  В чем есть неудобство при более чем одном обращении к ключу.

- case class с переопределённым hashCode

  Пример:

      case class Key(gidObjCalc: NGid, idBisObj: NLong) {
        override lazy val hashCode: Int = {
          def hvl(p: Nullable[_, _]) = if (p.isNull) -1.nl else p

          util.hashing.MurmurHash3.orderedHash(Seq(hvl(gidObjCalc), hvl(idBisObj)))
        }
      }

      case class Det()

      val avKeyToDet = mutable.Map.empty[Key, mutable.Buffer[Det]]
      // Заполнение avKeyToDet
      for ((vKey, avDet) <- avKeyToDet) {
        processDim(vKey.gidObjCalc, vKey.idBisObj) // прямо без nullif
      }

  В чем отсутствует недостаток постоянного написания nullif и nvl. Запись hashCode однотипная, шаблонная. В случае хэша lazy val всех входящих свойств важно указание как val констант.

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

Допустим документы имеют поля 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 - синхронизация с БД с помощью session.flush() имеет недостаток: у пользователя пропадает возможность отменить изменения на выборке.

Вариант 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. Для построения выборок, в которых ведутся изменения объекта, используйте объектный запрос.