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
  1. 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 основан на связанном списке.

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

Пример:

// До
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, если берете меньше половины атрибутов из rop#

При использовании 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)} нет достаточного количества для сторнирования резерва!""")
        }

      }

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

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

    val threadCount = Asf_SettingsApi().getThreadCount(idDepOwner) //кол-во параллельных сессий
    val batchSize = Asf_SettingsApi().getBatchSize(idDepOwner) //кол-во объектов в пачке для обработки
    Parallel.withPool(threadCount) { pool =>
      val idaInvNumb = ArrayBuffer[NLong]()
      val idaObject = (for (rv <- new ASelect {
        SQL(sQuery)
        on(param.toArray: _*)
      }) yield (rv.get("id").asNLong())).toVector
      val nCount = idaObject.length
      var nExec = 0
      f(nExec, nCount) //анонимная функция для показа счетчика обработки в интерфейсе
      idaObject.foreach{id => 
        idaInvNumb += id
        if (idaInvNumb.length > batchSize) {
          pool.submit(InputData(idaInvNumb.toList, dPeriod, gid, idDepOwner, isAcc, isTax, isMng)) { implicit session =>
            arg =>
              Asf_DeprCalcTaskApi().doPack(arg.idaInvNumb, arg.dPeriod, arg.gidDoc, arg.idDepOwner, arg.isAcc, arg.isTax, arg.isMng)
          } onSuccess { r =>
            nExec += idaInvNumb.length
            f(nExec, nCount)
          } onFailure { r =>
            r.raise()
          }
          idaInvNumb.clear()
        }
      }
      if (idaInvNumb.nonEmpty)
        Asf_DeprCalcTaskApi().doPack(idaInvNumb.toList, dPeriod, gid, idDepOwner, isAcc, isTax, isMng)
    }