1

我们使用两个数据库:主数据库和副本数据库。

用例:

  • 我们希望提供一个可用于运行 DBIOAction (s) 的对象,并能够根据平滑效果推断要使用哪个数据库。读取 => 副本。写入 => 主要。

  • 我还希望允许程序员将主数据库固定为读取,而不是复制到写入。如果有人试图将副本固定到读取操作,我想得到一个编译错误。

我有以下代码:

import scala.concurrent.Future
import slick.jdbc.MySQLProfile.api._
import Effect._
import scala.annotation.implicitNotFound

object DatabaseModule {
  // Used to create a constraint on what Role can an effect have.
  trait Role[E <: Effect]
  type ReplicaRole = Role[Read]
  type PrimaryRole = Role[Write] with ReplicaRole

  // database configuration depends on the role.
  sealed trait DatabaseConfiguration[R <: Role[_]] {
    def createDatabase(): Database
  }

  object DatabaseConfiguration {
    object Primary extends DatabaseConfiguration[PrimaryRole] {
      def createDatabase(): Database = Database.forConfig("slick.mysql.write")
    }
    object Replica extends DatabaseConfiguration[ReplicaRole] {
      def createDatabase(): Database = Database.forConfig("slick.mysql.read")
    }
  }

  class DB[R <: Role[_]](databaseConfiguration: DatabaseConfiguration[R]){
    val underlyingDatabase = databaseConfiguration.createDatabase()
  }

  object DB {
    // this error will be returned if the implicit is not found.
    @implicitNotFound("'${R}' database is not privileged to to perform effect '${E}'.")
    trait HasPrivilege[R <: Role[E], E <: Effect]

    // phantom types safe to assign null, used to enforce typing.
    implicit val replicaCanRead: ReplicaRole HasPrivilege Read = _
    implicit val primaryCanWrite: PrimaryRole HasPrivilege Write = _
    implicit val primaryCanRead: PrimaryRole HasPrivilege Read = _

    // primary and replica databases.
    implicit lazy val dbPrimary: DB[PrimaryRole] = new DB(DatabaseConfiguration.Primary)
    implicit lazy val dbReplica: DB[ReplicaRole] = new DB(DatabaseConfiguration.Replica)

    // this function should infer which configuration to use (primary for writes, replica for reads)
    def run[A, E <: Effect](a: DBIOAction[A, NoStream, E])(implicit defaultDb: DB[Role[E]], p: Role[E] HasPrivilege E)
    : Future[A] = defaultDb.underlyingDatabase.run(a)

    // If we want to pin to replica, use this as follow DB.run(dbioAction)(dbMaster)
    def run[A, E <: Effect](a: DBIOAction[A, NoStream, E])(db: Database)(implicit p: Role[E] HasPrivilege E)
    : Future[A] = db.run(a)
  }
}

但是这不起作用,当我尝试使用它时,我收到以下 sbt 错误

[error]   [A, E <: slick.jdbc.MySQLProfile.api.Effect](a: slick.jdbc.MySQLProfile.api.DBIOAction[A,slick.jdbc.MySQLProfile.api.NoStream,E])(db: slick.jdbc.MySQLProfile.api.Database)(implicit p: com.hautelook.support.db.mysql.DatabaseModule.DB.HasPrivilege[com.hautelook.support.db.mysql.DatabaseModule.Role[E],E])scala.concurrent.Future[A] <and>
[error]   [A, E <: slick.jdbc.MySQLProfile.api.Effect](a: slick.jdbc.MySQLProfile.api.DBIOAction[A,slick.jdbc.MySQLProfile.api.NoStream,E])(implicit defaultDb: com.hautelook.support.db.mysql.DatabaseModule.DB[com.hautelook.support.db.mysql.DatabaseModule.Role[E]], implicit p: com.hautelook.support.db.mysql.DatabaseModule.DB.HasPrivilege[com.hautelook.support.db.mysql.DatabaseModule.Role[E],E])scala.concurrent.Future[A]
[error]  cannot be applied to (slick.dbio.DBIOAction[Int,Any,slick.jdbc.MySQLProfile.api.Effect.Write])

任何人都知道如何正确解决这些类型?

4

1 回答 1

1

