Actor 中流畅的数据库访问

Slick database access in Actor

我有一个使用 SqLite 和 slick 的 play-scala 应用程序。我的表定义如下:

@Singleton
class DataSets @Inject()(protected val dbConfigProvider: DatabaseConfigProvider, keys: PublicKeys) extends DataSetsComponent
  with HasDatabaseConfigProvider[JdbcProfile] {
  import driver.api._

  val DataSets = TableQuery[DataSetsTable]

  def all = db.run(DataSets.sortBy { _.id }.result)
  ...
}

我的控制器通过 DI 访问:

@Singleton
class DataSetsController @Inject() (dataSets: DataSets, env: play.Environment) extends Controller {
...

如何在 Actor 中获取数据库句柄?

class TrainActor @Inject() (dataSets: DataSets) extends Actor {
...

当然不行,因为 Guice 找不到数据集 class。

编辑:澄清:我不想在控制器中使用 actor 进行数据库访问(通过询问),而是在请求后从控制器开始一些资源密集型计算,然后将它们存储在数据库中(异步)。

您可以将依赖项注入角色:

import com.google.inject.AbstractModule
import play.api.libs.concurrent.AkkaGuiceSupport

class MyModule extends AbstractModule with AkkaGuiceSupport {
  def configure = {
    bindActor[TrainActor]("injected-train-actor")
  }
}

之后只需将 actor 注入控制器:

class MyController @Inject()(@Named("injected-train-actor") trainActor: ActorRef) {

  def endpointTest = Action.async {
    for {
      items <- (trainActor ? FetchAll).mapTo[Seq[DataSetsTableRow]]
    } yield Ok(Json.toJson(items))
  }

}

而不是

@Singleton
class DataSets 

可以将其声明为一个简单的 Scala 对象,可以充当 DataSetsDAO

object DataSets

然后在 actor 中使用 DataSets.dbOperation 请记住,结果类型将是 Future,因此只需在 onComplete 中的 actor 中安排一条消息给自己避免任何副作用。

我现在找到了一种与 DI 集成的方法,紧跟 official documentation。因为需要一个ActorContextInjectedActorSupport只能被Actor继承。这意味着我必须创建一个除了实例化和启动新 "worker" Actor 之外什么都不做的 actor。也许有更简单的方法,但这个方法是正确的。

TrainActor.scala:

package actors

import javax.inject.Inject

import akka.actor._
import com.google.inject.assistedinject.Assisted
import models.{DataSet, Model, PublicKey}
import play.api.Logger
import tables.DataSets

import scala.concurrent.ExecutionContext.Implicits.global

object TrainActor {
  case object Start
  case class LoadData(d: DataSet, k: PublicKey)

  trait Factory {
    def apply(model: Model): Actor
  }
}

class TrainActor @Inject() (val dataSets: DataSets, @Assisted val model: Model) extends Actor {
  import TrainActor._

  def receive = {
    case Start =>
      dataSets.findWithKey(model.id.get)
      ...

TrainActorStarter.scala:

package actors

import javax.inject.Inject

import akka.actor.{Actor, ActorRef}
import models.Model
import play.api.libs.concurrent.InjectedActorSupport

object TrainActorStarter {
  case class StartTraining(model: Model)
}

/**
  * https://www.playframework.com/documentation/2.5.x/ScalaAkka#Dependency-injecting-actors
  * @param childFactory
  */
class TrainActorStarter @Inject() (childFactory: TrainActor.Factory) extends Actor with InjectedActorSupport {
  import TrainActorStarter._

  def receive = {
    case StartTraining(model: Model) =>
      val trainer: ActorRef = injectedChild(childFactory(model), s"train-model-model-${model.id.get}")
      trainer ! TrainActor.Start
  }
}

ActorModule.scala:

package actors

import com.google.inject.AbstractModule
import play.api.libs.concurrent.AkkaGuiceSupport

class ActorModule extends AbstractModule with AkkaGuiceSupport {
  def configure(): Unit = {
    bindActor[TrainActorStarter]("train-actor-starter")
    bindActorFactory[TrainActor, TrainActor.Factory]
  }
}

最后在控制器中:

package controllers

import javax.inject._

import actors.{TrainActorStarter, TrainCallbackActor}
import akka.actor.{ActorRef, ActorSystem, _}
import akka.stream.Materializer
...

@Singleton
class ModelsController @Inject() (implicit system: ActorSystem, materializer: Materializer, ..., @Named("train-actor-starter") trainActorStarter: ActorRef) extends Controller with InjectedActorSupport {

  def startTraining(model: Model): Unit = {
    if(model.id.isEmpty) return
    trainActorStarter ! TrainActorStarter.StartTraining(model)
  }