Skip to content

Instantly share code, notes, and snippets.

@cvogt
Last active September 9, 2019 01:30
Show Gist options
  • Star 32 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save cvogt/9239494 to your computer and use it in GitHub Desktop.
Save cvogt/9239494 to your computer and use it in GitHub Desktop.
Slick app architecture cheat sheet
// 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
Copy link

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?

@cvogt
Copy link
Author

cvogt commented Feb 27, 2014

@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.

@ms-tg
Copy link

ms-tg commented Feb 27, 2014

@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?

@ms-tg
Copy link

ms-tg commented Feb 27, 2014

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)

@octonato
Copy link

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.

@asido
Copy link

asido commented May 5, 2015

Typo on lines 19: s/RecordQueryExentions/RecordQueryExtensions/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment