# Scala код ## Используйте View или withFilter, чтоб избежать создания временных коллекций 1. View - это особый вид коллекции в Scala, который берет базовую коллекцию и лениво выполняет методы преобразования в этой коллекции. Мы можем преобразовать каждую коллекцию Scala в отложенное представление и обратно с помощью view метода. Вместо создания новой коллекции после каждой операции, они применяют сразу всю цепочку преобразований к каждому элементу оригинальной коллекции, благодаря чему цепочка преобразований выполняется за один проход. Преобразование виртуальной коллекции в реальную выполняется либо одним из преобразующих методов из серии to{Название коллекции}, либо методом force. Пример: ```scala // До 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. Пример: ```scala // До 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 несколько раз подряд Пример: ```scala // До seq.filter(p1).filter(p2) // После seq.filter(x => p1(x) && p2(x)) ``` ## Используйте lazy val Компилятор не сразу вычисляет связанное выражение отложенного значения val. Он вычисляет переменную только при первом обращении к ней. Пример: ```scala // До val idgForming = Btk_ClassStateApi().findByNameAndIdClass("Forming", idClass) // После lazy val idgForming = Btk_ClassStateApi().findByNameAndIdClass("Forming", idClass) ``` ## Используйте коллекции, имеющие оптимальную сложность поиска, взамен обладающих линейной при частом обращении за нахождением Пример: ```scala //до: 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 и рекурсии добавляйте защиту по кол-ву итераций Пример: ```scala // До 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 } ``` ## Не эмулируйте существующие методы работы с коллекциями Пример: ```scala // До 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](https://tobyhobson.com/posts/scala/collections-performance/) ## При проверке на существование - не прибегайте к фильтрации Пример: ```scala // До seq.filter(p).nonEmpty seq.filter(p).isEmpty // После seq.exists(p) !seq.exists(p) ``` ## При проверке на пустоту - не вычисляйте длину Пример: ```scala // До 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 и т.д.). Пример: ```scala //До 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 копируются, что увеличивает объем потребляемой оперативной памяти на сервере и может привести к ошибке потребляемой памяти в сессии. Пример: ```scala // До 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 Пример: ```scala // До 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) } ``` ## Не пишите длинные выражения и запросы в одну строчку Длинные выражения в одну строку затрудняют чтение кода и отнимает много времени на понимание Пример: ```scala // До 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 => ??? } ``` Пример: ```sql -- До 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 без реализации для разных отображений с одинаковой функциональностью Чтобы избежать дублирование кода и упростить дальнейшую поддержку Пример: ```scala 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 } ``` ## Создавайте библиотеки для использования общих методов в различных выборках Чтобы избежать дублирование кода и упростить дальнейшую поддержку Пример: ```scala Pm_Lib Btk_FileLib ``` ## Не используйте поля Tuple, а по возможности пользуйтесь экстрактором Пример: ```scala 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» может приводит к исключению в случае если коллекция пустая Пример: ```scala val unknownSizeList: List[_] = ??? //До val sum = unknownSizeList.reduce(_ + _) // есть шанс ошибки //После val sum = unknownSizeList.reduceOption(_ + _).nn // мы получим NNumber(null) в случае если список пустой ``` ## Не складывайте строки через +, используйте строковую интерполяция Пример: ```scala val hello = "hello" val world = "world" //До val str = hello + " " + world //После val str = s"$hello $world" ``` ## При сборе сложной строки не создавайте много промежуточных результатов Пример: ```scala 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) ссылками Пример: ```scala 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("Не получилось")) ``` ## Избегайте выполнения запросов внутри вложенных циклов Пример: ```scala 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 } ``` Пример с поисковым ключом: ```scala 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)} нет достаточного количества для сторнирования резерва!""") } } ``` ## Используйте параллельные вычисления, если требуется множественная обработка независимых объектов Задачи планируются в основном потоке, а выполняются в служебных потоках пула. Основным потоком является поток в котором был создан пул для планирования задач. Данные будут сразу доступны всем пользователям при каждом завершении открывшихся сессий. Пример: ```scala 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) } ```