Scala/Slick:使用继承和混合来减少样板文件

Scala/Slick: using inheritance and mixins to reduce boilerplate

我是 scala/play/slick 新手,所以如果我问了愚蠢的问题,请不要生气。

问题来了。
我有几个巧妙的 table 定义,这里是其中之一:

import javax.inject.Inject

import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.db.NamedDatabase
import slick.driver.JdbcProfile

import scala.concurrent.Future

case class User(id: Int, login: String, password: String) extends Identifiable

class UserDAO @Inject()(@NamedDatabase protected val dbConfigProvider: DatabaseConfigProvider) extends HasDatabaseConfigProvider[JdbcProfile] {
  import driver.api._

  private val users = TableQuery[UsersTable]

  def all(): Future[Seq[User]] = db.run(users.result)
  def insert(dog: User): Future[Unit] = db.run(users += dog).map { _ => () }
  def delete(id: Int): Future[Int] = db.run(users.filter(_.id === id).delete)


  private class UsersTable(tag: Tag) extends Table[User](tag, "USER") {
    def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
    def email = column[String]("email")
    def password = column[String]("password")
    def * = (id, email, password) <> (User.tupled, User.unapply)
  }
}  

假设我有更多的 tables 有 def id = column[Int]("id", O.PrimaryKey, O.AutoInc) 来消除这个我需要写这样的东西:

trait Identifiable {
  this: Table[_] =>
  def id = column[String]("id", O.PrimaryKey)
}

但是我如何以与数据库无关的方式在此处导入 Table?此外,还有更多的改进空间:所有提供对可识别 Tables 访问的 DAO 对象都可以从包含 allinsert、[=17= 的公共抽象 class 继承] 和 delete 方法。类似(无法编译):

abstract class BaseDAO[E <: Identifiable] extends DAO[E] with HasDatabaseConfigProvider[JdbcProfile] {
  import driver.api._
  private val entities = TableQuery[BaseTable]

  def all(): Future[Seq[E]] = db.run(entities.result)
  def insert(entity: E): Future[Unit] = db.run(entities += entity).map { _ => () }
  def delete(entity: E): Future[Int] = db.run(entities.filter(_.id === entity.id).delete)
  def find(id: Int): Future[E] = db.run(entities.filter(_.id === entities.id))

  trait BaseTable { this: Table[_] =>
    def id = column[String]("id", O.PrimaryKey, O.AutoInc)
  }
}

有人可以指出我的错误吗?谢谢。

数据库不可知且代码高度可重用

我正在使用 SlickPlayframework,这就是我实现数据库不可知和通用存储库的方式。

注意这个作品的灵感来源于Active Slick

我想在我的 case class 上定义像这样的基本 crud 操作。我应该能够做到 countupdatedeletecreate。我只想编写一次凝乳代码并永远重复使用它。

这是演示这一点的片段。

case class Dog(name: String, id: Option[Long] = None)
Dog("some_dog").save()
Dog("some_dog").insert()
Dog("some_dog", Some(1)).delete()

CrudActions.scala

import slick.backend.DatabaseConfig
import slick.driver.JdbcProfile

import scala.concurrent.ExecutionContext


trait CrudActions {
  val dbConfig: DatabaseConfig[JdbcProfile]
  import dbConfig.driver.api._

  type Model

  def count: DBIO[Int]

  def save(model: Model)(implicit ec: ExecutionContext): DBIO[Model]

  def update(model: Model)(implicit ec: ExecutionContext): DBIO[Model]

  def delete(model: Model)(implicit ec: ExecutionContext): DBIO[Int]

  def fetchAll(fetchSize: Int = 100)(implicit ec: ExecutionContext): StreamingDBIO[Seq[Model], Model]
}

现在让我们 Entity 进入画面。请注意 Entity 只不过是我们的案例 class

Entitycase class 我们在其上进行 crud 操作。为了定位我们的实体,还让 Id 就位。 Id 对于在数据库中定位和操作实体或记录很重要。还有 Id 实体的唯一标识

EntityActionsLike.scala

import slick.backend.DatabaseConfig
import slick.driver.JdbcProfile

import scala.concurrent.ExecutionContext

trait EntityActionsLike extends CrudActions {
  val dbConfig: DatabaseConfig[JdbcProfile]
  import dbConfig.driver.api._

  type Entity

  type Id

  type Model = Entity

  def insert(entity: Entity)(implicit ec: ExecutionContext): DBIO[Id]

  def deleteById(id: Id)(implicit ec: ExecutionContext): DBIO[Int]

  def findById(id: Id)(implicit ec: ExecutionContext): DBIO[Entity]

  def findOptionById(id: Id)(implicit ec: ExecutionContext): DBIO[Option[Entity]]
}

进口slick.ast.BaseTypedType 进口 slick.backend.DatabaseConfig 导入 slick.driver.JdbcProfile

进口scala.concurrent.ExecutionContext

