Posts DB evolutions
Post
Cancel

DB evolutions

You can create your schema manually and the app will work.
However, each developer that clones the code will have to do so too, and it results in a poor development experience.
While we have many repositories that suffer from this problem, we can fix it with little effort by using db evolutions.

Add evolutions support to our build

Add evolutions support to your project in build.sbt:

1
2
3
4
libraryDependencies ++= Seq(
  evolutions,
  // more dependencies here
)

Reload the sbt-shell (or reimport sbt projects in IntelliJ to see changes in the IDE).

Define the evolutions

Open a new directory under evolutions in the conf directory: right click on conf in the project pane (CMD+1) -> New -> Directory -> evolutions/dish_db -> Enter.

This directory will contain files like <number>.sql, starting from 1. create a file 1.sql in evolutions/dish_db:

1
2
3
4
5
6
7
8
9
10
11
12
# --- !Ups

CREATE TABLE IF NOT EXISTS Dish (
 DishName varchar(255) NOT NULL,
 Description varchar(255) NOT NULL,
 Price INT NOT NULL,
 Id INT NOT NULL AUTO_INCREMENT,
 PRIMARY KEY (Id)
) AUTO_INCREMENT=0;

# --- !Downs
DROP TABLE Dish;

Create a second file 2.sql:

1
2
3
4
5
6
7
8
9
# --- !Ups

INSERT
  INTO Dish (DishName, Description, Price)
  VALUES ('Avocado Sandwich', 'Whole grain bread with Brie cheese, tomatoes and avocado', 8);

# --- !Downs

DELETE FROM Dish WHERE DishName = 'Avocado Sandwich'

The !Ups and !Downs comments are special and contain required transformations and how to revert them, respectively.
The downs evolutions will not be applied in Prod mode (more on modes later).

Run the evolutions

In AppComponents, mix-in EvolutionsComponents and add a method to run the evolutions:

1
2
3
4
5
6
7
8
import play.api.db.evolutions.{ApplicationEvolutions, EvolutionsComponents}

class AppComponents(context: Context) extends BuiltInComponentsFromContext(context)
/* with other components */
  with EvolutionsComponents {
  // some code
  def runEvolutions(): ApplicationEvolutions = applicationEvolutions
}

We need to call runEvolutions() when we start the application, so they would run automatically.
Change AppLoader.scala to do so:

1
2
3
4
5
def load(context: Context): Application = {
  val components = new AppComponents(context)
  components.runEvolutions() // or simply components.applicationEvolutions
  components.application
}

Notice that applicationEvolutions is a lazy val, and will be instantiated when you access it.
Therefore, we could have avoid defining runEvolutions() in AppComponents and just access this val in the loader, but we chose to be more explicit here.

Run the app

You can now run the server by running run in the sbt-shell.
Invoking the dishes API (e.g by browsing to http://localhost:9000/dishes) will show

Database ‘dish_db’ needs evolution!

When evolutions are activated, Play will check your database schema state before each request in DEV mode, or before starting the application in PROD mode.
In DEV mode, if your database schema is not up to date, an error page will suggest that you synchronize your database schema by running the appropriate SQL script. Again, more on modes later.

You can click the button to run the evolutions.
The dishes API will now work as expected.

Apply the evolutions automatically

Lets stop the server and configure the app to auto apply the evolutions.
Add a configuration object for play.evolutions.db.<your_db> In conf/application.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
play {
    http.secret.key="MePzCIzgeI8jhPfWg8RPGCFvLobM6K8bnCabNgSdDBc="
    application.loader = com.example.playground.configuration.AppLoader
    evolutions.db {
        dish_db {
            enabled = true
            schema = "dish_db"
            useLocks = false
            autoApply = true
            autocommit = false
        }
    }
}

Now the evolutions will run automatically in production + developers that clone the repository will not have to click the apply button when running the app.
They will only have to use the sbt run command.

Important: If you need to run more than a single command (usually sbt run, sbt test, mvn run, etc.) in order to run the code locally or test it after you clone the repository, it should raise a red flag regarding the development experience.

You can now run the server by running run in the sbt-shell.
The dishes API will now work as expected.