diff --git a/docs/manual/working/javaGuide/main/sql/JavaEbean.md b/docs/manual/working/javaGuide/main/sql/JavaEbean.md index 6cfc7d35..4f3a1cb4 100644 --- a/docs/manual/working/javaGuide/main/sql/JavaEbean.md +++ b/docs/manual/working/javaGuide/main/sql/JavaEbean.md @@ -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..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//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..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//` 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//`. 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//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 diff --git a/play-ebean/src/main/java/play/db/ebean/EbeanDynamicEvolutions.java b/play-ebean/src/main/java/play/db/ebean/EbeanDynamicEvolutions.java index 6f44edd6..cc0a6a38 100644 --- a/play-ebean/src/main/java/play/db/ebean/EbeanDynamicEvolutions.java +++ b/play-ebean/src/main/java/play/db/ebean/EbeanDynamicEvolutions.java @@ -97,7 +97,7 @@ public void create() { return; } File evolutions = - environment.getFile( + evolutionsFile( Evolutions$.MODULE$.fileName( key, 1, evolutionsConfig.forDatasource(key).path())); try { @@ -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(); @@ -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. * diff --git a/play-ebean/src/test/java/play/db/ebean/EbeanEvolutionsScriptPathTest.java b/play-ebean/src/test/java/play/db/ebean/EbeanEvolutionsScriptPathTest.java new file mode 100644 index 00000000..722673ea --- /dev/null +++ b/play-ebean/src/test/java/play/db/ebean/EbeanEvolutionsScriptPathTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +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..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 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 scripts() throws IOException { + Path base = folder.getRoot().toPath(); + try (Stream files = Files.walk(base)) { + return files + .filter(file -> file.getFileName().toString().equals("1.sql")) + .map(base::relativize) + .collect(Collectors.toList()); + } + } + + private void assertGeneratesScript(Map 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()); + } +}