scalagenericsslickdatabase-agnostic

Slick generic AND driver agnostic


Basically what I want to achieve is a combination of:

Slick 3.0.0 database agnostism and Slick 3 reusable generic repository

I tried a lot, actually, but I can't get this to work at all.

abstract class BaseModel[T <: slick.lifted.AbstractTable[_]](query: TableQuery[T], val driver: JdbcProfile, val dbTableName: String)
{
  lazy val all: TableQuery[T] = TableQuery[T]
  import driver.api._

  def createTable = all.schema.create
  def dropTable = all.schema.create

  abstract class BaseTable[B](val tag: Tag) extends Table[B](tag, dbTableName)
  {
    def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
  }
}

Now here we have a problem already:

def createTable = all.schema.create and the same with dropTable... -> schema cannot be resolved here, although I import the driver before.

But an even bigger problem comes in when I subclass this:

Here is the code

class NodeModel(driver: JdbcProfile, dbTableName: String) extends BaseModel[NodeTable](TableQuery[NodeTable], driver, dbTableName) {

  val dbDriver = driver
  import dbDriver.api._

  class NodeTable(tag: Tag) extends BaseTable[Node](tag)
  {
    override def * = id.? <> (Node, Node.unapply)
  }

  //lazy val all: TableQuery[NodeTable] = TableQuery[NodeTable]
  def createTable: DBIO[Unit] = all.schema.create
  def dropTable: DBIO[Unit] = all.schema.drop 
  def insert(node: Node) = all += node
}

This won't compile obviously because I cannot pass NodeTable as T, but gives an idea of what I want to achieve.

Do you have any idea how to solve this? I also tried with companion objects, moving the BaseTable out of the BaseModel and trying to load a simpleDriver... but it looks like that functionality was removed from Slick in a recent version :(


Solution

  • Database agnostic and Code is highly reusable

    I am using Slick with Playframework and this is how I achieved database agnostic and generic repository.

    Note that this work is inspired from Active Slick

    I want to have basic crud operations like this to be defined on my case class. I should be able to do count, update, delete and create. I want to write the curd code just once and reuse it for ever.

    Here is the snippet which demonstrates this.

    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]
    }
    

    Now lets get our Entity into picture. Note that Entity is nothing but our case class

    Entity is case class on which we do crud operations. For locating our entity lets also have Id in place. Id is important for locating and operating an entity or record in the database. Also Id uniquely identities for entity

    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]]
    }
    

    Now lets implement these methods. For doing operations we need Table and TableQuery. Lets say we have table and tableQuery. The good about traits is we can declare a contract and leave the implementation details to subclasses or subtypes

    EntityActions.scala

    import slick.ast.BaseTypedType
    import slick.backend.DatabaseConfig
    import slick.driver.JdbcProfile
    
    import scala.concurrent.ExecutionContext
    
    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)
    

    How to Use

    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()
    }
    

    Now we can do operations on Dog directly like this

    Dog("some_dog").save()
    

    This implicit does the magic for us

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

    You can also add scheme creation and dropping logic in EntityActions

    tableQuery.schema.create
    table.schema.drop