I have currently this code:
class UsersRepository #Inject()(protected val dbConfigProvider: DatabaseConfigProvider) extends HasDatabaseConfigProvider[JdbcProfile] {
import driver.api._
private val Users = TableQuery[UsersTable]
def findByName(name: String): Future[Option[User]] =
db.run(Users.filter(_.name === name).take(1).result.headOption)
implicit val localDateTimeColumnType = MappedColumnType.base[LocalDateTime, Timestamp](
d => Timestamp.from(d.toInstant(ZoneOffset.ofHours(0))),
d => d.toLocalDateTime
)
trait GenericTable {
this: Table[_] =>
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def createdAt = column[LocalDateTime]("created_at")
def updatedAt = column[LocalDateTime]("updated_at")
}
private class UsersTable(tag: Tag) extends Table[User](tag, "User") with GenericTable {
def name = column[String]("name")
def url = column[String]("url")
def nameIndex = index("name_index", name, unique = true)
override def * = (id, createdAt, updatedAt, name, url) <> (User.tupled, User.unapply)
}
}
How can I now easily move the GenericTable trait out of the UsersRepository so that I can use it with other tables? If I just move it out, then things like column, Table and O are not found anymore, because I loose the things from the driver import.
I also would like to move the UsersTable definition itself out of the DAO/repository class. I have the same problem in this case.
Thanks,
Magnus
Provider dbConfig in the trait as a no input parameter def
trait GenericTableProvider {
val dbConfig: DatabaseConfig[JdbcProfile]
import dbConfig.driver.api._
trait GenericTable {
this: Table[_] =>
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def createdAt = column[LocalDateTime]("created_at")
def updatedAt = column[LocalDateTime]("updated_at")
}
}
and then use it as shown in the code snippet below.
class UsersRepository #Inject()(protected override val dbConfigProvider: DatabaseConfigProvider)
extends HasDatabaseConfigProvider[JdbcProfile]
with GenericTableProvider {
import driver.api._
private val Users = TableQuery[UsersTable]
private class UsersTable(tag: Tag) extends Table[User](tag, "User") with GenericTable {
def name = column[String]("name")
def url = column[String]("url")
def nameIndex = index("name_index", name, unique = true)
override def * = (id, createdAt, updatedAt, name, url) <> (User.tupled, User.unapply)
}
.....
}
override def dbConfigProvider with a val dbConfigProvider
Related
I've been creating a new project using Play framework, Slick and Postgresql and I don't understand how can I extract entities from DAO. Most tutorials show examples where entity (class extending table) is a class within dao singleton.
I've done actually the same:
import javax.inject.{Inject, Singleton}
import org.joda.time.DateTime
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
import slick.jdbc.JdbcProfile
import scala.concurrent.{ExecutionContext, Future}
#Singleton
class CardDao #Inject()(protected val dbConfigProvider: DatabaseConfigProvider)
(implicit executionContext: ExecutionContext) extends HasDatabaseConfigProvider[JdbcProfile] {
import dbConfig.profile.api._
val Cards = TableQuery[CardsTable]
private[dao] class CardsTable(tag: Tag) extends Table[Card](tag, "card") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name", O.Unique)
def value = column[BigDecimal]("value")
def currencyId = column[Int]("currency_id")
def created = column[DateTime]("created")
def active = column[Boolean]("active")
override def * = (id, name, value, currencyId, created) <> (Card.tupled, Card.unapply)
}
def all(): Future[Seq[Card]] = db.run(Cards.result)
}
That's ok. I don't mind having an entity here but when I create another DAO I cannot access my TableQuery (joining tables or creating foreign keys) as it's just a field in singleton.
I was trying to extract CardsTable to separate class with companion object containing TableQuery but it turns out that column method, O, foreignKey come somewhere from HasDatabaseConfigProvider trait (importing dbConfig.profile.api._) so I'm not sure but I guess I have to pass dbConfig implicitly to the class.
How would you do that ? It's just the beginning of the project so really don't want to make some rookie mistakes at this point.
Thanks to Łukasz I've found a way how to do this:
trait Tables {
this: HasDatabaseConfigProvider[JdbcProfile] => {}
import dbConfig.profile.api._
val Cards = TableQuery[CardsTable]
val FaceValues = TableQuery[FaceValuesTable]
val Currencies = TableQuery[CurrenciesTable]
val Cryptocurrencies = TableQuery[CryptocurrenciesTable]
val Wallets = TableQuery[WalletsTable]
val Transactions = TableQuery[TransactionsTable]
class CryptocurrenciesTable(tag: Tag) extends Table[Cryptocurrency](tag, "cryptocurrency") with ActiveAndCreated[Cryptocurrency] {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name", O.Unique)
def cryptocurrencyCode = column[String]("cryptocurrency_code", O.Unique)
def blockchainExplorerUrl = column[String]("blockchain_explorer_url")
def iconSvg = column[String]("icon_svg")
override def * = (id, name, cryptocurrencyCode, blockchainExplorerUrl, iconSvg, created) <>
(Cryptocurrency.tupled, Cryptocurrency.unapply)
}
class FaceValuesTable(tag: Tag) extends Table[FaceValue](tag, "face_value") with ActiveAndCreated[FaceValue] {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def value = column[BigDecimal]("value")
def currencyId = column[Int]("currency_id")
def cardId = column[Int]("card_id")
def card = foreignKey("card_fk", cardId, Cards)(_.id)
def currency = foreignKey("currency_fk", currencyId, Currencies)(_.id)
override def * = (id, value, currencyId, cardId) <> (FaceValue.tupled, FaceValue.unapply)
}
...
}
Simple Dao trait:
trait Dao extends HasDatabaseConfigProvider[JdbcProfile] with Tables
And now all DAOs are very simple:
#Singleton
class WalletDao #Inject()(protected val dbConfigProvider: DatabaseConfigProvider)
(implicit executionContext: ExecutionContext) extends Dao {
import dbConfig.profile.api._
def find(walletAddress: WalletAddress): Future[Option[Wallet]] = {
val query = for {
wallet <- Wallets
cryptocurrency <- Cryptocurrencies if cryptocurrency.id === wallet.id &&
cryptocurrency.cryptocurrencyCode === walletAddress.cryptocurrency
} yield wallet
db.run(query.result.headOption)
}
def find(walletId: Int): Future[Option[Wallet]] = db.run(Wallets.filter(_.id === walletId).result.headOption)
def save(wallet: Wallet): Future[Int] = db.run(Wallets += wallet)
}
I am trying to implement a simple Repository in a PlayFramework app with the play-slick plugin.
I am new to scala, new to slick, new to play.
When trying to compile my code, i get the following error:
type mismatch;
found : slick.lifted.TableQuery[UserRepository.this.UserTable]
required: UserRepository.this.profile.api.TableQuery[UserRepository.this.BaseTable]
(which expands to) slick.lifted.TableQuery[UserRepository.this.BaseTable]
Note: UserRepository.this.UserTable <: UserRepository.this.BaseTable, but class TableQuery is invariant in type E.
You may wish to define E as +E instead. (SLS 4.5)
Here is my code:
trait BaseEntity {
def id: Long
def createdAt: LocalDateTime
def updatedAt: LocalDateTime
def deletedAt: LocalDateTime
}
case class User (id: Long, firstname: String, lastname: String, password: String, createdAt: LocalDateTime, updatedAt: LocalDateTime, deletedAt: LocalDateTime) extends BaseEntity
abstract class BaseRepository[E <: BaseEntity](protected val dbConfigProvider: DatabaseConfigProvider, executionContext: ExecutionContext) extends HasDatabaseConfigProvider[JdbcProfile] {
import profile.api._
implicit val localDateTimeToTimestamp: BaseColumnType[LocalDateTime] = MappedColumnType.base[LocalDateTime, Timestamp](
l => Timestamp.valueOf(l),
t => t.toLocalDateTime
)
protected abstract class BaseTable(tag: Tag, tableName: String) extends Table[E](tag, tableName) {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def createdAt = column[LocalDateTime]("created_at")
def updatedAt = column[LocalDateTime]("updated_at")
def deletedAt = column[LocalDateTime]("deleted_at")
}
protected val query: TableQuery[BaseTable]
def all(): Future[Seq[E]] = db.run(query.result)
}
class UserRepository #Inject()(#NamedDatabase("effective-potato-play") override protected val dbConfigProvider: DatabaseConfigProvider)(implicit executionContext: ExecutionContext) extends BaseRepository[User](dbConfigProvider, executionContext) {
import profile.api._
protected class UserTable(tag: Tag) extends BaseTable(tag, "user") {
def firstname = column[String]("firstname")
def lastname = column[String]("lastname")
def password = column[String]("password")
def * = (id, firstname, lastname, password, createdAt, updatedAt, deletedAt) <> (User.tupled, User.unapply)
}
protected val query = TableQuery[UserTable]
}
pamus comment led me in the right directions.
I solved the following by moving the inner Table-Class to an own class and giving the query for the repository as constructor argument.
abstract class BaseRepository[E <: BaseEntity, T <: BaseTable[E]](val dbConfigProvider: DatabaseConfigProvider, executionContext: ExecutionContext, query: TableQuery[T]) extends HasDatabaseConfigProvider[JdbcProfile] {
def all(): Future[Seq[E]] = db.run(query.result)
def find(id: Long): Future[Option[E]] = db.run(query.filter(_.id === id).result.headOption)
}
class UserRepository #Inject()(dbConfigProvider: DatabaseConfigProvider)(implicit executionContext: ExecutionContext) extends BaseRepository[User, UserTable](dbConfigProvider, executionContext, TableQuery[UserTable]) {
}
abstract class BaseTable[E <: BaseEntity](tag: Tag, tableName: String) extends Table[E](tag, tableName) {
implicit val localDateTimeToTimestamp: BaseColumnType[LocalDateTime] = MappedColumnType.base[LocalDateTime, Timestamp](
l => Timestamp.valueOf(l),
t => t.toLocalDateTime
)
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def createdAt = column[LocalDateTime]("created_at")
def updatedAt = column[LocalDateTime]("updated_at")
def deletedAt = column[Option[LocalDateTime]]("deleted_at", O.Default(null))
}
class UserTable(tag: Tag) extends BaseTable[User](tag, "user") {
def firstname = column[String]("firstname")
def lastname = column[String]("lastname")
def password = column[String]("password")
def * = (id, firstname, lastname, password, createdAt, updatedAt, deletedAt) <> (User.tupled, User.unapply)
}
I can then use the queries from the BaseRepository like so
userRepository.find(1).map { user =>
Ok(Json.toJson(user))
}
I want to define the queries and tables without the direct import of the final database profile (H2, MySQL etc) - so in unit tests I would use H2, and for the staging/production I would use MySQL. So far I couldn't find a way to import all necessary abstract components to get this working:
import slick.jdbc.H2Profile.api._
class OAuthCredentialsTable(tag: Tag) extends Table[OAuth](tag, "credentials_oauth") {
def username: Rep[String] = column[String]("username", O.SqlType("VARCHAR"))
def service: Rep[String] = column[String]("service", O.SqlType("VARCHAR"))
def serviceId: Rep[String] = column[String]("service_id", O.SqlType("VARCHAR"))
def userRef: ForeignKeyQuery[UserTable, User] = foreignKey("oauth_user_fk", username, userTable)(_.username, onDelete = ForeignKeyAction.Cascade)
override def * = (username, service, serviceId) <> (OAuth.tupled, OAuth.unapply)
}
val oauthTable: TableQuery[OAuthCredentialsTable] = TableQuery[OAuthCredentialsTable]
Eventually I discovered that to accomplish the driver-agnostic setup, it could be done as simple as that:
object UserPersistence extends JdbcProfile {
import api._
class UserTable(tag: Tag) extends Table[User](tag, "users") {
def username: Rep[String] = column[String]("username", O.PrimaryKey, O.SqlType("VARCHAR"))
def password: Rep[String] = column[String]("password", O.SqlType("VARCHAR"))
def serverkey: Rep[String] = column[String]("serverkey", O.SqlType("VARCHAR"), O.Length(64))
def salt: Rep[String] = column[String]("salt", O.SqlType("VARCHAR"), O.Length(64))
def iterations: Rep[Int] = column[Int]("iterationcount", O.SqlType("INT"))
def created: Rep[Timestamp] = column[Timestamp]("created_at", O.SqlType("TIMESTAMP"))
val mkUser: ((String, String, String, String, Int, Timestamp)) ⇒ User = {
case ((name, pwd, _, _, _, created)) ⇒ User(name, pwd, created.toInstant)
}
def unMkUser(u: User) = Some(u.username, u.password, "", "", 0, new Timestamp(u.createdAt.toEpochMilli))
override def * = (username, password, serverkey, salt, iterations, created) <> (mkUser, unMkUser)
}
val userTable: TableQuery[UserTable] = TableQuery[UserTable]
}
and then in order to use different profiles - you need to supply different database implementation when you run something, e.g
trait UserPersistence {
protected def db: Database
protected implicit val ec: ExecutionContext
override def findCredentialsOrRegister(oauthCredentials: OAuth): Future[User] = {
val userFound = (for (
creds ← oauthTable.filter(x ⇒ x.service === oauthCredentials.service && x.serviceId === oauthCredentials.serviceId);
user ← userTable.filter(x ⇒ x.username === creds.username)
) yield user).result
val authenticate = userFound.flatMap {
case Seq(user) ⇒
DBIO.from(Future.successful(user))
case Seq() ⇒
val newUUID = UUID.randomUUID
val user = User(newUUID.toString, UUID.randomUUID().toString, Instant.now())
DBIO.seq(
userTable += user,
oauthTable += oauthCredentials.copy(username = newUUID.toString)
) andThen DBIO.from(Future.successful(user))
}
db.run(authenticate.transactionally)
}
and then in test
val impl = new UserPersistence {
override def db: H2Profile.api.Database = // initialize and populate the database
override implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.global
}
For MySql just assign the MySQL profile to db property (and change type).
I hope I got you right, you can achieve it with injections and your configuration, separating test config from prod,
So I think you can do something like this-
Create binding for injection-
class DbModule extends Module {
bind[slick.driver.JdbcProfile#Backend#Database] toNonLazy DatabaseConfigProvider.get[JdbcProfile](inject[Application]).db
}
Then (for example)-
abstract class BaseDaoImpl[T <: IdentifiableTable[S] : ClassTag, S <: RichEntity[S]](implicit injector: Injector)
extends BaseDao[S] with Injectable with Logger{
protected val db: slick.driver.JdbcProfile#Backend#Database = inject[slick.driver.JdbcProfile#Backend#Database]
protected val table: TableQuery[T]
def insert(obj: S): Future[S] = {
val insertQuery = table.returning(table.map(_.id)) into ((item, id) => item.withDifferentId(Some(id)))
val res = db.run(insertQuery += obj)
logger.debug(s"Inserting ${this.getClass} - ${res.toString}")
res
}
def all(): Future[Seq[S]] = {
db.run(table.result)
}
}
I used this configuration in order to change the DB profile in a application.conf file.
import slick.jdbc.JdbcProfile
import slick.basic._
trait DBComponent {
// use the application.conf to change the profile
val database = DatabaseConfig.forConfig[JdbcProfile]("h2")
val driver = database.profile
}
trait BankTable extends DBComponent {
import driver.api._
class BankTable(tag: Tag) extends Table[Bank](tag, "bank") {
val id = column[Int]("id", O.PrimaryKey, O.AutoInc)
val name = column[String]("name")
def * = (name, id.?) <> (Bank.tupled, Bank.unapply)
}
protected val bankTableQuery = TableQuery[BankTable]
protected def bankTableAutoInc = bankTableQuery returning
bankTableQuery.map(_.id)
}
class BankRepositoryBDImpl extends BankTable with BankRepository {
import driver.api._
val db = database.db
def createBank(bank: Bank): Future[Int] = db.run { bankTableAutoInc += bank }
}
and use a application.conf file
h2 = {
url = "jdbc:h2:mem:test1"
driver = org.h2.Driver
connectionPool = disabled
keepAliveConnection = true
}
sqlite = {
url = "jdbc:sqlite::memory:"
driver = org.sqlite.JDBC
connectionPool = disabled
keepAliveConnection = true
}
To accomplish this in my project I created an object out of the JdbcProfile trait and imported that everywhere:
JdbcProfile.scala:
package my.application.data.support
import slick.jdbc.JdbcProfile
object JdbcProfile extends JdbcProfile
Wherever I define tables I import it as follows:
import my.application.data.support.JdbcProfile.api._
...
In order to support extensions to slick, I created an abstraction called DatabaseSupport where each database type would have their own concrete implementation which is injected at startup-time depending on configuration.
DatabaseSupport.scala:
package my.application.data.support
/**
* Database support implicits and methods.
*/
trait DatabaseSupport {
/**
* Implicit database array type.
*/
implicit val seqStringType: JdbcProfile.DriverJdbcType[Seq[String]]
}
This way I can have seperate H2DatabaseSupport.scala and PostgresDatabaseSupport.scala implementations that specifies seqStringType in database-specific ways.
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?
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.
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 (lorber.sebastien#gmail.com)
*/
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
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))
}
}
I've been looking around on how to implement a generic trait for commons CRUD and other kinds of operations, I looked at this and this and the method specified are working well.
What I would like to have is a generic method for insertion, my class looks like this at the moment (non generic implementation):
object CampaignModel {
val campaigns = TableQuery[Campaign]
def insert(campaign: CampaignRow)(implicit s: Session) = {
campaigns.insert(campaign)
}
}
What I tried so far, following the first link, was this (generic implementation):
trait PostgresGeneric[T <: Table[A], A] {
val tableReference = TableQuery[T]
def insertGeneric(row: ? What type goes here ?)(implicit s: Session) = tableReference.insert(row)
}
When I inspect the insert method it looks like the right type should be T#TableElementType but my knowledge is pretty basic and I can't wrap my head around types, I tried T and A and the compiler says that the classtype does not conform to the trait one's.
Other infos, the tables are generated with the slick table generation tools
case class CampaignRow(id: Long, name: Option[String])
/** Table description of table campaign. Objects of this class serve as prototypes for rows in queries. */
class Campaign(tag: Tag) extends Table[CampaignRow](tag, "campaign") {
def * = (id, name) <>(CampaignRow.tupled, CampaignRow.unapply)
/** Maps whole row to an option. Useful for outer joins. */
def ? = (id.?, name).shaped.<>({
r => import r._; _1.map(_ => CampaignRow.tupled((_1.get, _2)))
}, (_: Any) => throw new Exception("Inserting into ? projection not supported."))
/** Database column id AutoInc, PrimaryKey */
val id: Column[Long] = column[Long]("id", O.AutoInc, O.PrimaryKey)
/** Database column name */
val name: Column[Option[String]] = column[Option[String]]("name")
}
I managed to make it work, this is my generic trait:
import scala.slick.driver.PostgresDriver
import scala.slick.driver.PostgresDriver.simple._
import path.to.RichTable
trait PostgresGeneric[T <: RichTable[A], A] {
val tableReference: TableQuery[T]
def insert(row: T#TableElementType)(implicit s: Session) =
tableReference.insert(row)
def insertAndGetId(row: T#TableElementType)(implicit s: Session) =
(tableReference returning tableReference.map(_.id)) += row
def deleteById(id: Long)(implicit s: Session): Boolean =
tableReference.filter(_.id === id).delete == 1
def updateById(id: Long, row: T#TableElementType)(implicit s: Session): Boolean =
tableReference.filter(_.id === id).update(row) == 1
def selectById(id: Long)(implicit s: Session): Option[T#TableElementType] =
tableReference.filter(_.id === id).firstOption
def existsById(id: Long)(implicit s: Session): Boolean = {
(for {
row <- tableReference
if row.id === id
} yield row).firstOption.isDefined
}
}
Where RichTable is an abstract class with an id field, this, with the upper bound constraint is useful to get the id field of T#TableElementType (see this for more info):
import scala.slick.driver.PostgresDriver.simple._
import scala.slick.jdbc.{GetResult => GR}
abstract class RichTable[T](tag: Tag, name: String) extends Table[T](tag, name) {
val id: Column[Long] = column[Long]("id", O.PrimaryKey, O.AutoInc)
}
And my campaign table now looks like this:
import scala.slick.driver.PostgresDriver.simple._
import scala.slick.jdbc.{GetResult => GR}
import scala.slick.lifted.TableQuery
case class CampaignRow(id: Long, name: Option[String])
class Campaign(tag: Tag) extends RichTable[CampaignRow](tag, "campaign") {
def * = (id, name) <>(CampaignRow.tupled, CampaignRow.unapply)
def ? = (id.?, name).shaped.<>({
r => import r._; _1.map(_ => CampaignRow.tupled((_1.get, _2)))
}, (_: Any) => throw new Exception("Inserting into ? projection not supported."))
override val id: Column[Long] = column[Long]("id", O.AutoInc, O.PrimaryKey)
val name: Column[Option[String]] = column[Option[String]]("name")
}
The model implementing the generic trait looks like this:
object CampaignModel extends PostgresGeneric[Campaign, CampaignRow] {
override val tableReference: PostgresDriver.simple.TableQuery[Tables.Campaign] =
TableQuery[Campaign]
def insertCampaign(row: CampaignRow) = {
insert(CampaignRow(0, "test"))
}
}