-->

Slick 3 reusable generic repository

2019-01-20 04:04发布

问题:

I am experiencing issues making Slick's TableQuery used in a generic fashion.

Observe the regular situation:

class AccountRepository {
override protected val dbConfig = DatabaseConfigProvider.get[JdbcProfile](Play.current)
val accounts = TableQuery[Accounts]
def all = db.run(accounts.result)
...

The idea would be to extract everything possible into generic trait or abstract class in order to avoid repetition. For the sake of simplicity I included only the problematic code.

abstract class GenericRepository[T] extends HasDatabaseConfig[JdbcProfile] {
override protected val dbConfig = DatabaseConfigProvider.get[JdbcProfile(Play.current)
val table = TableQuery[T]
}

And to use it like:

class AccountRepository extends GenericRepository[Accounts] {

However, that creates a compilation error:

type arguments [T] conform to the bounds of none of the overloaded alternatives of value apply: [E <: slick.lifted.AbstractTable[]]=> slick.lifted.TableQuery[E] [E <: slick.lifted.AbstractTable[]](cons: slick.lifted.Tag => E)slick.lifted.TableQuery[E]

Trying to fix the issue by setting a boundary doesn't help as well.

abstract class GenericRepository[T <: slick.lifted.AbstractTable[T]] extends HasDatabaseConfig[JdbcProfile] {

However, we end up with a different error:

class type required but T found

at following place:

val table = TableQuery[T]

Any idea about the solution?

回答1:

I guess if you can solve the initialization of tableQuery, then you can continue your GenericRepository. I am using Slick 3.0 with PostgreSQL.

In the slick.lifted.TableQuery, there is a method like the following

// object TableQuery
def apply[E <: AbstractTable[_]](cons: Tag => E): TableQuery[E] =
    new TableQuery[E](cons)

So if we can get an instance of E on the fly, then we can get a generic way to create TableQuery. So reflection seems to be a possible way to solve it.

 import scala.reflect.runtime.{ universe => ru }
 import slick.lifted.{ AbstractTable, ProvenShape, Tag }
 import slick.driver.PostgresDriver.api._


  object Reflection {
    val runtimeMirror = ru.runtimeMirror(getClass.getClassLoader)

    def getTypeTag[T: ru.TypeTag] = ru.typeTag[T]

    def createClassByConstructor[T: ru.TypeTag](args: Any*) =
      runtimeMirror.reflectClass(getTypeTag[T].tpe.typeSymbol.asClass)  
       .reflectConstructor(ru.typeOf[T].declaration(ru.nme.CONSTRUCTOR)
       .asMethod)(args: _*).asInstanceOf[T]
  }


  // context bound here is for createClassByConstructor to use
  abstract class GenericTableQuery[U, T <: AbstractTable[U]: ru.TypeTag] {

    import Reflection._

    // look at following code: Students, if you want to initialize Students
    // you're gonna need a tag parameter, that's why we pass tag here
    val tableQuery = TableQuery.apply(tag => createClassByConstructor[T](tag))

  }

 // Sample Table
 case class Student(name: String, age: Int)
 class Students(tag: Tag) extends Table[Student](tag, "students") {
    def name = column[String]("name")
    def age = column[Int]("age")
    override def * : ProvenShape[Student] = (name, age) 
      <> (Student.tupled, Student.unapply _)
 }

 // get TableQuery
 object TestGenericTableQuery extends GenericTableQuery[Student, Students] {
    val studentQuery = tableQuery
 }

The codes mentioned above is just focused on the issue of generic TableQuery, try to combine it with your GenericRepository and your problem may get solved.

Anyway, hope it helps.



回答2:

You have to pass table query manually,

 abstract class GenericRepository[T <: slick.lifted.AbstractTable[_]](query: TableQuery[T])

and in implementation,

class AccountRepository extends GenericRepository[Accounts](TableQuery[Accounts])

I hope this will solve your problem.