Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/manual/working/javaGuide/main/sql/JavaEbean.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ play.ebean.db.default.generateEvolutionsScripts = true

This is independent of whether Play applies the evolutions to a database (`play.evolutions.db.<name>.enabled`). For example, turn off generating the script for a read-only database, or for a database whose evolution scripts come from the classpath (e.g. from a dependency), because a `conf/evolutions/<name>/1.sql` file takes precedence over these.

Play Ebean writes the script where Play's evolutions look for it, so it follows the [location of the evolution scripts](https://www.playframework.com/documentation/latest/Evolutions#Location-of-the-evolution-scripts) (`play.evolutions.path` or `play.evolutions.db.<name>.path`), including absolute paths and paths relative to the project's root folder.

If your project doesn't use Play's layout (`disablePlugins(PlayLayoutPlugin)`), so that its resources are in `src/main/resources` instead of `conf`, be aware that Play still looks for the scripts in `conf/evolutions/<name>/` first, but doesn't package that folder. So the script that Play Ebean generates there would be missing in production, and in dev and test mode it would take precedence over a script of yours in `src/main/resources/evolutions/<name>/`. You can let Play Ebean write the script into the resources in dev and test mode:

```properties
# Generates the script in src/main/resources/evolutions/default/1.sql
play.evolutions.db.default.path = "./src/main/resources/evolutions"
```

Then start the application in production with `-Dplay.evolutions.db.default.path=evolutions`, so that Play reads the packaged script from the classpath. Or turn off generating the script, as shown above. That doesn't remove a script that Play Ebean generated before, so if there is a generated `conf/evolutions/<name>/1.sql` that you don't want, delete that file.

> For more information about Ebean, see the [Ebean documentation](https://ebean-orm.github.io/docs).

#### Configuring the sbt plugin
Expand Down
14 changes: 11 additions & 3 deletions play-ebean/src/main/java/play/db/ebean/EbeanDynamicEvolutions.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public void create() {
return;
}
File evolutions =
environment.getFile(
evolutionsFile(
Evolutions$.MODULE$.fileName(
key, 1, evolutionsConfig.forDatasource(key).path()));
try {
Expand All @@ -109,8 +109,7 @@ public void create() {
if (content.isEmpty()
|| content.startsWith("# --- Created by Ebean DDL")
|| content.startsWith("-- Created by Ebean DDL")) {
environment
.getFile(
evolutionsFile(
Evolutions$.MODULE$.directoryName(
key, evolutionsConfig.forDatasource(key).path()))
.mkdirs();
Expand All @@ -125,6 +124,15 @@ public void create() {
});
}

/**
* Resolves the location of an evolution script like Play's evolutions do: an absolute path as it
* is, any other path relative to the application's root folder.
*/
private File evolutionsFile(String path) {
File file = new File(path);
return file.isAbsolute() ? file : environment.getFile(path);
}

/**
* Helper method that generates the required evolution to properly run Ebean/DB.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package play.db.ebean;

import static org.junit.Assert.assertEquals;

import io.ebean.DB;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import play.inject.guice.GuiceApplicationBuilder;
import play.test.Helpers;

/**
* Checks that Play applies the generated evolution script, wherever {@code
* play.evolutions.db.<name>.path} puts it.
*/
public class EbeanEvolutionsScriptPathTest {

private static final AtomicInteger databases = new AtomicInteger();

/** Contains the application's root folder {@code app}, and the folders next to it. */
@Rule public TemporaryFolder folder = new TemporaryFolder();

private File root() {
return new File(folder.getRoot(), "app");
}

/**
* Runs the application, and returns the number of rows of the given table, which only works if
* Play applied an evolution script that creates it.
*/
private int run(Map<String, Object> config, String table) {
root().mkdirs();
GuiceApplicationBuilder builder =
new GuiceApplicationBuilder()
.in(root())
.configure(
Map.of(
"db.default.driver",
"org.h2.Driver",
"db.default.url",
"jdbc:h2:mem:play-ebean-path-" + databases.incrementAndGet(),
"ebean.default",
List.of("play.db.ebean.models.first.*"),
"play.evolutions.db.default.autoApply",
true))
.configure(config);
int[] rows = new int[1];
Helpers.running(
builder.build(),
() ->
rows[0] =
DB.byName("default")
.sqlQuery("select count(*) as c from " + table)
.findOne()
.getInteger("c"));
return rows[0];
}

/** Returns the evolution scripts in the temporary folder, relative to it. */
private List<Path> scripts() throws IOException {
Path base = folder.getRoot().toPath();
try (Stream<Path> files = Files.walk(base)) {
return files
.filter(file -> file.getFileName().toString().equals("1.sql"))
.map(base::relativize)
.collect(Collectors.toList());
}
}

private void assertGeneratesScript(Map<String, Object> config, String script) throws IOException {
assertEquals(0, run(config, "first_model"));
assertEquals(List.of(Path.of(script)), scripts());
}

@Test
public void defaultPath() throws IOException {
assertGeneratesScript(Map.of(), "app/conf/evolutions/default/1.sql");
}

@Test
public void folderInConf() throws IOException {
assertGeneratesScript(
Map.of("play.evolutions.db.default.path", "migrations"),
"app/conf/migrations/default/1.sql");
}

@Test
public void folderInProject() throws IOException {
assertGeneratesScript(
Map.of("play.evolutions.db.default.path", "./migrations"), "app/migrations/default/1.sql");
}

@Test
public void folderNextToProject() throws IOException {
assertGeneratesScript(
Map.of("play.evolutions.db.default.path", "../migrations"), "migrations/default/1.sql");
}

@Test
public void absoluteFolder() throws IOException {
String path = new File(folder.getRoot(), "migrations").getAbsolutePath();
assertGeneratesScript(
Map.of("play.evolutions.db.default.path", path), "migrations/default/1.sql");
}

@Test
public void keepsHandWrittenScriptInAbsoluteFolder() throws IOException {
File migrations = new File(folder.getRoot(), "migrations");
Path script = new File(migrations, "default/1.sql").toPath();
String handWritten =
String.join(
"\n",
"-- !Ups",
"create table first_model (id bigint not null primary key);",
"create table hand_written (id bigint);",
"",
"-- !Downs",
"drop table hand_written;",
"drop table first_model;",
"");
Files.createDirectories(script.getParent());
Files.write(script, handWritten.getBytes(StandardCharsets.UTF_8));

assertEquals(
0,
run(
Map.of("play.evolutions.db.default.path", migrations.getAbsolutePath()),
"hand_written"));
assertEquals(handWritten, new String(Files.readAllBytes(script), StandardCharsets.UTF_8));
assertEquals(List.of(Path.of("migrations/default/1.sql")), scripts());
}
}