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

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

Пример, где подсчитывается сумма по записям коллекции 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.

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

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

Загрузка данных из Excel#

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

Доступные средства#

Для реализации загрузки можно использовать:

  • files.uploadFileToTemp — загрузка выбранного пользователем файла во временное хранилище;

  • Bts_XlsxPkg — чтение значений ячеек Excel и преобразование их в строковые, числовые и датовые значения;

  • стандартные API работы с сущностями и транзакциями — создание, изменение и сохранение записей.

Общесистемные средства обеспечивают работу с файлом и его содержимым. Структура формы загрузки, сопоставление колонок, проверки и правила создания или обновления сущностей определяются в прикладном коде.

Принцип реализации#

Для реализации загрузки данных из Excel:

  1. Добавьте операцию, из которой пользователь сможет выбрать Excel-файл.

  2. Загрузите выбранный файл во временное хранилище.

  3. Получите сведения о структуре Excel-файла и необходимые значения: доступные листы, границы данных и значения ячеек.

  4. При необходимости реализуйте форму для настройки параметров загрузки.

  5. Определите правила сопоставления колонок Excel-файла с атрибутами загружаемой сущности.

  6. Реализуйте обработку строк, включая преобразование значений и поиск связанных объектов.

  7. Определите условия создания новой записи и обновления существующей.

  8. Определите обработку ошибок и границы транзакции.

Конкретная реализация этих этапов зависит от структуры загружаемой сущности и требований к формату Excel-файла.

Пример реализации#

Пример загрузки реализован для расчетов годовой арендной платы Prt_RentCalculation в модуле prt.

В примере используются:

  • Prt_RentCalculationAvi#List.importFromXlsx() — операция, из которой запускается загрузка;

  • Prt_RentCalculationAvi#Card_LoadFromExcel — форма выбора листа, диапазона строк и режима проверки заголовка;

  • Prt_RentCalculationApi.loadFromExcel() — обработка строк Excel-файла и создание или обновление расчетов.

После выбора Excel-файла операция получает список листов и номер последней зарегистрированной строки каждого листа. Эти сведения используются для формирования исходных значений формы загрузки.

После подтверждения формы выбранные параметры передаются в Prt_RentCalculationApi.loadFromExcel().

Сигнатура метода:

def loadFromExcel(
  file: File,
  idpRentTemplate: NLong,
  npHeaderRow: NNumber,
  npFirstRow: NNumber,
  npLastRow: NNumber,
  spSheetName: NString,
  bpValidateHeader: Boolean
): Unit

Параметр

Назначение

file

Excel-файл

idpRentTemplate

Идентификатор шаблона, определяющего дополнительные параметры расчета и их порядок в файле

npHeaderRow

Номер строки заголовка, начиная с 1

npFirstRow

Номер первой обрабатываемой строки, начиная с 1

npLastRow

Номер последней обрабатываемой строки включительно, начиная с 1

spSheetName

Точное наименование обрабатываемого листа

bpValidateHeader

Признак проверки заголовка и определения колонок по их наименованиям

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

Обработка данных

loadFromExcel() обрабатывает выбранный лист в диапазоне от npFirstRow до npLastRow включительно.

Строка пропускается, если соответствующий объект строки в Excel-файле отсутствует (getRow == null). Физически существующая строка обрабатывается даже при незаполненных ячейках.

Строка заголовка обязательна даже при bpValidateHeader = false. Непустые повторяющиеся заголовки не допускаются в обоих режимах.

При bpValidateHeader = true колонки определяются по наименованиям заголовков, поэтому их физический порядок в Excel-файле не имеет значения. нки формулы — Формула расчета.

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

Для каждой строки способ обработки определяется по колонке Идентификатор:

  • если значение не заполнено, создается новый Prt_RentCalculation;

  • если идентификатор указан, выполняется поиск существующего расчета и его обновление;

  • если запись по указанному идентификатору не найдена, обработка завершается ошибкой.

Ссылочные значения Вид исполь., Участок, Договор, Формула расчета, а также ссылочные параметры шаблона задаются мнемокодами.

Если объект с указанным мнемокодом не найден, обработка завершается ошибкой. Если найдено несколько объектов с одинаковым мнемокодом, обработка завершается ошибкой AppException.

Помимо основных атрибутов расчета метод обрабатывает параметры выбранного шаблона, сумму и формулу расчета.

При первой выброшенной ошибке дальнейшая обработка прекращается. Ошибки при заполнении атрибутов преобразуются в AppException с указанием номера строки Excel и наименования поля.

После успешной обработки всех строк loadFromExcel() выполняет session.commit().

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

Реализация Prt_RentCalculation находится в модуле prt. Для использования этой реализации модуль prt должен быть подключен.

Prt_RentCalculationApi.loadFromExcel() предназначен для загрузки Prt_RentCalculation. Для другой сущности правила сопоставления колонок, обработки строк и создания или обновления записей необходимо реализовать с учетом ее структуры.

При возникновении ошибки loadFromExcel() не выполняет session.commit() и явно не вызывает rollback. Созданные и измененные до возникновения ошибки объекты остаются в текущей транзакции до ее завершения внешним владельцем. Способ завершения или отката внешней транзакции после ошибки в исследованной реализации не определен и требует уточнения.

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

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