Common Repository/DAO method to insert value with autoincremented ID using Slick is missing implicits

185 views Asked by At

I'm rather new to scala and trying to learn slick and started with play-slick-example where everything was understandable.

I went off and created my own entities, tables and queries. First trick was to work around getting autoincremeted id upon insertion, but example code covers it, though idiom may be improved. Next thing to work around was to move all common code to one place. By common code I mean basic CRUD operations that are basically copy-paste for all entities. So I went and created base Entity

trait Entity[T <: Entity[T, ID], ID] {
  val id: Option[ID]    
  def withId(id: ID): T
}

With that I went and created BaseRepo that should contain all common code:

abstract class BaseRepo[T <: Entity[T, ID], ID] {    
  protected val dbConfigProvider: DatabaseConfigProvider
  val dbConfig = dbConfigProvider.get[JdbcProfile]

  import dbConfig._
  import profile.api._

  type TableType <: Keyed[ID] with RelationalProfile#Table[T]

  protected val tableQuery: TableQuery[TableType]
}

Where dbConfigProvider is injected into implementations and allows to import proper config (not sure it's needed, but example has it like that). Keyed is another trait that represents Tables with columns id:

trait Keyed[ID] {
  def id: Rep[ID]
}

Everything looks good for now. To extend BaseRepo one would need to properly assign TableType and tableQuery and everything should work. I start with following implementation:

case class Vehicle(override val id: Option[Long], name: String, plate: String, modelId: Long)
  extends Entity[Vehicle, Long] {
  override def withId(id: Long): Vehicle = this.copy(id = Some(id))
}

And following repo:

@Singleton
class VehicleRepository @Inject()(override val dbConfigProvider: DatabaseConfigProvider)
                                 (implicit ec: ExecutionContext) 
  extends BaseRepo [Vehicle, Long]{

  import dbConfig._
  import profile.api._

  type TableType = Vehicles
  val tableQuery = TableQuery[Vehicles]

  class Vehicles(tag:Tag) extends Table[Vehicle](tag:Tag, "vehicles") with Keyed[Long] {
    def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
    def name = column[String]("name")
    def plate = column[String]("plate")
    def modelId = column[Long]("modelId")

    def * = (id.?, name,plate,modelId) <> ((Vehicle.apply _).tupled, Vehicle.unapply)
  }

Everything still looks great! Now I add all() to BaseRepo:

  def all() = db.run {
    tableQuery.result
  }

And it works! I can list all my Vehicle entities through injected repo:VehicleRepository with simple repo.all() (Well, i get Future to be precise, but who cares)

Next, I go and try to generalize insert with autoincremented id and put it into BaseRepo:

def create(item: T, ec:ExecutionContext) = db.run{
    ((tableQuery returning tableQuery.map(_.id)) += item)
      .map(id => item.withId(id))(ec)
  }

Don't mind explicit ExecutionContext here, but anyway, this does not work and Error I get is frustrating:

Slick does not know how to map the given types.
Possible causes: T in Table[T] does not match your * projection,
 you use an unsupported type in a Query (e.g. scala List),
 or you forgot to import a driver api into scope.
  Required level: slick.lifted.FlatShapeLevel
     Source type: slick.lifted.Rep[ID]
   Unpacked type: T
     Packed type: G
]

If I move this method back into VehicleRepository (replacing T with Vehicle) everything works like a charm.

Some hours of digging later I understand that tableQuery.map takes some (implicit shape: Shape[_ <: FlatShapeLevel, F, T, G]) as implicit parameter and I literally have no idea where does that come from into the scope of VehicleRepository and why is it not available in my BaseRepo

Any comment or advice on how to work around this or maybe some other approaches to generalize CRUDs with Slick would be appriciated!

I'm using Play-2.8 Slick-3.3.2 play-slick-5.0.0 scala-2.13.1

0

There are 0 answers