0

I'm implementing a web service using Slick (3.3) for the DB layer. I am trying to make the Slick implementation as generic as possible, hoping to achieve DB-agnosticism as well as generic table, table query, and DAO classes that abstract over the services models as much as possible. I'm trying to do this combining several techniques:

  1. A model hierarchy extending from a common base trait
  2. A table hierarchy, extracting common columns for model traits (inspired by http://gavinschulz.com/posts/2016-01-30-common-model-fields-with-slick-3-part-i.html)
  3. DB-agnosticism, relying on the JdbcProfile trait rather than any DB-specific profile implementation ( as described here: https://stackoverflow.com/a/31128239/4234254 and in the slick multi-db docs)
  4. Cake pattern for dependency injection

I'm having trouble layering some of the schema elements, however, and not being a Scala type expert, I've been unable to figure out the solution on my own. I've created a reproduction of the issue, trying to minimalize it as much as possible, and using a mock slick library. The full code can be found here: https://github.com/anqit/slick_cake_minimal_error_repro/blob/master/src/main/scala/com/anqit/repro/Repro.scala but I'll go through it below.

My "slick" library and model classes:

    abstract class Table[E]
    type TableQuery[W <: Table[_]] = List[W] // not actually a list, but need a concrete type constructor to demonstrate the issue
    object TableQuery {
        def apply[W <: Table[_]]: TableQuery[W] = List[W]()
    }

    trait BaseEntity
    case class SubEntityA() extends BaseEntity
    case class SubEntityB() extends BaseEntity

Combining technique 2 in the list above with the cake pattern, I'm creating traits that wrap schema elements for each model. The base schema includes columns common to entity tables (e.g. id), and entity tables inherit from that:

    trait BaseSchema[E <: BaseEntity] {
        // provides common functionality
        abstract class BaseTableImpl[E] extends Table[E]

        def wrapper: TableQuery[_ <: BaseTableImpl[E]]
    }

    // functionality specific to SubEntityA
    trait SchemaA extends BaseSchema[SubEntityA] {
        class TableA extends BaseTableImpl[SubEntityA]

        // this definition compiles fine without a type annotation
        val queryA = TableQuery[TableA]
        def wrapper = queryA
    }

    // functionality specific to SubEntityB that depends on SchemaA
    trait SchemaB extends BaseSchema[SubEntityB] { self: SchemaA =>
        class TableB extends BaseTableImpl[SubEntityB] {
            // uses SchemaA's queryA to make a FK   
        }

        /*
            attempting to define wrapper here without a type annotation results in the following compilation error (unlike defining wrapper for SchemaA above):
            def wrapper = Wrapper[WrappedB]
                type mismatch;
                [error]  found   : Repro.this.Wrapper[SubB.this.WrappedB]
                [error]     (which expands to)  List[SubB.this.WrappedB]
                [error]  required: Repro.this.Wrapper[_ <: SubB.this.BaseWrapMeImpl[_1]]
                [error]     (which expands to)  List[_ <: SubB.this.BaseWrapMeImpl[_1]]
                [error]         def wrapper = Wrapper[WrappedB]
                [error]                              ^
            it does, however, compile if defined with an explicit type annotation as below
        */

        val queryB = TableQuery[TableB]
        def wrapper: TableQuery[TableB] = queryB
    }

This is where I get my first error, a type mismatch, that I have currently worked around using an explicit type annotation, but I suspect it is related to the main error, stay tuned.

A base DAO, that will provide common query methods:

    trait BaseDao[E <: BaseEntity] { self: BaseSchema[E] => }

And finally, putting all the cake layers together:

    // now, the actual injection of the traits
    class DaoA extends SchemaA
        with BaseDao[SubEntityA]
    // so far so good...

    class DaoB extends SchemaA
        with SchemaB
        with BaseDao[SubEntityB] // blargh! failure! :

    /*
         illegal inheritance;
        [error]  self-type Repro.this.DaoB does not conform to Repro.this.BaseDao[Repro.this.SubEntityB]'s selftype Repro.this.BaseDao[Repro.this.SubEntityB] with Repro.this.BaseSchema[Repro.this.SubEntityB]
        [error]         with BaseDao[SubEntityB]
        [error]              ^
     */

The first error (the type mismatch in SchemaB), I'm completely at a loss. One of the few tricks in my bag is to add explicit type annotations when I run in to type-related errors in Scala, which is the only reason I tried that, and got it to compile. I would love an explanation as to why that is happening, and I suspect fixing my code such that I can write that without the type would probably help me with the second error. Which brings me to... the second error. To me, it looks like I've included all of the necessary traits to satisfy the self-type tree, but I guess not. My guess is that SchemaB, while extending BaseSchema[SubEntityB], is somehow not being recognized as a BaseSchema[SubEntityB]? Have I not set up my hierarchy properly? Or maybe I need to use bounds instead of strict type references?

anqit
  • 780
  • 3
  • 12
  • 1
    See [Scala Slick Cake Pattern: over 9000 classes?](https://stackoverflow.com/q/22866342/2359227) and if not, non of those are helpful? https://stackoverflow.com/search?q=%5Bscala%5D+%5Bslick%5D+cake – Tomer Shetah Jan 03 '21 at 06:05
  • The first question linked deals with Slick 2, which I think is different enough from Slick 3 to the point that it might not be relevant, and anyways, my issue seems to be stemming from the dependency between tables, which I doesn't look like it gets addressed. Also, I should clarify that I don't think this is a Slick-specific issue, I only mention Slick because I don't think I could have explained why the program is structured this way outside of a Slick context :) I think this has more to do with how I'm misusing Scala types and structuring dependencies – anqit Jan 03 '21 at 06:55
  • I've narrowed the culprit down to `SchemaB` 's `SchemaA` self-type declaration. If I remove that, everything compiles as expected, and I don't even need extra-verbose type declarations. Is there some sort of type clash occurring because they're both a `BaseSchema` perhaps? – anqit Jan 04 '21 at 03:00
  • Yes, because of type erasure, no class can extend both `A[B]` and `A[C]`. – Levi Ramsey Jan 04 '21 at 05:13
  • That makes sense, thanks! I've fixed it by separating concerns further, and importing a different trait. – anqit Jan 07 '21 at 04:50

0 Answers0