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。因为需要一个ActorContext
,InjectedActorSupport
只能被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)
}
我有一个使用 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。因为需要一个ActorContext
,InjectedActorSupport
只能被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)
}