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

# Scala код

## Использование View или withFilter для предотвращения создания временных коллекций

1. View - это особый вид коллекции в Scala, который берет базовую коллекцию и лениво выполняет методы преобразования в этой коллекции.

   Мы можем преобразовать каждую коллекцию Scala в отложенное представление и обратно с помощью `view` метода. Вместо создания новой коллекции после каждой операции, они применяют сразу всю цепочку преобразований к каждому элементу оригинальной коллекции, благодаря чему цепочка преобразований выполняется за один проход.

   Преобразование виртуальной коллекции в реальную выполняется либо одним из преобразующих методов из серии to{Название коллекции}, либо методом force.

Пример:

    // До
    seq.map(f).flatMap(g).filter(p)

    // После
    seq.view.map(f).flatMap(g).filter(p).force

2. `withFilter` работает так же, как и `view` — создает временный объект, который ограничивает область последующих преобразований коллекции (так, что он реорганизует возможные побочные эффекты). Однако, нет нужды явно преобразовывать коллекцию к (или наоборот) от временного представления (вызвав view и force). Если `filter` используется перед `map`, `flatMap` или `foreach`, то для лучшей производительности вместо него должен использоваться `withFilter`.

Пример:

    // До
    Stm_OrderInDetApi()
          .byParent(rop)
          .filter(f => f.get(_.idStock).isNotNull)
          .foreach { ropDet =>
            if (Stk_StockApi().load(ropDet.get(_.idStock)).get(_.idBisObj).isDistinct(value))
              Stm_OrderInDetApi().setidStock(ropDet, None.nl)
          }

    // После
    Stm_OrderInDetApi()
          .byParent(rop)
          .withFilter(f => f.get(_.idStock).isNotNull)
          .foreach { ropDet =>
            if (Stk_StockApi().load(ropDet.get(_.idStock)).get(_.idBisObj).isDistinct(value))
              Stm_OrderInDetApi().setidStock(ropDet, None.nl)
          }

## Избегайте использования filter несколько раз подряд

Пример:

    // До
    seq.filter(p1).filter(p2)

    // После
    seq.filter(x => p1(x) && p2(x))

## Использование lazy val

Компилятор не сразу вычисляет связанное выражение отложенного значения val. Он вычисляет переменную только при первом обращении к ней.

Пример:

    // До
    val idgForming = Btk_ClassStateApi().findByNameAndIdClass("Forming", idClass)

    // После
    lazy val idgForming = Btk_ClassStateApi().findByNameAndIdClass("Forming", idClass)

## Используйте коллекции, имеющие оптимальную сложность поиска, взамен обладающих линейной при частом обращении за нахождением

Пример:

      //до:
        def registerByParent(ropDoc: RopDoc, idap: CSeq[NLong]): Unit = {
          val idavAlreadyRegistered = byParent(ropDoc).map(_.get(_.idGdsType)).toList

          idap.foreach { idv =>
            if (idv.isNotNull) {
              if (!idavAlreadyRegistered.contains(idv)) {
                val rop = // ...
              }
            }
          }
        }

      //после:
        def registerByParent(ropDoc: RopDoc, idap: CSeq[NLong]): Unit = {
          val idavAlreadyRegistered = byParent(ropDoc).map(_.get(_.idGdsType)).toSet

          idap.foreach { idv =>
            if (idv.isNotNull) {
              if (!idavAlreadyRegistered(idv)) {
                val rop = // ...
              }
            }
          }
        }

## На while и рекурсии добавляйте защиту по кол-ву итераций

Пример:

    // До
    while (dotsNeedUpdate.nonEmpty) {
      val newDotsNeedUpdate = scala.collection.mutable.HashSet[NNumber]()
      ???
      dotsNeedUpdate = newDotsNeedUpdate.toSet
    }
    // После
    var count = 0
    while (dotsNeedUpdate.nonEmpty && count < 10000) {
      val newDotsNeedUpdate = scala.collection.mutable.HashSet[NNumber]()
      ???
      dotsNeedUpdate = newDotsNeedUpdate.toSet
      count += 1
    }

## Не эмулируйте существующие методы работы с коллекциями

Пример:

    // До
    seq.map(f).flatten
    seq.toSet.toSeq
    seq.reverse.iterator
    seq.reverse.map(f)
    seq.collect{case P => ???}.headOption

    // После
    seq.flatMap(f)
    seq.distinct
    seq.reverse.iterator
    seq.reverseMap(f)
    seq.collectFirst{case P => ???}

