Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala reflection to instantiate scala.slick.lifted.TableQuery

I have this base trait

trait MyBase {
  type M
  type T <: Table[M]
  val query: TableQuery[T]
}

Where TableQuery is scala.slick.lifted.TableQuery

My subclasses instantiate TableQuery like so:

type M = Account
type T = AccountsTable
val query = TableQuery[T]

I'd like to instantiate the TableQuery in the base trait, possibly by using a lazy val, i.e.

lazy val query: TableQuery[T] = {
  ...
}

I've been playing around with reflection, but haven't had much luck.

like image 215
ic3b3rg Avatar asked May 12 '15 06:05

ic3b3rg


1 Answers

If I understand correctly, what you want is to be able to extend MyBase by simply defining M and T but without having to explicitly instantiate the TableQuery in each derived class.

Using reflection is not really an option because normally you use TableQuery.apply for that (as in val query = TableQuery[MyTable]), and this is implemented through a macro, so you've got a "runtime vs compile-time" issue.

If you absolutely need MyBase to be a trait (as opposed to a class), then I don't see any viable solution. However if you can turn MyBase into a class and turn M and T into type parameters (instead of abstract types), then there is at least one solution. As I hinted in another related question (How to define generic type in Scala?), you can define a type class (say TableQueryBuilder) to capture the call to TableQuery.apply (at the point where the concrete type is known) along with an implicit macro (say TableQueryBuilder.builderForTable) to provide an instance of this type class. You can then define a method (say TableQueryBuilder.build) to actually instantiate the TableQuery, which will just delegate to job to the type class.

// NOTE: tested with scala 2.11.0 & slick 3.0.0
import scala.reflect.macros.Context
import scala.language.experimental.macros
object TableQueryBuilderMacro {
  def createBuilderImpl[T<:AbstractTable[_]:c.WeakTypeTag](c: Context) = {
    import c.universe._
    val T = weakTypeOf[T]
    q"""new TableQueryBuilder[$T]{
      def apply(): TableQuery[$T] = {
        TableQuery[$T]
      }
    }"""
  }
}
trait TableQueryBuilder[T<:AbstractTable[_]] {
  def apply(): TableQuery[T]
}
object TableQueryBuilder {
  implicit def builderForTable[T<:AbstractTable[_]]: TableQueryBuilder[T] = macro TableQueryBuilderMacro.createBuilderImpl[T]
  def build[T<:AbstractTable[_]:TableQueryBuilder](): TableQuery[T] = implicitly[TableQueryBuilder[T]].apply()
}

The net effect is that you don't need anymore to know the concrete value of the type T in order to be able to instantiate a TableQuery[T], provided that you have an implicit instance of TableQueryBuilder[T] in scope. In other words, you can shift the need to know the concrete value of T up to the point where you actually know it.

MyBase (now a class) can then be implemented like this:

class MyBase[M, T <: Table[M] : TableQueryBuilder] {
  lazy val query: TableQuery[T] = TableQueryBuilder.build[T]
}

And you can then extend it without the need to explcitly call TableQuery.apply:

class Coffees(tag: Tag) extends Table[(String, Double)](tag, "COFFEES") {
  def name = column[String]("COF_NAME")
  def price = column[Double]("PRICE")
  def * = (name, price)
}

class Derived extends MyBase[(String, Double), Coffees] // That's it!

What happens here is that in Derived's constructor, an implicit value for TableQueryBuilder[Coffees] is implicitly passed to MyBase's constructor.

The reason why you cannot apply this pattern if MyBase were a trait is pretty mundane: trait constructors cannot have parameters, let alone implicit parameters, so there would be no implicit way to pass the TableQueryBuilder instance.

like image 150
Régis Jean-Gilles Avatar answered Sep 28 '22 01:09

Régis Jean-Gilles