LINUX.ORG.RU

проектирование приложений с использованием scala slick

 ,


0

6

есть несколько вопросов, которые вылезли практически сразу, не успев донести и первый мелкий проект до продакшена. может, кто-то поможет победить, а то скала нравится очень, но пока что есть боль.

вопросы на примере кода

package dao

import javax.inject.Inject

import model.FullKeyword
import play.api.db.slick.DatabaseConfigProvider
import slick.jdbc.JdbcProfile

import scala.concurrent.ExecutionContext

class DAO @Inject() (dbConfigProvider: DatabaseConfigProvider)(implicit ec: ExecutionContext) {
  private val dbConfig = dbConfigProvider.get[JdbcProfile]
  import dbConfig._
  import profile.api._
  import tables._

  def keywordById(id: Int) = db.run(keywords.filter(_.id === id).result).map(_.headOption)

  def linksByKeywordId(keywordId: Int) = db.run(links.filter(_.keywordId === keywordId).result)

  def imagesByKeywordId(keywordId: Int) = db.run(images.filter(_.keywordId === keywordId).result)

  def relatedByKeywordId(keywordId: Int) = db.run(relatedQueries.filter(_.keywordId === keywordId).result)

  def fullKeywordById(id: Int) = keywordById(id).map(_.map(keyword => {
    for {
      links <- linksByKeywordId(keyword.id)
      related <- relatedByKeywordId(keyword.id)
      images <- imagesByKeywordId(keyword.id)
    } yield(FullKeyword(keyword = keyword,links = links,related = related,images = images))
  }))

  def countKeywords = db.run(keywords.size.result)

  def keywordsNPart(n: Int,size: Int) = {
    val fromId = n*size + 1
    val toId = fromId + size
    db.run(keywords.filter(e => e.id >= fromId && e.id < toId).sortBy(_.id.asc).result)
  }
}
боль по порядку:

  1. class DAO @Inject() (dbConfigProvider: DatabaseConfigProvider)(implicit ec: ExecutionContext) {
      private val dbConfig = dbConfigProvider.get[JdbcProfile]
    

    использую slick 3.2 и это в нем уже диприкейтед. свежее экзамплов не нашел. собственно, как же сейчас?

  2.   private val dbConfig = dbConfigProvider.get[JdbcProfile]
    
    везде в экзамплах делают именно это: импортируют провайдер и вызывают у него .get[A]. я вижу здесь только противоречие IoC и ничего больше. в чем я не прав? не используется ни инициализация по требованию, никуда ниже компонентом провайдер тоже не передается, ну то есть нет в чем разумном их заподозрить.
  3. сам апи выглядит довольно низкоуровневым:
    • везде пишешь эти db.run(query.filter(...).result), когда кажется нормальным что-то вроде query.filter(...).execute
    • импорт implicit из переменной dbConfig.profle.api._ `===` выглядит как копипаста, если писать много всяких DAO. это вообще нормально? выглядит как какой-то странный полиморфизм
    в целом, это все склеить как-то хочется.
  4.   def linksByKeywordId(keywordId: Int) = db.run(links.filter(_.keywordId === keywordId).result)
    
      def imagesByKeywordId(keywordId: Int) = db.run(images.filter(_.keywordId === keywordId).result)
    
      def relatedByKeywordId(keywordId: Int) = db.run(relatedQueries.filter(_.keywordId === keywordId).result)
    
    вообще почти одинаковых три метода. как это сократить до def itemByKeywordId(item: TableQuery[?],keywordId: Int) и что за типы тут будут и где интерфейс для keywordId заимплиментить?
  5.   def fullKeywordById(id: Int) = keywordById(id).map(_.map(keyword => {
        for {
          links <- linksByKeywordId(keyword.id)
          related <- relatedByKeywordId(keyword.id)
          images <- imagesByKeywordId(keyword.id)
        } yield(FullKeyword(keyword = keyword,links = links,related = related,images = images))
      }))
    
    тут вообще похоже на какой-то жаваскрипт получается. Future[Option[Future]] как сократить до Option[Future] ?

    и, как я понимаю, именно здесь и понадобился нам ExecutionContext, и мы завернули в кучу Future код, который оказался блокирующим. или зачем тут ExecutionContext ?

да и в целом класс получился как неймспес для методов. ООП тут точно нет. где тогда ФП и, конечно, где от него кайф?

По поводу 5 вопроса, можно сократить до Future[Option[]], переписав fullKeywordById

import scala.concurrent.duration.Duration
import scala.concurrent.{Await, ExecutionContext, Future}

case class Keyword(id: Int, keyword: String)
case class Link(id: Int, keywordId: Int, value: String)
case class Image(id: Int, keywordId: Int)
case class FullKeyword(id: Int, keyword: String, links: Seq[Link], images: Seq[Image])


object Application{
  implicit val ec = ExecutionContext.global

  def imagesByKeyword(keywordId: Int) : Future[Seq[Image]] = Future.successful(
    1.to(3).map(id=>Image(id, keywordId))
  )

  def linksByKeyword(keywordId: Int) : Future[Seq[Link]] = Future.successful(
    1.to(5).map(id=>Link(id, keywordId, "Link"+id))
  )

  def keywordById(id: Int) : Future[Option[Keyword]] = Future.successful(Some(Keyword(id, "kw")))

  def fullKeywordById(id: Int) : Future[Option[FullKeyword]] = keywordById(id).flatMap({
    case None => Future.successful(None)
    case Some(keyword)=> for {
      links <- linksByKeyword(keyword.id)
      images <- imagesByKeyword(keyword.id)
    } yield Some(FullKeyword(id, keyword.keyword, links, images))
  })

  def main(args: Array[String]): Unit = {
    println(Await.result(fullKeywordById(42), Duration.Inf))
  }
}

userd ()

Ну наконец-то хоть где-то моя игрушечная писанина кому-то пригодится.

Тыц https://github.com/fancywriter/car-adverts/blob/master/app/dao/CarAdvertsDao....

Тыц2 https://github.com/fancywriter/car-adverts/blob/master/app/loader/Loader.scala

везде пишешь эти db.run(query.filter(...).result), когда кажется нормальным что-то вроде query.filter(...).execute

В slick2 примерно так и было, но slick3 переписали на DBIO, чтобы каждая операция была асинхронной, поэтому запрос мало создать, надо его обернуть в db.run()...

тут вообще похоже на какой-то жаваскрипт получается. Future[Option[Future]] как сократить до Option[Future] ?

Можете обмазаться трансформерами монад, если хотите. Станет ли лучше, не уверен ;) http://typelevel.org/cats/datatypes/optiont.html