## Используйте ArrayBuffer вместо ListBuffer

ArrayBuffer выигрывает в производительности ListBuffer при операциях добавления и итерации, а это 90% всего использования. Также ArrayBuffer занимает меньший объем в оперативной памяти, потому что ListBuffer основан на связанном списке.

- Scala Collections Performance

## При проверке на существование - не прибегайте к фильтрации

Пример:

    // До
    seq.filter(p).nonEmpty
    seq.filter(p).isEmpty

    // После
    seq.exists(p)
    !seq.exists(p)

## При проверке на пустоту - не вычисляйте длину

Пример:

    // До
    seq.length > 0
    seq.length != 0
    seq.length == 0

    // После
    seq.nonEmpty
    seq.nonEmpty
    seq.isEmpty

## В checkWorkability (CWA) используйте getVar вместо обращений к БД (ASelect, ATSQL, OQuery, Load, byParent, byKey и т.д.)

Так как CWA вызывается на большое количество событий, выполняемые в нем запросы к БД могут существенно замедлить работу интерфейса пользователя. Вычисления дополнительных полей для CWA можно осуществлять в onRefreshExt, selectStatement или на события при открытии карточки (onLoadMeta, beforeOpen, afterOpen и т.д.). Пример:

    //До
    override def checkWorkability(): Unit = {
      super.checkWorkability()
      if (thisApi.load(getVar("id").asNLong).get(_.idStateMC) >= 300.nn)
        opers("delete").isEnabled = false
    }

    //После
    override def checkWorkability(): Unit = {
      super.checkWorkability()
      if (getSelfVar("idStateMC").asNNumber >= 300.nn)
        opers("delete").isEnabled = false

    }

## Избегайте использовать copyAro

При использовании copyAro все данные из rop копируются, что увеличивает объем потребляемой оперативной памяти на сервере и может привести к ошибке потребляемой памяти в сессии. Пример:

    // До
    val rvWorkDoc = Mct_DocumentApi().loadByGid(rop.get(_.gidMctDocument)).copyAro()
    val idvPrj = rvWorkDoc.idPrj
    val idvPrjVer = rvWorkDoc.idPrjVer
    // После
    val ropWorkDoc = Mct_DocumentApi().loadByGid(rop.get(_.gidMctDocument))
    val idvPrj = ropWorkDoc.get(_.idPrj)
    val idvPrjVer = ropWorkDoc.get(_.idPrjVer)

## В Avi пишите только бизнес логику для взаимодействия с пользователем, все остальное выносите в методы Api

Пример:

    // До
    override protected def onInsertItem(): Unit = {
      super.onInsertItem()
      if (getSelfVar("idCandidat#").asNLong.isNotNull) thisApi().setidCandidate(thisRop(), getSelfVar("idCandidat#").asNLong)
      if (getSelfVar("idVacancy#").asNLong.isNotNull) thisApi().setidVacancy(thisRop(), getSelfVar("idVacancy#").asNLong)
      if (getSelfVar("nSalaryProbAcc#").asNNumber.isNotNull) thisApi().setnSalaryProb(thisRop(), getSelfVar("nSalaryProbAcc#").asNNumber)
      if (getSelfVar("nProbationAcc#").asNNumber.isNotNull) thisApi().setnProbation(thisRop(), getSelfVar("nProbationAcc#").asNNumber)
      if (getSelfVar("nSalaryAcc#").asNNumber.isNotNull) thisApi().setnSalary(thisRop(), getSelfVar("nSalaryAcc#").asNNumber)
      if (getSelfVar("idCurAcc#").asNLong.isNotNull) thisApi().setidCur(thisRop(), getSelfVar("idCurAcc#").asNLong)
      if (getSelfVar("idEmployment#").asNLong.isNotNull) thisApi().setidEmployment(thisRop(), getSelfVar("idEmployment#").asNLong)
      if (getSelfVar("idWorkGraph#").asNLong.isNotNull) thisApi().setidWorkGraph(thisRop(), getSelfVar("idWorkGraph#").asNLong)
      if (getSelfVar("idWorkMode#").asNLong.isNotNull) thisApi().setidWorkMode(thisRop(), getSelfVar("idWorkMode#").asNLong)
      if (getSelfVar("idMentor#").asNLong.isNotNull) thisApi().setidCurator(thisRop(), getSelfVar("idMentor#").asNLong)
      if (getSelfVar("idHREmpl#").asNLong.isNotNull) thisApi().setidHREmpl(thisRop(), getSelfVar("idHREmpl#").asNLong)
      if (getSelfVar("dPlan#").asNDate.isNotNull) thisApi().setdPlan(thisRop(), getSelfVar("dPlan#").asNDate)
    }
    // После
    override protected def onInsertItem(): Unit = {
      super.onInsertItem()
      if (getSelfVar("idOffer#").notNull())
        thisApi().fillByOffer(thisRop(), getSelfVar("idOffer#").asNLong)
    }