现在让我们来实现这些方法。为了进行操作,我们需要 TableTableQuery。假设我们有 tabletableQuery。 traits 的好处是我们可以声明一个契约并将实现细节留给 subclasses 或 subtypes

EntityActions.scala

trait EntityActions extends EntityActionsLike {
  val dbConfig: DatabaseConfig[JdbcProfile]
  import dbConfig.driver.api._

  type EntityTable <: Table[Entity]

  def tableQuery: TableQuery[EntityTable]

  def $id(table: EntityTable): Rep[Id]

  def modelIdContract: ModelIdContract[Entity,Id]

  override def count: DBIO[Int] = tableQuery.size.result

  override def insert(entity: Entity)(implicit ec: ExecutionContext): DBIO[Id] = {
    tableQuery.returning(tableQuery.map($id(_))) += entity
  }

  override def deleteById(id: Id)(implicit ec: ExecutionContext): DBIO[Int] = {
    filterById(id).delete
  }

  override def findById(id: Id)(implicit ec: ExecutionContext): DBIO[Entity] = {
    filterById(id).result.head
  }

  override def findOptionById(id: Id)(implicit ec: ExecutionContext): DBIO[Option[Entity]] = {
    filterById(id).result.headOption
  }

  override def save(model: Entity)(implicit ec: ExecutionContext): DBIO[Entity] = {
    insert(model).flatMap { id =>
      filterById(id).result.head
    }.transactionally
  }

  override def update(model: Entity)(implicit ec: ExecutionContext): DBIO[Entity] = {
    filterById(modelIdContract.get(model)).update(model).map { _ => model }.transactionally
  }

  override def delete(model: Entity)(implicit ec: ExecutionContext): DBIO[Int] = {
    filterById(modelIdContract.get(model)).delete
  }

  override def fetchAll(fetchSize: Int)(implicit ec: ExecutionContext): StreamingDBIO[Seq[Entity], Entity] = {
    tableQuery.result.transactionally.withStatementParameters(fetchSize = fetchSize)
  }

  def filterById(id: Id) = tableQuery.filter($id(_) === id)

  def baseTypedType: BaseTypedType[Id]

  protected implicit lazy val btt: BaseTypedType[Id] = baseTypedType

}

ActiveRecord.scala

import slick.dbio.DBIO

import scala.concurrent.ExecutionContext


abstract class ActiveRecord[R <: CrudActions](val repo: R) {
  def model: repo.Model
  def save()(implicit ec: ExecutionContext): DBIO[repo.Model] = repo.save(model)
  def update()(implicit ec: ExecutionContext): DBIO[repo.Model] = repo.update(model)
  def delete()(implicit ec: ExecutionContext): DBIO[Int] = repo.delete(model)
}

ModelContract.scala

case class ModelIdContract[A, B](get: A => B, set: (A, B) => A)

使用方法

Sample.scala

import com.google.inject.{Inject, Singleton}
import play.api.db.slick.DatabaseConfigProvider
import slick.ast.BaseTypedType
import slick.backend.DatabaseConfig
import slick.driver.JdbcProfile
import slick.{ActiveRecord, EntityActions, ModelIdContract}

case class Dog(name: String, id: Option[Long] = None)

@Singleton
class DogActiveRecord @Inject() (databaseConfigProvider: DatabaseConfigProvider) extends EntityActions {

  override val dbConfig: DatabaseConfig[JdbcProfile] = databaseConfigProvider.get[JdbcProfile]

  import dbConfig.driver.api._

  override def tableQuery = TableQuery(new Dogs(_))

  override def $id(table: Dogs): Rep[Id] = table.id

  override def modelIdContract: ModelIdContract[Dog, Id] = ModelIdContract(dog => dog.id.get, (dog, id) => dog.copy(id = Some(id)))

  override def baseTypedType: BaseTypedType[Id] = implicitly[BaseTypedType[Id]]

  override type Entity = Dog
  override type Id = Long
  override type EntityTable = Dogs

  class Dogs(tag: Tag) extends Table[Dog](tag, "DogsTable") {
    def name = column[String]("name")
    def id = column[Long]("id", O.PrimaryKey)
    def * = (name, id.?) <> (Dog.tupled, Dog.unapply)
  }

  implicit class ActiveRecordImplicit(val model: Entity) extends ActiveRecord(this)

  import scala.concurrent.ExecutionContext.Implicits.global

  val result = Dog("some_dog").save()

  val res2 = Dog("some_other_dog", Some(1)).delete()

  val res3 = Dog("some_crazy_dog", Some(1)).update()
}

现在我们可以像这样直接对Dog进行操作

Dog("some_dog").save()

这个隐式对我们来说很神奇

implicit class ActiveRecordImplicit(val model: Entity) extends ActiveRecord(this)

您还可以在 EntityActions

中添加 scheme 创建和删除逻辑
tableQuery.schema.create
table.schema.drop