It was more than 3 years ago that I struggled to find a tutorial for integrating the Play Framework with Slick.

At the time I decided to write the most comprehensive tutorial I could make on how to create a full app with persistent storage using:

Play Framework - the most adopted MVC framework in the scala ecosystem,

- the most adopted MVC framework in the scala ecosystem, Slick - the standard database access framework in scala,

- the standard database access framework in scala, MySQL - because I had always used Postgres and wanted to try MySQL.

I looked for existing demo apps and started removing all the boilerplate I could. I removed dependency injection, useless controllers, internationalization, etc.

Back to the present, the resulting tutorial I wrote got a very good feedback. Lot of visits, lot of comments (27 comments wow), a few github issues, and even pull requests. It was awesome to feel I was helping people getting into the scala ecosystem.

In these 3 years I got some people asking, and even contributing, to update the demo app to Play 2.5. But since I’ve been a bit away from scala, I let the tutorial become deprecated.

With the release of Play 2.7.0 on February, I decided to finally update the demo app. You can find the updated demo for the newest version in the GitHub repository, as well as all the other versions.

You can see the differences on the corresponding pull request. No big changes were needed. Let’s go through the differences step by step from the original guide:

0. Creating a new play template project

First difference: activator is no longer the way to create new projects from a template. Where before you’d do:

activator new application-name play-scala

now you do:

sbt new playframework/play-scala-seed.g8

1. Creating the model

This step suffers no major changes since it’s just plain scala code. But since Play Framework moved into Guice as the default dependency injection, we are going to stop relying in scala objects, and creating classes.

case class User ( id : Long , firstName : String , lastName : String , mobile : Long , email : String ) case class UserFormData ( firstName : String , lastName : String , mobile : Long , email : String ) object UserForm { val form = Form ( mapping ( "firstName" -> nonEmptyText , "lastName" -> nonEmptyText , "mobile" -> longNumber , "email" -> email )( UserFormData . apply )( UserFormData . unapply ) ) } class Users () { // notice the change here var users : Seq [ User ] = Seq () def add ( user : User ) : String = { users = users :+ user . copy ( id = users . length ) // manual id increment "User successfully added" } def delete ( id : Long ) : Option [ Int ] = { val originalSize = users . length users = users . filterNot ( _ . id == id ) Some ( originalSize - users . length ) // returning the number of deleted users } def get ( id : Long ) : Option [ User ] = users . find ( _ . id == id ) def listAll : Seq [ User ] = users }

2. Application Controllers

Here we have a few more small changes, mostly due to dependency injection.

@Singleton class ApplicationController @Inject () ( cc : ControllerComponents , userService : UserService ) extends AbstractController ( cc ) with Logging { def index () = Action . async { implicit request : Request [ AnyContent ] => userService . listAllUsers map { users => Ok ( views . html . index ( UserForm . form , users )) } } def addUser () = Action . async { implicit request : Request [ AnyContent ] => UserForm . form . bindFromRequest . fold ( // if any error in submitted data errorForm => { logger . warn ( s "Form submission with error: ${errorForm.errors}" ) Future . successful ( Ok ( views . html . index ( errorForm , Seq . empty [ User ]))) }, data => { val newUser = User ( 0 , data . firstName , data . lastName , data . mobile , data . email ) userService . addUser ( newUser ). map ( _ => Redirect ( routes . ApplicationController . index ())) }) } def deleteUser ( id : Long ) = Action . async { implicit request : Request [ AnyContent ] => userService . deleteUser ( id ) map { res => Redirect ( routes . ApplicationController . index ()) } } }

3. UI views

Here we have no changes 🎉!

Adding persistent storage

Now let’s go through the steps to add storage configuration.

0. Depencies

As expected, dependencies got new versions:

"com.typesafe.play" %% "play-slick" % "4.0.0" , "com.typesafe.play" %% "play-slick-evolutions" % "4.0.0" , "mysql" % "mysql-connector-java" % "8.0.15" ,

1. Configurations

Some configurations keys have changed, but no big deal:

# this allows to skip some form security checks # see https://www.playframework.com/documentation/2.7.x/Filters#disabling-default-filters play.filters.disabled+=play.filters.csrf.CSRFFilter slick.dbs.default.profile = "slick.jdbc.MySQLProfile$" slick.dbs.default.db.driver = "com.mysql.jdbc.Driver" slick.dbs.default.db.url = "jdbc:mysql://localhost/example?serverTimezone=UTC" slick.dbs.default.db.user = "root" slick.dbs.default.db.password = ""

2. Integrate models with slick

The table definition is exactly the same as the original (for Play 2.4 tutorial), but due to dependency injection once again, we have a minor change on the way to get the database config provider:

import slick.jdbc.MySQLProfile.api._ class UserTableDef ( tag : Tag ) extends Table [ User ]( tag , "user" ) { def id = column [ Long ]( "id" , O . PrimaryKey , O . AutoInc ) def firstName = column [ String ]( "first_name" ) def lastName = column [ String ]( "last_name" ) def mobile = column [ Long ]( "mobile" ) def email = column [ String ]( "email" ) override def * = ( id , firstName , lastName , mobile , email ) <>( User . tupled , User . unapply ) } class Users @Inject () ( protected val dbConfigProvider : DatabaseConfigProvider ) ( implicit executionContext : ExecutionContext ) extends HasDatabaseConfigProvider [ JdbcProfile ] { // the HasDatabaseConfigProvider trait gives access to the // dbConfig object that we need to run the slick queries val users = TableQuery [ UserTableDef ] def add ( user : User ) : Future [ String ] = { dbConfig . db . run ( users += user ). map ( res => "User successfully added" ). recover { case ex : Exception => ex . getCause . getMessage } } def delete ( id : Long ) : Future [ Int ] = { dbConfig . db . run ( users . filter ( _ . id === id ). delete ) } def get ( id : Long ) : Future [ Option [ User ]] = { dbConfig . db . run ( users . filter ( _ . id === id ). result . headOption ) } def listAll : Future [ Seq [ User ]] = { dbConfig . db . run ( users . result ) } }

3. Evolutions

Database evolutions also keep exactly the same 🎉🎉🎉 !

4. Final Remarks

As you can see, the tutorial is still quite simple, and I hope it helps solving those small but annoying issues when starting with Play and Slick.

If you want to know everything that has changed, you should have a look at the official migration guides: Play 2.7 migration guide.

Resources