## Не пишите длинные выражения и запросы в одну строчку

Длинные выражения в одну строку затрудняют чтение кода и отнимает много времени на понимание Пример:

    // До
    Stk_InternalWarrantDetApi().byParent(rop).withFilter(f => (gidapDet.contains(f.gid) || gidapDet.isEmpty)).foreach { ropDet =>
        ???
      }

    // После
    Stk_InternalWarrantDetApi().byParent(rop)
      .withFilter(f => (gidapDet.contains(f.gid) || gidapDet.isEmpty))
      .foreach { ropDet =>
         ???
      }

Пример:

    -- До
    select gidObj, nPercent, nQty from Bs_CostDistr d where d.gidSrc = :gid
    -- После
    select d.gidObj
           ,d.nPercent
           ,d.nQty
    from   Bs_CostDistr d
    where  d.gidSrc = :gid

## Создавайте общие trait без реализации для разных отображений с одинаковой функциональностью

Чтобы избежать дублирование кода и упростить дальнейшую поддержку
Пример:

    trait List_gidDocGeneral extends Default with super.List {
      ???
    }

    trait List_gidDocTax extends List_gidDocGeneral {
      override def idAccKind: NLong = Bs_AccKindApi().idTax
    }

    trait List_gidDocMng extends List_gidDocGeneral {
      override def idAccKind: NLong = Bs_AccKindApi().idMng
    }

    trait List_gidDocAit extends List_gidDocGeneral {
      override def idAccKind: NLong = Bs_AccKindApi().idAit
    }

## Создавайте библиотеки для использования общих методов в различных выборках

Чтобы избежать дублирование кода и упростить дальнейшую поддержку
Пример:

    Pm_Lib
    Btk_FileLib

## Не используйте поля Tuple, а по возможности пользуйтесь экстрактором

Пример:

      val t = ("Vlad", 23, "M")
    //До
    setName(t._1)
    setAge(t._2)
    setSex(t._3)

    //После
    val (name, age, sex) = t
    setName(name)
    setAge(age)
    setSex(sex)

    Seq(t)
      .foreach { case (name, age, sex) =>
        setName(name)
        setAge(age)
        setSex(sex)
      }

## Используйте «.lastOption, .headOption, dropRight(1), .drop(1), .reduceOption» вместо «.last, .head, .init, .tail, .reduce»

Использование `.last, .head, .init, .tail, .reduce` может приводит к исключению в случае если коллекция пустая Пример:

    val unknownSizeList: List[_] = ???
    //До
    val sum = unknownSizeList.reduce(_ + _) // есть шанс ошибки
    //После
    val sum = unknownSizeList.reduceOption(_ + _).nn // мы получим NNumber(null) в случае если список пустой

## Не складывайте строки через +, используйте строковую интерполяцию

Пример:

    val hello = "hello"
    val world = "world"
    //До
    val str = hello + " " + world

    //После
    val str = s"$hello $world"

## При сборе сложной строки не создавайте много промежуточных результатов

Пример:

    val ropa: List[???] = ???
    //До
    var str = "Names: "

    val result =
      for (rop <- ropa) yield {
        str += rop.sName + ";" // каждый раз создаётся новая строка
        load(rop.idSmth)
      }

    str += " end"
    //После
    val stringBuilder =
      new StringBuilder()
        .append("Names: ")
    val result =
      for (rop <- ropa) yield {
        stringBuilder
          .append(rop.sName)
          .append(";")
        load(rop.idSmth)
      }

    val names = stringBuilder.append(" end").toString()
    // или если есть возможность сформируйте список
    val (result, names) = ropa.map { rop =>
      (load(rop.idSmth), rop.sName)
    }.unzip

    val str =  names.mkString("Names: ", ";", " end")

## Используйте Option при работе с пустыми (null) ссылками

Пример:

    val rop = ???
    val oldJavaLib = ???
    //До
    val ropSmth = load(rop.idSmth) // забыли обработать null

    var result = _
    val thing = oldJavaLib.getThing
    if(thing != null) {
      val smth = thing.getSmth
      if(smth != null) {
        try {
          result = smth.doWork()
        } catch {
          case _: Exception => {}
        }
      }
    }
    if(result == null) throw AppException("Не получилось")
    //После
    val ropSmth = get(rop.idSmth)
      .getOrElse(throw AppException("Чего то нету"))

    val result = (for {
      thing <- Option(oldJavaLib.getThing)
      smth <- Option(thing.getSmth)
      result <- Try(smth.doWork()).toOption
    } yield result).getOrElse(throw AppException("Не получилось"))

## Избегайте выполнения запросов внутри вложенных циклов

Пример:

      case class TaskDoc(/* атрибуты */, aDocDet: CSeq[TaskDocDet])

      case class TaskDocDet(/* атрибуты */, aFile: CSeq[TaskDocDetFile])

      case class TaskDocDetFile(/* атрибуты */)

    //до:
        def getTasksData(dpFrom: NDate, dpTo: NDate): CSeq[Task] = {
          val avResult = mutable.ArrayBuffer.empty[TaskDoc]

          for (rvx <- new ASelect {
            // документы Task
          }) {
            // ...

            for (rvxDet <- new ASelect {
              // позиции Task
            }) {
              // ...

              for (rvxDetFile <- new ASelect {
                // файлы позиций Task
              }) {
                // ...
              }
            }
          }

          avResult
        }

    //после:
        def getTasksData(dpFrom: NDate, dpTo: NDate): CSeq[Task] = {
          val avResult = mutable.ArrayBuffer.empty[TaskDoc]

          val avIdToTask = new ASelect {
            val id = asNLong("id")
            // остальные атрибуты
          }.map(rvx => rvx.id() -> TaskDoc(/* ... */)).toMap

          val avIdDocAndTaskDets = new ASelect {
            SQL"""
              select t.idDoc
                    ,t.id
                    ,t.nRow
                  -- ..
                from Md_TaskDet t
               where t.idDoc in (select unnest(${LongPgArray(avIdToTask.keys)}) )
              """
          }.map { rvx =>
            (rvx.idDoc(), rvx.id(), TaskDocDet(/* ... */))
          }.toSeq

          val gidavTaskDet = avIdDocAndTaskDets.map(vEntry => Md_TaskDetApi().getGid(vEntry._2))
          val avIdTaskDetAndFiles = new ASelect {
            SQL"""
              --
              where t.gidSrc in (select unnest(${GidPgArray(gidavTaskDet)}) )
              """
          }.map { rvx =>
            (rvx.id(), TaskDocDetFile(/* ... */))
          }

          // агрегация с циклом по Task, поиском TaskDet, подбором File для Det

          avResult
        }

