Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala type inference working with Slick Table

Have such models (simplified):

case class User(id:Int,name:String)
case class Address(id:Int,name:String)
...

Slick (2.1.0 version) table mapping:

class Users(_tableTag: Tag) extends Table[User](_tableTag, "users") with WithId[Users, User] {`
  val id: Column[Int] = column[Int]("id", O.AutoInc, O.PrimaryKey)
  ...
}
trait WithId[T, R] {
  this: Table[R] =>
  def id: Column[Int]
}

Mixing trait WithId I want to implement generic DAO methods for different tables with column id: Column[Int] (I want method findById to work with both User and Address table mappings)

trait GenericSlickDAO[T <: WithId[T, R], R] {
  def db: Database

  def findById(id: Int)(implicit stk: SlickTableQuery[T]): Option[R] = db.withSession { implicit session =>
    stk.tableQuery.filter(_.id === id).list.headOption
  }

trait SlickTableQuery[T] {
  def tableQuery: TableQuery[T]
}

object SlickTableQuery {
  implicit val usersQ = new SlickTableQuery[Users] {
    val tableQuery: Table Query[Users] = Users
  }
}

The problem is that findById doesn't compile:

Error:(13, 45) type mismatch; found : Option[T#TableElementType] required: Option[R] stk.tableQuery.filter(_.id === id).list.headOption

As I see it T is of type WithId[T, R] and at the same time is of type Table[R]. Slick implements the Table type such that if X=Table[Y] then X#TableElementType=Y.

So in my case T#TableElementType=R and Option[T#TableElementType] should be inferred as Option[R] but it isn't. Where am I wrong?

like image 556
ka4eli Avatar asked Dec 11 '14 00:12

ka4eli


Video Answer


3 Answers

Your assumption about WithId[T, R] being of type Table[R] is wrong. The self-type annotation in WithId[T, R] just requires a Table[R] to be mixed in, but that doesn't mean that WithId[T, R] is a Table[R].

I think you confuse the declaration of WithId with instances of WithId which eventually need to be an instance of a Table.

Your upper type bound constraint in the GenericSlickDAO trait also doesn't guarantee you the property of WithId to be an instance of Table, since any type is a subtype of itself.

See this question for a more elaborate explanation about the differences between self-types and subtypes.

like image 132
edi Avatar answered Oct 15 '22 10:10

edi


I'm using play-slick and I tried to do exactly like you, with a trait and using self-type without success.

But I succeeded with the following:

import modelsunscanned.TableWithId

import scala.slick.jdbc.JdbcBackend
import scala.slick.lifted.TableQuery
import play.api.db.slick.Config.driver.simple._


/**
 * @author Sebastien Lorber ([email protected])
 */
package object models {

  private[models] val Users = TableQuery(new UserTable(_))
  private[models] val Profiles = TableQuery(new ProfileTable(_))
  private[models] val Companies = TableQuery(new CompanyTable(_))
  private[models] val Contacts = TableQuery(new ContactTable(_))


  trait ModelWithId {
    val id: String
  }


  trait BaseRepository[T <: ModelWithId] {
    def tableQuery: TableQuery[TableWithId[T]]


    private val FindByIdQuery = Compiled { id: Column[String] =>
      tableQuery.filter(_.id === id)
    }


    def insert(t: T)(implicit session: JdbcBackend#Session) = {
      tableQuery.insert(t)
    }


    def getById(id: String)(implicit session: JdbcBackend#Session): T = FindByIdQuery(id).run.headOption
      .getOrElse(throw new RuntimeException(s"Could not find entity with id=$id"))

    def findById(id: String)(implicit session: JdbcBackend#Session): Option[T] = FindByIdQuery(id).run.headOption




    def update(t: T)(implicit session: JdbcBackend#Session): Unit = {
      val nbUpdated = tableQuery.filter(_.id === t.id).update(t)
      require(nbUpdated == 1,s"Exactly one should have been updated, not $nbUpdated")
    }

    def delete(t: T)(implicit session: JdbcBackend#Session) = {
      val nbDeleted = tableQuery.filter(_.id === t.id).delete
      require(nbDeleted == 1,s"Exactly one should have been deleted, not $nbDeleted")
    }

    def getAll(implicit session: JdbcBackend#Session): List[T] = tableQuery.list

  }

}


// play-slick bug, see https://github.com/playframework/play-slick/issues/227
package modelsunscanned {
   abstract class TableWithId[T](tableTag: Tag,tableName: String) extends Table[T](tableTag,tableName) {
    def id: Column[String]
  }
}

I give you an exemple usage:

object CompanyRepository extends BaseRepository[Company] {
  // Don't know yet how to avoid that cast :(
  def tableQuery = Companies.asInstanceOf[TableQuery[TableWithId[Company]]] 

  // Other methods here
  ...
}




case class Company(
                    id: String = java.util.UUID.randomUUID().toString,
                    name: String,
                    mainContactId: String,
                    logoUrl: Option[String],
                    activityDescription: Option[String],
                    context: Option[String],
                    employeesCount: Option[Int]
                    ) extends ModelWithId


class CompanyTable(tag: Tag) extends TableWithId[Company](tag,"COMPANY") {
  override def id = column[String]("id", O.PrimaryKey)
  def name = column[String]("name", O.NotNull)
  def mainContactId = column[String]("main_contact_id", O.NotNull)
  def logoUrl = column[Option[String]]("logo_url", O.Nullable)
  def activityDescription = column[Option[String]]("description", O.Nullable)
  def context = column[Option[String]]("context", O.Nullable)
  def employeesCount = column[Option[Int]]("employees_count", O.Nullable)
  //
  def * = (id, name, mainContactId,logoUrl, activityDescription, context, employeesCount) <> (Company.tupled,Company.unapply)
  //
  def name_index = index("idx_name", name, unique = true)
}

Note that active-slick is also using something similar

like image 43
Sebastien Lorber Avatar answered Oct 15 '22 08:10

Sebastien Lorber


This helped me out a lot. It's a pretty simple example of a genericdao https://gist.github.com/lshoo/9785645

package slicks.docs.dao

import scala.slick.driver.PostgresDriver.simple._
import scala.slick.driver._


trait Profile {
  val profile: JdbcProfile
}

trait CrudComponent {
  this: Profile =>

  abstract class Crud[T <: Table[E] with IdentifiableTable[PK], E <: Entity[PK], PK: BaseColumnType](implicit session: Session) {

    val query: TableQuery[T]

    def count: Int = {
      query.length.run
    }

    def findAll: List[E] = {
      query.list()
    }

    def queryById(id: PK) = query.filter(_.id === id)

    def findOne(id: PK): Option[E] = queryById(id).firstOption

    def add(m: E): PK = (query returning query.map(_.id)) += m 

    def withId(model: E, id: PK): E 

    def extractId(m: E): Option[PK] = m.id

    def save(m: E): E = extractId(m) match {
      case Some(id) => {
        queryById(id).update(m)
        m
      }
      case None => withId(m, add(m))
    }

    def saveAll(ms: E*): Option[Int] = query ++= ms

    def deleteById(id: PK): Int = queryById(id).delete

    def delete(m: E): Int = extractId(m) match {
      case Some(id) => deleteById(id)
      case None => 0
    }

  }
}

trait Entity[PK] {
  def id: Option[PK]
}

trait IdentifiableTable[I] {
  def id: Column[I]
}


package slicks.docs

import slicks.docs.dao.{Entity, IdentifiableTable, CrudComponent, Profile}


case class User(id: Option[Long], first: String, last: String) extends Entity[Long]

trait UserComponent extends CrudComponent {
  this: Profile =>

  import profile.simple._

  class UsersTable(tag: Tag) extends Table[User](tag, "users") with IdentifiableTable[Long] {

    override def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
    def first = column[String]("first")
    def last = column[String]("last")

    def * = (id.?, first, last) <> (User.tupled, User.unapply)

  }

  class UserRepository(implicit session: Session) extends Crud[UsersTable, User, Long] {

    override def query = TableQuery[UsersTable]

    override def withId(user: User, id: Long): User = user.copy(id = Option(id))
  }
}
like image 21
ben jarman Avatar answered Oct 15 '22 10:10

ben jarman