-
-
Save cvogt/9239494 to your computer and use it in GitHub Desktop.
| // Please comment in case of typos or bugs | |
| import scala.slick.driver.H2Driver._ | |
| val db = Database.for...(...) | |
| case class Record( ... ) | |
| class Records(tag: Tag) extends Table[Record](tag,"RECORDS"){ | |
| ... | |
| def * = ... <> (Record.tupled,Record.unapply) | |
| // place additional methods here which return values of type Column | |
| // (compute artificial columns based on other columns) based on a | |
| // records row | |
| def foo = (col1+col2, col3) | |
| ... | |
| } | |
| object records extends TableQuery(new Records(_)){ | |
| // Only place methods here which return a not-yet executed Query or | |
| // (individually meaningful) Column which is based on the WHOLE table. | |
| // Seldom useful, often better placed in RecordQueryExentions | |
| def foos = this.map(_.foo) // <- can only be used on the whole table | |
| ... | |
| } | |
| // usage: | |
| db.withSession{ records.map(_.foo).run } | |
| db.withSession{ records.foos.run } | |
| implicit class RecordQueryExtensions(val q: Query[Records,Record]) extends AnyVal{ | |
| // Only place methods here which return a not-yet executed Query or | |
| // (individually meaningful) Column. | |
| // Methods placed here can be chained/combined. | |
| ... | |
| def foos = q.map(_.foo) | |
| def byId(id: Column[Long]) = q.filter(_.id === id) | |
| } | |
| // usage: | |
| db.withSession{ records.filter(_.age < 30).foos.run } | |
| object RecordsDAO{ | |
| // place methods here that require a database connection | |
| // i.e. do not compose without executing queries, e.g. | |
| // methods take Session argument | |
| // usage: | |
| // db.withSession{ RecordsDAO.foos } | |
| // or | |
| // db.withSession{ implicit session => Records.DAO.foos } | |
| def foos(implicit s:Session) = records.foos.run | |
| val byIdCompiled = Compiled(records.byId) | |
| def byId(id: Long)(implicit s:Session) = byIdCompiled(id).run.headOption | |
| ... | |
| } | |
| // usage: | |
| db.withSession{ RecordsDAO.foos } | |
| db.withSession{ implicit session => Records.DAO.foos } | |
| // Alternative DAO implementation: | |
| case class RecordsDAO(implicit s:Session){ | |
| // centralized implicit session into class argument | |
| def foos = records.foos.run | |
| ... | |
| } | |
| // usage: | |
| db.withSession{ RecordsDAO().foos } | |
| db.withSession{ implicit session => RecordsDAO().foos } | |
| // Another alternative DAO implementation: | |
| object RecordsDAO{ | |
| // in user code | |
| // no withSession boilerplate but no | |
| // control over foos / transaction management | |
| def foos = db.withSession{ records.foos.run } | |
| // or: | |
| def foos = db.withSession{ implicit s => records.foos.run } | |
| // or: | |
| def foos = db.withSession{ records.foos.list()(_) } | |
| ... | |
| } | |
| // usage: | |
| RecordsDAO.foos | |
| // Alternative Table class implementation using a mapped projection case class | |
| // Allows projection case classes to be nested and re-used in queries and multiple tables. | |
| // Uses the recently born CaseClassShape suggested here: | |
| // https://github.com/slick/slick/pull/692 | |
| case class RecordProjection( ... : Column[...], ... : Column[...], ... ){ | |
| // place additional methods here which return values of type Column | |
| // (compute artificial columns based on other columns) based on a | |
| // records row | |
| def foo = (col1+col2, col3) | |
| ... | |
| } | |
| implicit object RecordShape extends CaseClassShape(RecordProjection.tupled,Record.tupled) | |
| class Records extends Table[Record](...){ | |
| def projection = RecordProjection(column(...),column(...),...) // column types can be inferred | |
| def * = projection | |
| } | |
| val records = TableQuery[Records].map(_.projection) | |
| // usage: | |
| db.withSession{ records.map(_.foo).run } |
@francisdb
The byId you showed returns a query, so RecordQueryExtensions. The compiled version is not composable anymore, so RecordDAO makes sense. Updated the code to include byId in appropriate locations.
Regarding slick-dao, I would have to see some real world use cases in order to tell.
@cvogt this is extremely helpful, thank you!
So, in a play-slick project, should each controller action be a DBAction, thereby creating an implicit Session at line 186?
So, for example, a controller action to retrieve all records might be
def getRecords = DBAction { implicit s =>
RecordsDAO.foos
// or, RecordsDAO().foos
}@freekh does this line up with your understanding as well?
@cvogt: where can I introduce a transaction in this pattern?
Also, @cvogt, where should one put a pre-compiled query, and how to use it from the DAO?
Update: Nevermind! I see the example above val byIdCompiled = Compiled(records.byId)
The advantage of the slick-dao is that it implements the basic CRUD in a DAO so we don't have to manipulate queries or any infrastructure/DB code in your domain. Nothing more than that.
I do that because I want everything to go through a DAO, basic CRUD and queries. Otherwise the domain gets cluttered with some locally defined queries, some DAOs, extensions and some on.
Typo on lines 19: s/RecordQueryExentions/RecordQueryExtensions/
Hi Christopher, thanks for the template. I have some more questions:
is the lowercase r on the records object on purpose?
where would I put this?
def byId(id: Column[Long]) = for {
r <- Records if r.id === id
} yield r
and the compiled version?
and the version that takes a session and calls firstOption?
What do you think about this project? https://github.com/rcavalcanti/slick-dao
Anything you would do differently?
You probably want to reduce access to some of these classes/objects/methods? (private)
Other tips on removing boilerplate?