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)
}
Related
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.
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
I'm scala/play/slick newbie so please don't be too mad if I ask dumb question.
Here goes the question.
I have several slick table definitions, here is one of them:
import javax.inject.Inject
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.db.NamedDatabase
import slick.driver.JdbcProfile
import scala.concurrent.Future
case class User(id: Int, login: String, password: String) extends Identifiable
class UserDAO #Inject()(#NamedDatabase protected val dbConfigProvider: DatabaseConfigProvider) extends HasDatabaseConfigProvider[JdbcProfile] {
import driver.api._
private val users = TableQuery[UsersTable]
def all(): Future[Seq[User]] = db.run(users.result)
def insert(dog: User): Future[Unit] = db.run(users += dog).map { _ => () }
def delete(id: Int): Future[Int] = db.run(users.filter(_.id === id).delete)
private class UsersTable(tag: Tag) extends Table[User](tag, "USER") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def email = column[String]("email")
def password = column[String]("password")
def * = (id, email, password) <> (User.tupled, User.unapply)
}
}
Imagine I have much more tables which have def id = column[Int]("id", O.PrimaryKey, O.AutoInc) to eliminate this I need to write something like:
trait Identifiable {
this: Table[_] =>
def id = column[String]("id", O.PrimaryKey)
}
But how do I import Table here in a database agnostic manner? Moreover there is more room for enhancements: all DAO objects providing access to Identifiable Tables can be inherited from a common abstract class containing all, insert, find and delete methods. Something like (was unable to compile it):
abstract class BaseDAO[E <: Identifiable] extends DAO[E] with HasDatabaseConfigProvider[JdbcProfile] {
import driver.api._
private val entities = TableQuery[BaseTable]
def all(): Future[Seq[E]] = db.run(entities.result)
def insert(entity: E): Future[Unit] = db.run(entities += entity).map { _ => () }
def delete(entity: E): Future[Int] = db.run(entities.filter(_.id === entity.id).delete)
def find(id: Int): Future[E] = db.run(entities.filter(_.id === entities.id))
trait BaseTable { this: Table[_] =>
def id = column[String]("id", O.PrimaryKey, O.AutoInc)
}
}
Could somebody please point me to my mistakes? Thanks.
Database agnostic and Code is highly reusable
I am using Slick with Playframework and this is how I achieved database agnostic and generic repository.
Note that this work is inspired from Active Slick
I want to have basic crud operations like this to be defined on my case class. I should be able to do count, update, delete and create. I want to write the curd code just once and reuse it for ever.
Here is the snippet which demonstrates this.
case class Dog(name: String, id: Option[Long] = None)
Dog("some_dog").save()
Dog("some_dog").insert()
Dog("some_dog", Some(1)).delete()
CrudActions.scala
import slick.backend.DatabaseConfig
import slick.driver.JdbcProfile
import scala.concurrent.ExecutionContext
trait CrudActions {
val dbConfig: DatabaseConfig[JdbcProfile]
import dbConfig.driver.api._
type Model
def count: DBIO[Int]
def save(model: Model)(implicit ec: ExecutionContext): DBIO[Model]
def update(model: Model)(implicit ec: ExecutionContext): DBIO[Model]
def delete(model: Model)(implicit ec: ExecutionContext): DBIO[Int]
def fetchAll(fetchSize: Int = 100)(implicit ec: ExecutionContext): StreamingDBIO[Seq[Model], Model]
}
Now lets get our Entity into picture. Note that Entity is nothing but our case class
Entity is case class on which we do crud operations. For locating our entity lets also have Id in place. Id is important for locating and operating an entity or record in the database. Also Id uniquely identities for entity
EntityActionsLike.scala
import slick.backend.DatabaseConfig
import slick.driver.JdbcProfile
import scala.concurrent.ExecutionContext
trait EntityActionsLike extends CrudActions {
val dbConfig: DatabaseConfig[JdbcProfile]
import dbConfig.driver.api._
type Entity
type Id
type Model = Entity
def insert(entity: Entity)(implicit ec: ExecutionContext): DBIO[Id]
def deleteById(id: Id)(implicit ec: ExecutionContext): DBIO[Int]
def findById(id: Id)(implicit ec: ExecutionContext): DBIO[Entity]
def findOptionById(id: Id)(implicit ec: ExecutionContext): DBIO[Option[Entity]]
}
import slick.ast.BaseTypedType
import slick.backend.DatabaseConfig
import slick.driver.JdbcProfile
import scala.concurrent.ExecutionContext
Now lets implement these methods. For doing operations we need Table and TableQuery. Lets say we have table and tableQuery. The good about traits is we can declare a contract and leave the implementation details to subclasses or subtypes
EntityActions.scala
trait EntityActions extends EntityActionsLike {
val dbConfig: DatabaseConfig[JdbcProfile]
import dbConfig.driver.api._
type EntityTable <: Table[Entity]
def tableQuery: TableQuery[EntityTable]
def $id(table: EntityTable): Rep[Id]
def modelIdContract: ModelIdContract[Entity,Id]
override def count: DBIO[Int] = tableQuery.size.result
override def insert(entity: Entity)(implicit ec: ExecutionContext): DBIO[Id] = {
tableQuery.returning(tableQuery.map($id(_))) += entity
}
override def deleteById(id: Id)(implicit ec: ExecutionContext): DBIO[Int] = {
filterById(id).delete
}
override def findById(id: Id)(implicit ec: ExecutionContext): DBIO[Entity] = {
filterById(id).result.head
}
override def findOptionById(id: Id)(implicit ec: ExecutionContext): DBIO[Option[Entity]] = {
filterById(id).result.headOption
}
override def save(model: Entity)(implicit ec: ExecutionContext): DBIO[Entity] = {
insert(model).flatMap { id =>
filterById(id).result.head
}.transactionally
}
override def update(model: Entity)(implicit ec: ExecutionContext): DBIO[Entity] = {
filterById(modelIdContract.get(model)).update(model).map { _ => model }.transactionally
}
override def delete(model: Entity)(implicit ec: ExecutionContext): DBIO[Int] = {
filterById(modelIdContract.get(model)).delete
}
override def fetchAll(fetchSize: Int)(implicit ec: ExecutionContext): StreamingDBIO[Seq[Entity], Entity] = {
tableQuery.result.transactionally.withStatementParameters(fetchSize = fetchSize)
}
def filterById(id: Id) = tableQuery.filter($id(_) === id)
def baseTypedType: BaseTypedType[Id]
protected implicit lazy val btt: BaseTypedType[Id] = baseTypedType
}
ActiveRecord.scala
import slick.dbio.DBIO
import scala.concurrent.ExecutionContext
abstract class ActiveRecord[R <: CrudActions](val repo: R) {
def model: repo.Model
def save()(implicit ec: ExecutionContext): DBIO[repo.Model] = repo.save(model)
def update()(implicit ec: ExecutionContext): DBIO[repo.Model] = repo.update(model)
def delete()(implicit ec: ExecutionContext): DBIO[Int] = repo.delete(model)
}
ModelContract.scala
case class ModelIdContract[A, B](get: A => B, set: (A, B) => A)
How to Use
Sample.scala
import com.google.inject.{Inject, Singleton}
import play.api.db.slick.DatabaseConfigProvider
import slick.ast.BaseTypedType
import slick.backend.DatabaseConfig
import slick.driver.JdbcProfile
import slick.{ActiveRecord, EntityActions, ModelIdContract}
case class Dog(name: String, id: Option[Long] = None)
#Singleton
class DogActiveRecord #Inject() (databaseConfigProvider: DatabaseConfigProvider) extends EntityActions {
override val dbConfig: DatabaseConfig[JdbcProfile] = databaseConfigProvider.get[JdbcProfile]
import dbConfig.driver.api._
override def tableQuery = TableQuery(new Dogs(_))
override def $id(table: Dogs): Rep[Id] = table.id
override def modelIdContract: ModelIdContract[Dog, Id] = ModelIdContract(dog => dog.id.get, (dog, id) => dog.copy(id = Some(id)))
override def baseTypedType: BaseTypedType[Id] = implicitly[BaseTypedType[Id]]
override type Entity = Dog
override type Id = Long
override type EntityTable = Dogs
class Dogs(tag: Tag) extends Table[Dog](tag, "DogsTable") {
def name = column[String]("name")
def id = column[Long]("id", O.PrimaryKey)
def * = (name, id.?) <> (Dog.tupled, Dog.unapply)
}
implicit class ActiveRecordImplicit(val model: Entity) extends ActiveRecord(this)
import scala.concurrent.ExecutionContext.Implicits.global
val result = Dog("some_dog").save()
val res2 = Dog("some_other_dog", Some(1)).delete()
val res3 = Dog("some_crazy_dog", Some(1)).update()
}
Now we can do operations on Dog directly like this
Dog("some_dog").save()
This implicit does the magic for us
implicit class ActiveRecordImplicit(val model: Entity) extends ActiveRecord(this)
You can also add scheme creation and dropping logic in EntityActions
tableQuery.schema.create
table.schema.drop
Basically what I want to achieve is a combination of:
Slick 3.0.0 database agnostism
and
Slick 3 reusable generic repository
I tried a lot, actually, but I can't get this to work at all.
abstract class BaseModel[T <: slick.lifted.AbstractTable[_]](query: TableQuery[T], val driver: JdbcProfile, val dbTableName: String)
{
lazy val all: TableQuery[T] = TableQuery[T]
import driver.api._
def createTable = all.schema.create
def dropTable = all.schema.create
abstract class BaseTable[B](val tag: Tag) extends Table[B](tag, dbTableName)
{
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
}
}
Now here we have a problem already:
def createTable = all.schema.create and the same with dropTable... -> schema cannot be resolved here, although I import the driver before.
But an even bigger problem comes in when I subclass this:
Here is the code
class NodeModel(driver: JdbcProfile, dbTableName: String) extends BaseModel[NodeTable](TableQuery[NodeTable], driver, dbTableName) {
val dbDriver = driver
import dbDriver.api._
class NodeTable(tag: Tag) extends BaseTable[Node](tag)
{
override def * = id.? <> (Node, Node.unapply)
}
//lazy val all: TableQuery[NodeTable] = TableQuery[NodeTable]
def createTable: DBIO[Unit] = all.schema.create
def dropTable: DBIO[Unit] = all.schema.drop
def insert(node: Node) = all += node
}
This won't compile obviously because I cannot pass NodeTable as T, but gives an idea of what I want to achieve.
Do you have any idea how to solve this? I also tried with companion objects, moving the BaseTable out of the BaseModel and trying to load a simpleDriver... but it looks like that functionality was removed from Slick in a recent version :(
Database agnostic and Code is highly reusable
I am using Slick with Playframework and this is how I achieved database agnostic and generic repository.
Note that this work is inspired from Active Slick
I want to have basic crud operations like this to be defined on my case class. I should be able to do count, update, delete and create. I want to write the curd code just once and reuse it for ever.
Here is the snippet which demonstrates this.
case class Dog(name: String, id: Option[Long] = None)
Dog("some_dog").save()
Dog("some_dog").insert()
Dog("some_dog", Some(1)).delete()
CrudActions.scala
import slick.backend.DatabaseConfig
import slick.driver.JdbcProfile
import scala.concurrent.ExecutionContext
trait CrudActions {
val dbConfig: DatabaseConfig[JdbcProfile]
import dbConfig.driver.api._
type Model
def count: DBIO[Int]
def save(model: Model)(implicit ec: ExecutionContext): DBIO[Model]
def update(model: Model)(implicit ec: ExecutionContext): DBIO[Model]
def delete(model: Model)(implicit ec: ExecutionContext): DBIO[Int]
def fetchAll(fetchSize: Int = 100)(implicit ec: ExecutionContext): StreamingDBIO[Seq[Model], Model]
}
Now lets get our Entity into picture. Note that Entity is nothing but our case class
Entity is case class on which we do crud operations. For locating our entity lets also have Id in place. Id is important for locating and operating an entity or record in the database. Also Id uniquely identities for entity
EntityActionsLike.scala
import slick.backend.DatabaseConfig
import slick.driver.JdbcProfile
import scala.concurrent.ExecutionContext
trait EntityActionsLike extends CrudActions {
val dbConfig: DatabaseConfig[JdbcProfile]
import dbConfig.driver.api._
type Entity
type Id
type Model = Entity
def insert(entity: Entity)(implicit ec: ExecutionContext): DBIO[Id]
def deleteById(id: Id)(implicit ec: ExecutionContext): DBIO[Int]
def findById(id: Id)(implicit ec: ExecutionContext): DBIO[Entity]
def findOptionById(id: Id)(implicit ec: ExecutionContext): DBIO[Option[Entity]]
}
Now lets implement these methods. For doing operations we need Table and TableQuery. Lets say we have table and tableQuery. The good about traits is we can declare a contract and leave the implementation details to subclasses or subtypes
EntityActions.scala
import slick.ast.BaseTypedType
import slick.backend.DatabaseConfig
import slick.driver.JdbcProfile
import scala.concurrent.ExecutionContext
trait EntityActions extends EntityActionsLike {
val dbConfig: DatabaseConfig[JdbcProfile]
import dbConfig.driver.api._
type EntityTable <: Table[Entity]
def tableQuery: TableQuery[EntityTable]
def $id(table: EntityTable): Rep[Id]
def modelIdContract: ModelIdContract[Entity,Id]
override def count: DBIO[Int] = tableQuery.size.result
override def insert(entity: Entity)(implicit ec: ExecutionContext): DBIO[Id] = {
tableQuery.returning(tableQuery.map($id(_))) += entity
}
override def deleteById(id: Id)(implicit ec: ExecutionContext): DBIO[Int] = {
filterById(id).delete
}
override def findById(id: Id)(implicit ec: ExecutionContext): DBIO[Entity] = {
filterById(id).result.head
}
override def findOptionById(id: Id)(implicit ec: ExecutionContext): DBIO[Option[Entity]] = {
filterById(id).result.headOption
}
override def save(model: Entity)(implicit ec: ExecutionContext): DBIO[Entity] = {
insert(model).flatMap { id =>
filterById(id).result.head
}.transactionally
}
override def update(model: Entity)(implicit ec: ExecutionContext): DBIO[Entity] = {
filterById(modelIdContract.get(model)).update(model).map { _ => model }.transactionally
}
override def delete(model: Entity)(implicit ec: ExecutionContext): DBIO[Int] = {
filterById(modelIdContract.get(model)).delete
}
override def fetchAll(fetchSize: Int)(implicit ec: ExecutionContext): StreamingDBIO[Seq[Entity], Entity] = {
tableQuery.result.transactionally.withStatementParameters(fetchSize = fetchSize)
}
def filterById(id: Id) = tableQuery.filter($id(_) === id)
def baseTypedType: BaseTypedType[Id]
protected implicit lazy val btt: BaseTypedType[Id] = baseTypedType
}
ActiveRecord.scala
import slick.dbio.DBIO
import scala.concurrent.ExecutionContext
abstract class ActiveRecord[R <: CrudActions](val repo: R) {
def model: repo.Model
def save()(implicit ec: ExecutionContext): DBIO[repo.Model] = repo.save(model)
def update()(implicit ec: ExecutionContext): DBIO[repo.Model] = repo.update(model)
def delete()(implicit ec: ExecutionContext): DBIO[Int] = repo.delete(model)
}
ModelContract.scala
case class ModelIdContract[A, B](get: A => B, set: (A, B) => A)
How to Use
Sample.scala
import com.google.inject.{Inject, Singleton}
import play.api.db.slick.DatabaseConfigProvider
import slick.ast.BaseTypedType
import slick.backend.DatabaseConfig
import slick.driver.JdbcProfile
import slick.{ActiveRecord, EntityActions, ModelIdContract}
case class Dog(name: String, id: Option[Long] = None)
#Singleton
class DogActiveRecord #Inject() (databaseConfigProvider: DatabaseConfigProvider) extends EntityActions {
override val dbConfig: DatabaseConfig[JdbcProfile] = databaseConfigProvider.get[JdbcProfile]
import dbConfig.driver.api._
override def tableQuery = TableQuery(new Dogs(_))
override def $id(table: Dogs): Rep[Id] = table.id
override def modelIdContract: ModelIdContract[Dog, Id] = ModelIdContract(dog => dog.id.get, (dog, id) => dog.copy(id = Some(id)))
override def baseTypedType: BaseTypedType[Id] = implicitly[BaseTypedType[Id]]
override type Entity = Dog
override type Id = Long
override type EntityTable = Dogs
class Dogs(tag: Tag) extends Table[Dog](tag, "DogsTable") {
def name = column[String]("name")
def id = column[Long]("id", O.PrimaryKey)
def * = (name, id.?) <> (Dog.tupled, Dog.unapply)
}
implicit class ActiveRecordImplicit(val model: Entity) extends ActiveRecord(this)
import scala.concurrent.ExecutionContext.Implicits.global
val result = Dog("some_dog").save()
val res2 = Dog("some_other_dog", Some(1)).delete()
val res3 = Dog("some_crazy_dog", Some(1)).update()
}
Now we can do operations on Dog directly like this
Dog("some_dog").save()
This implicit does the magic for us
implicit class ActiveRecordImplicit(val model: Entity) extends ActiveRecord(this)
You can also add scheme creation and dropping logic in EntityActions
tableQuery.schema.create
table.schema.drop
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"))
}
}