Search code examples
scalagenericsslick

How to abstract a column definition between slick tables?


I'm trying to add a new column to several tables in an existing slick application. The column is the same across all the tables: adding a client id to the table to enable filtering by client in all our queries. However, in some cases this id will be optional: as such I need to handle both cases using our existing queries.

The problem comes when I try to write some code to use the new column to filter on the new client. I want to write a method to wrap up the table queries for all of the modified tables and optionally filter on client, but slick fails when I try to write a method to do this.

I've tried adding an abstract class extending Table in slick to wrap up tables where I want to add the new client column, as such

  abstract class ClientTable[T](tag: Tag, name: String) extends Table[T](tag,name)  {
    def client: Rep[Option[Int]]
  }

  class Feeds(tag: Tag) extends ClientTable[Feed](tag, "feeds") {
    def id: Rep[Int] = column[Int]("id", O.PrimaryKey, O.AutoInc)

    def name: Rep[String] = column[String]("name")

    ...

    def client: Rep[Option[Int]] = column[Option[Int]]("client")

    private val list = id :: name :: ... :: client :: HNil

    def * : ProvenShape[Feed] = list.mappedWith(Generic[Feed])
  }

However, this doesn't seem to work with slick at all.

Overall, I am hoping to get this method (or something like it) to work:

  def tableView[T <: ClientTable[E], E] (tableQuery: TableQuery[E], clientId: Option[Int]) = {
    clientId match {
      case Some(id) => tableQuery.filter(_.client === id)
      case None => tableQuery
    }
  }
}

so that I can replace the TableQuery definition in the queries with private val xTable = tableView(TableQuery[X], clientId), and then hopefully not have to change all my existing queries.


Solution

  • Nothing worse than an unanswered stack overflow question! As such, here's how I ended up resolving this.


    The method that I was working towards ended up being correct, I just had my generics slightly wrong. The working version looks like this:

    def tableView[T <: ClientTable[E], E <: ClientColumn](tableQuery: TableQuery[T], clientId: Option[Int]): Query[T, T#TableElementType, Seq] = {
        clientId match {
          case Some(id) => tableQuery.filter(_.client === id)
          case None => tableQuery
        }
      }
    

    In the table definitions you need to both extend the Table type with an implementation that has the client column on it:

    abstract class ClientTable[T](tag: Tag, name: String) extends Table[T](tag, name) {
      def client: Rep[Option[Int]] = column[Option[Int]]("client")
    }
    

    and extend your row case classes with a trait that adds the client field to all of them as such

    trait ClientColumn {
      def client: Option[Int]
    }
    
    case class Feed(id: Int = 0,
                    name: String,
    ...
                    client: Option[Int]) extends ClientColumn
    

    Now the tableView method will work as intended, although seemingly only when passing in type parameters for example val x = tableView[X, x](TableQuery[X], client) as Scala doesn't seem to be able to infer the types of both of the generic parameters.

    One thing that is worth mentioning is that I am not using Slick for my table creation or ddl statements, I am using flyway instead. Using this method with an abstract class and trait might not work in that case.

    Either way good luck anyone that finds this question and answer in the future!