и, как я понимаю, именно здесь и понадобился нам ExecutionContext, и мы завернули в кучу Future код, который оказался блокирующим. или зачем тут ExecutionContext ?

Где вы тут увидели блокирующий код? Его нет, он весь целиком в dbio от slick. Почитайте http://docs.scala-lang.org/overviews/core/futures.html

Вообще for-comprehension - сахар на flatMap/withFilter/map/foreach. Попробуйте поэтапно переписать всё это через цепочку flatMap - в качестве упражнения, естественно. Выглядеть будет хуже и страшнее, зато вам будет понятнее, что там на самом деле происходит.

Future.map/flatMap требует ExecutionContext - в нём будут выполняться операции над результатом, который future (когда-нибудь) вернёт. Вот для этого нужен. Надеюсь, понятно объяснил...

P.S. Ещё хорошим стилем считается писать тип возвращаемого значения для public def/val. Хоть компилятор его потом и выведет, зато с типами читать легче. Да и где-то читал, что компилируется быстрее, типы выводить не надо (не замерял, не уверен).

BattleCoder ★★★★★ ()
Последнее исправление: BattleCoder (всего исправлений: 1)

Future[Option[Future]] как сократить до Option[Future]

Не подождав выполнения наружнего Future — никак, ведь ты заранее не знаешь, Some он вернёт или None

Softwayer ★★ ()

или зачем тут ExecutionContext ?

Slick ничего не выполняет в твоем потоке. Все обращения к БД происходят в его собственных потоках. ExecutionContext нужен для выполнения функций которые ты передаешь в map/flatMap. В потоках для БД они не выполняются чтобы случайно не занять их все твоей бизнес-логикой.

Future[Option[Future]] как сократить до Option[Future]

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

Правильный вопрос это как сократить это до Future[Option], так как Option никак нельзя получить до обращения к БД.

Тут решение такое - если Option всего один, то проще всего что-то такое:

def fullKeywordById(id: Int) = keywordById(id).flatMap(_.map(keyword => {
    for {
      links <- linksByKeywordId(keyword.id)
      related <- relatedByKeywordId(keyword.id)
      images <- imagesByKeywordId(keyword.id)
    } yield(Some(FullKeyword(keyword = keyword,links = links,related = related,images = images)))
  }).getOrElse(Future.successful(None))

Если в API много Future[Option], то можно использовать трансформер OptionT.

maxcom ★★★★★ ()
Ответ на: комментарий от userd


 def fullKeywordById(id: Int) : Future[Option[FullKeyword]] = keywordById(id).flatMap({
    case None => Future.successful(None)
    case Some(keyword)=> for {
      links <- linksByKeyword(keyword.id)
      images <- imagesByKeyword(keyword.id)
    } yield Some(FullKeyword(id, keyword.keyword, links, images))
  })

выглядит как бойлерплейт, т.е. именно такой код везде писать придеться. нету ничего готового?

yetanotherguy ()
Ответ на: комментарий от BattleCoder

Тыц https://github.com/fancywriter/car-adverts/blob/master/app/dao/CarAdvertsDao....

Тыц2 https://github.com/fancywriter/car-adverts/blob/master/app/loader/Loader.scala

не совсем понятно к чему этот код.

В slick2 примерно так и было, но slick3 переписали на DBIO, чтобы каждая операция была асинхронной, поэтому запрос мало создать, надо его обернуть в db.run()...

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

yetanotherguy ()
Ответ на: комментарий от maxcom

Slick ничего не выполняет в твоем потоке. Все обращения к БД происходят в его собственных потоках. ExecutionContext нужен для выполнения функций которые ты передаешь в map/flatMap. В потоках для БД они не выполняются чтобы случайно не занять их все твоей бизнес-логикой.

не помню сигнатур, но выходит ExecutionContext разный может быть и он не переиспульзуется где-то там? как-то мне казалось, что смысл всего этого в том что ты не решаешь где там и что выполняется, твоя задача типа дать неблокирующий код и все

Правильный вопрос это как сократить это до Future[Option], так как Option никак нельзя получить до обращения к БД.

ну да, я тут это и имел ввиду

yetanotherguy ()
Ответ на: комментарий от BattleCoder

ну я под тем чтобы все как-то сократить до query.filter.execute не имеел ввиду отказаться от чего-либо, а хотел узнать как это либо привести к этой форме, либо может через какой-то другой апи заюзать, который я не знаю.

yetanotherguy ()
Ответ на: комментарий от yetanotherguy

выходит ExecutionContext разный может быть

JDBC бывает только блокирующий. Slick выполяет его в своих потоках, а тебе предоставляет неблокирующий API.

Интерпретатор Slick не выполняет твоей код в своих потоках. Если у тебя там весь код не блокирующий, то ты можешь завести себе один пул потоков на приложение. Если блокирующий, то тут уже есть варианты.

maxcom ★★★★★ ()
Ответ на: комментарий от yetanotherguy

Я посмотрел еще на твой код и думаю что ты зря делаешь db.run в каждой функции. Лучше возвращать DBIO, а run вызывать только в fullKeywordById. Это позволит, при желании, соеденить запросы в одно транзакцию. DBIO, как и Future, прекрасно работает в for.

maxcom ★★★★★ ()
Последнее исправление: maxcom (всего исправлений: 2)
Ответ на: комментарий от maxcom

Интерпретатор Slick не выполняет твоей код в своих потоках. Если у тебя там весь код не блокирующий, то ты можешь завести себе один пул потоков на приложение. Если блокирующий, то тут уже есть варианты.

вроде бы понял. они гарантировали себе что их короткий пул не забьется блокировками, выходит так?

Я посмотрел еще на твой код и думаю что ты зря делаешь db.run в каждой функции. Лучше возвращать DBIO, а run вызывать только в fullKeywordById. Это позволит, при желании, соеденить запросы в одно транзакцию. DBIO, как и Future прекрасно работает в for.

вот, что-то интересное. а можно еще с примером, если не сложно?

yetanotherguy ()
Ответ на: комментарий от yetanotherguy

Как-то так (не проверял):

  def keywordById(id: Int) = keywords.filter(_.id === id).result.headOption

  def linksByKeywordId(keywordId: Int) = links.filter(_.keywordId === keywordId).result

  def imagesByKeywordId(keywordId: Int) = images.filter(_.keywordId === keywordId).result

  def relatedByKeywordId(keywordId: Int) = relatedQueries.filter(_.keywordId === keywordId).result

  def fullKeywordById(id: Int) = db.run(keywordById(id).flatMap(_.map(keyword => {
    for {
      links <- linksByKeywordId(keyword.id)
      related <- relatedByKeywordId(keyword.id)
      images <- imagesByKeywordId(keyword.id)
    } yield(FullKeyword(keyword = keyword,links = links,related = related,images = images))
  }).getOrElse(DBIO.successful(None)).transactionally)

maxcom ★★★★★ ()
Ответ на: комментарий от maxcom

вот, очень интересное замечание! спасибо! еще один глупый вопрос: у db есть close когда его дергать вообще надо? ну и может все-таки есть что сказать по поводу остальных вопросов?

я вообще заметил, что в комьюнити у них часто ломают апи, может по этому они их делают слегка своебразными?

yetanotherguy ()
Ответ на: комментарий от yetanotherguy

Через ExecutionContext ты как раз во многих случаях можешь сам решать, где и что будет исполняться. Но в простых случаях используют один единственный глобальный, импортируя его через implcits, и особо не парятся, даже не задумываясь об этом. Однако, если дело дойдет до akka http, то там очень пригодится.

А вложенные вычисления элементарно сводятся к более простым через x.flatMap(m => m). Эта штука известна под личинами join, concat, flatten и еще как-то в теории категорий

dave ★★★★★ ()
Ответ на: комментарий от BattleCoder

По P.S. подтверждаю. Сейчас не могу найти пулл-реквест, но было, кажется, в гитхабе скала-плагина идеи, когда добавили в проект аннотации типов и билд ускорился чуть ли не в разы. Ну и вообще, со сликом компиляция - это боль, а идея помирает на 5 таблицах по 10 полей. Было полгода назад, может, уже лучше.

cdshines ★★★★ ()

никуда ниже компонентом провайдер тоже не передается, ну то есть нет в чем разумном их заподозрить.

Как не передается, если ты делаешь импорт его содержимого?

везде пишешь эти db.run(query.filter(...).result), когда кажется нормальным что-то вроде query.filter(...).execute

Сделай имплицитный execute, в чем проблема?

вообще почти одинаковых три метода. как это сократить до def itemByKeywordId(item: TableQuery[?],keywordId: Int) и что за типы тут будут и где интерфейс для keywordId заимплиментить?

В классах для таблиц.

тут вообще похоже на какой-то жаваскрипт получается. Future[Option[Future]] как сократить до Option[Future] ?

засунуть keywordById внутрь for

anonymous ()
Ответ на: комментарий от cdshines

Добавьте памяти. По умолчанию идея идёт с неприлично маленьким для 2017 года (и для скалы) максимальным размером heap.

$ cat /opt/idea-ultimate-2017.2/bin/idea64.vmoptions 
-Xms512m
-Xmx2g
-XX:+CMSClassUnloadingEnabled
-XX:ReservedCodeCacheSize=240m
-XX:+UseConcMarkSweepGC
-XX:SoftRefLRUPolicyMSPerMB=50
-ea
-Dsun.io.useCanonCaches=false
-Djava.net.preferIPv4Stack=true
-XX:+HeapDumpOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow
-Dawt.useSystemAAFontSettings=lcd
-Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine

Я тут два гигабайта поставил (по опыту, может и больше сожрать), по умолчанию что ли 700 мегабайт - безбожно тормозит. Ну то есть конечно, это scala компилятор на самом деле прожорливый, да и идея сама, надо ПО оптимизировать, а не памяти добавлять, но первое не в наших руках, а второе в наших.

BattleCoder ★★★★★ ()
Последнее исправление: BattleCoder (всего исправлений: 3)
Ответ на: комментарий от BattleCoder

Не помогало, я и 4 давал. И пробовал ограничить глубину индукции имплиситов, и все остальное - ну просто пока идея не может так. То же самое с акка-хттп, кстати, если позволить себе иметь слишком развесистые роуты с логикой в куче, идея помирает.

cdshines ★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.