Пример с поисковым ключом:

    case class objRem(
                             idGds: NLong
                             , idStock: NLong
                             , gidSlitAccount: NGid
                             , gidSTKAcc: NGid
                             , gidMaster: NGid
                             , gidSuite: NGid
                             , idCons: NLong
                             , var nQtyBase: NNumber
                           )
          val avRem = collection.mutable.Map[NString, ArrayBuffer[objRem]]()
          for (rv <- new ASelect {
            val idGds = asNLong("idGds")
            val idCons = asNLong("idCons")
            val idStock = asNLong("idStock")
            val gidMaster = asNGid("gidMaster")
            val gidSTKAcc = asNGid("gidSTKAcc")
            val gidSuite = asNGid("gidSuite")
            val gidSlitAccount = asNGid("gidSlitAccount")
            val nQtyBaseMsr = asNNumber("nQtyBaseMsr")
            SQL"""
        SELECT sum(-r.nQtyBaseMsr) nQtyBaseMsr
               ,r.idGds
               ,r.idStock
               ,r.gidMaster
               ,r.gidSTKAcc
               ,r.gidSuite
               ,r.gidSlitAccount
               ,r.idCons
        from   (select dr.idgds
                      ,dr.gidMaster
                      ,dr.gidObjNeed
                      ,sum(dr.nQtyBase) nQtyBase
                from (
                    select *
                    from unnest(${idavGds}
                           ,${gidavMaster}
                           ,${gidaObjNeed}
                           ,${naQtyBase}
                           ) as x(idGds, gidMaster, gidObjNeed, nQtyBase)
                     ) dr
                group by dr.idgds
                      ,dr.gidmaster
                      ,dr.gidObjNeed
               ) d
        join   Stk_RegistryGdsMov r on r.idGds = d.idGds and r.gidSlitAccount = d.gidObjNeed and r.gidMaster = d.gidMaster
        where  r.idDepOwner = $idpDepOwner
        and    r.sType = 'res'
        and    r.idStock in (select st.idchild
                             from   stk_stocktree st
                             where  st.idparent = $idpStock)
        group by r.idGds
               ,r.idStock
               ,r.gidMaster
               ,r.gidSTKAcc
               ,r.gidSuite
               ,r.gidSlitAccount
               ,r.idCons
        having sum(-r.nQtyBaseMsr) > 0"""
          }) {
            val sKey = rv.idGds() + "_" + rv.gidMaster().nvl("0".ng) + "_" + rv.gidSlitAccount().nvl("0".ng)
            avRem.getOrElseUpdate(sKey, ArrayBuffer[objRem]()) += objRem(
              idGds = rv.idGds()
              , idStock = rv.idStock()
              , gidSlitAccount = rv.gidSlitAccount()
              , gidSTKAcc = rv.gidSTKAcc()
              , gidMaster = rv.gidMaster()
              , gidSuite = rv.gidSuite()
              , nQtyBase = rv.nQtyBaseMsr()
              , idCons = rv.idCons()
            )
          }

          apObjNeed.foreach { vObjNeed =>
            val sKey = vObjNeed.idGds + "_" + vObjNeed.gidMaster.nvl("0".ng) + "_" + vObjNeed.gidObjNeed.nvl("0".ng)
            if (avRem.contains(sKey)) {
              avRem(sKey).filter(f => f.nQtyBase > 0.nn).foreach { rem =>
                lazy val ropGds = Bs_GoodsApi().load(vObjNeed.idGds)

                val nvQtyBase = vObjNeed.nQtyBase.min(rem.nQtyBase)
                avOperationData += OperationData(
                  gidParent = vObjNeed.gidDet
                  , gidDet = vObjNeed.gidDet
                  , gidSrcObject = vObjNeed.gidSrcObject
                  , idGds = vObjNeed.idGds
                  , idCons = rem.idCons
                  , gidSuite = rem.gidSuite
                  , idStock = rem.idStock
                  , gidMaster = rem.gidMaster
                  , gidSTKAcc = rem.gidSTKAcc
                  , gidSlitAccountRes = rem.gidSlitAccount
                  , nQtyBase = nvQtyBase
                  , nQtyBase2 = {
                    if (vObjNeed.nConvertBaseToBase2 > 0.nn) {
                      (nvQtyBase / vObjNeed.nConvertBaseToBase2).round(ropGds.get(_.nRoundPlaces2))
                    } else None.nn
                  }
                  , idReplacedGds = vObjNeed.idReplacedGds
                  , nRateReplacedGds = vObjNeed.nConvertReplacedGds
                  , nPrimeCostSum = (nvQtyBase * vObjNeed.nPrimeCostPrc).round(2.nn)
                  , nPrimeCostSumTax = (nvQtyBase * vObjNeed.nPrimeCostPrcTax).round(2.nn)
                  , nPrimeCostSumMng = (nvQtyBase * vObjNeed.nPrimeCostPrcMng).round(2.nn)
                  , dDateTime = dpExec
                  , idAddCondGenOper = Stk_AddCondGenOperApi().idvStornResByDetNeed
                )
                rem.nQtyBase -= nvQtyBase
                vObjNeed.nQtyBase -= nvQtyBase
              }
            }

            if (vObjNeed.nQtyBase > 0.nn && params.bValidateRemStorno) {
              throw AppException(s"""По ТМЦ ${Bs_GoodsApi().getMnemoCode(vObjNeed.idGds)} ${Bs_GoodsApi().getHeadLine(vObjNeed.idGds)} нет достаточного количества для сторнирования резерва!""")
            }

          }

## Используйте параллельные вычисления, если требуется множественная обработка независимых объектов

Руководство разработчика: Параллельные вычисления.