I'm not sure my answer is exactly what you want, but it seems to me close enough.

The immediate reasons of the compilation error is that the two run calls are indistinguishable to the compiler when called the way you want. The thing is that if you have two methods:

def foo(p1:Int):Int 
def foo(p1:Int)(p2:Int):Int

and a call

foo(1)

the compiler doesn't know if it is the full call of the foo#1 or a partial application of the foo#2. Both choices look totally valid to the compiler. And even if you fix that I don't think you can easily make your first run work.

The main idea in my solution is that for the first run signature, the one that selects the default DB, to work you need some implicit mapping from the Effect onto the DatabaseConfiguration. The second run signature, that just verifies if the DB can run such a query, is done pretty the same way as in your solution. Here is full code

object DatabaseModule {

  sealed trait DatabaseConfiguration {
    lazy val underlyingDatabase: Database = createDatabase()

    protected def createDatabase(): Database
  }

  object PrimaryDb extends DatabaseConfiguration {
    def createDatabase(): Database = Database.forConfig("slick.mysql.write")
  }

  object ReplicaDb extends DatabaseConfiguration {
    def createDatabase(): Database = Database.forConfig("slick.mysql.read")
  }

  @implicitNotFound("'No default mapping for to perform effect '${E}'.")
  sealed case class DefaultDbMapping[E <: Effect] private(dbConfig: DatabaseConfiguration)

  object DefaultDbMapping {
    implicit val replicaIsDefaultRead = DefaultDbMapping[Read](ReplicaDb)

    // write goes to the primary
    implicit val primaryIsDefaultWrite = DefaultDbMapping[Write](PrimaryDb)
    // everything except for Read goes to the primary
    // implicit def primaryIsDefaultForEverythingElse[E <: Effect] = DefaultDbMapping[E](PrimaryDb)
  }


  object DB {

    // this error will be returned if the implicit is not found.
    @implicitNotFound("'${D}' database is not privileged to to perform effect '${E}'.")
    trait HasPrivilege[D <: DatabaseConfiguration, E <: Effect]

    // phantom types safe to assign null, used to enforce typing.
    implicit val replicaCanRead: ReplicaDb.type HasPrivilege Read = null
    implicit val primaryCanWrite: PrimaryDb.type HasPrivilege Write = null
    implicit val primaryCanRead: PrimaryDb.type HasPrivilege Read = null

    // this function should infer which configuration to use (primary for writes, replica for reads)
    def run[A, E <: Effect](a: DBIOAction[A, NoStream, E])(implicit defaultDb: DefaultDbMapping[E]): Future[A] = defaultDb.dbConfig.underlyingDatabase.run(a)

    // You have some choices for the second run:
    // join 2 parameters so the compiler sees the difference between run(q) and run(q,db)
    def run[A, E <: Effect, D <: DatabaseConfiguration](a: DBIOAction[A, NoStream, E], db: D)(implicit p: D HasPrivilege E): Future[A] = db.underlyingDatabase.run(a)
    // put the DB as the first parameter
    def run[A, E <: Effect, D <: DatabaseConfiguration](db: D)(a: DBIOAction[A, NoStream, E])(implicit p: D HasPrivilege E): Future[A] = db.underlyingDatabase.run(a)
    // rename method to by explicitly different 
    def runOn[A, E <: Effect, D <: DatabaseConfiguration](db: D)(a: DBIOAction[A, NoStream, E])(implicit p: D HasPrivilege E): Future[A] = db.underlyingDatabase.run(a)
  }
}

At the end I provide a few alternatives on how to fix your compilation issue as well. Choose whichever you like.

One more choice is to move the second method run to the DatabaseConfiguration like so:

  sealed trait DatabaseConfiguration {
    lazy val underlyingDatabase: Database = createDatabase()

    protected def createDatabase(): Database

    import DB._
    def run[A, E <: Effect](a: DBIOAction[A, NoStream, E])(implicit p: this.type HasPrivilege E): Future[A] = underlyingDatabase.run(a)
  }

So you have a choice of 3 calls: DB.run(query), PrimaryDb.run(query) and ReplicaDb.run(query). In this case it might make sense to rename DB to something like DefaultDb so the calls look more natural.

于 2019-01-29T02:30:03.307 回答