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
7 changes: 7 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ lazy val core = project
(Compile / classDirectory).value,
"play/db/ebean/**"
),
// The models of the tests need to be enhanced as well
Test / compile := enhanceEbeanClasses(
(Test / dependencyClasspath).value,
(Test / compile).value,
(Test / classDirectory).value,
"play/db/ebean/models/**"
),
)

lazy val plugin = project
Expand Down
21 changes: 18 additions & 3 deletions docs/manual/working/javaGuide/main/sql/JavaEbean.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,36 @@ Note that Ebean will also make use of a `conf/orm.xml` file (if present), to con

Ebean can run queries against a separate, read-only data source, typically a [read replica](https://ebean.io/docs/read-replicas/). This applies to queries executed outside of a transaction and to explicitly read-only transactions (e.g. `TxScope.required().setReadOnly(true)`). All writes and all queries inside of other transactions, including the ones of `@Transactional` actions, keep using the main data source.

To use a read replica, configure it as an additional Play database and map it to the Ebean server via `play.ebean.readOnlyDatasources`:
To use a read replica, configure it as an additional Play database and set it as the `readOnlyDatasource` of the Ebean server:

```properties
db.default.url = "jdbc:postgresql://primary-host/app"
db.replica.url = "jdbc:postgresql://replica-host/app"
db.replica.hikaricp.readOnly = true

ebean.default = ["models.*"]
play.ebean.readOnlyDatasources.default = "replica"
play.ebean.db.default.readOnlyDatasource = "replica"
```

The key is the name of the Ebean server (here `default`) and the value is the name of the Play database used for its read-only queries. Play manages the connection pool of the read replica like that of any other database, so all the usual `db` settings apply.
Here `default` is the name of the Ebean server and `replica` the name of the Play database used for its read-only queries. Play manages the connection pool of the read replica like that of any other database, so all the usual `db` settings apply.

Ebean supports one read-only data source per Ebean server. To spread the read-only queries across multiple read replicas, use a JDBC URL that lists all of them (if supported by your JDBC driver, e.g. the PostgreSQL driver's `loadBalanceHosts` and `targetServerType` parameters), or point the URL to a load balancer or the reader endpoint of your database service.

#### Generating evolution scripts

In dev and test mode, Play Ebean generates the evolution script `conf/evolutions/<server>/1.sql` of each Ebean server from its models, so that Play's [evolutions](https://www.playframework.com/documentation/latest/Evolutions) can create the database schema. Whenever the models change, Play Ebean updates the script, as long as it starts with the `-- Created by Ebean DDL` comment. To write the evolutions yourself, remove this comment (both lines), then Play Ebean doesn't touch the file anymore.

You can turn off generating the scripts for all Ebean servers, and override that for single Ebean servers:

```properties
# Don't generate the scripts of the Ebean servers...
play.ebean.generateEvolutionsScripts = false
# ...except the one of the "default" Ebean server
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.

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

#### Configuring the sbt plugin
Expand Down
29 changes: 24 additions & 5 deletions play-ebean/src/main/java/play/db/ebean/DefaultEbeanConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,24 @@ public class DefaultEbeanConfig implements EbeanConfig {

private final boolean generateEvolutionsScripts;

private final Map<String, Boolean> generateEvolutionsScriptsByServer;

public DefaultEbeanConfig(
String defaultServer,
Map<String, DatabaseConfig> serverConfigs,
boolean generateEvolutionsScripts) {
boolean generateEvolutionsScripts,
Map<String, Boolean> generateEvolutionsScriptsByServer) {
this.defaultServer = defaultServer;
this.serverConfigs = serverConfigs;
this.generateEvolutionsScripts = generateEvolutionsScripts;
this.generateEvolutionsScriptsByServer = generateEvolutionsScriptsByServer;
}

public DefaultEbeanConfig(
String defaultServer,
Map<String, DatabaseConfig> serverConfigs,
boolean generateEvolutionsScripts) {
this(defaultServer, serverConfigs, generateEvolutionsScripts, Collections.emptyMap());
}

public DefaultEbeanConfig(String defaultServer, Map<String, DatabaseConfig> serverConfigs) {
Expand All @@ -59,6 +70,11 @@ public boolean generateEvolutionsScripts() {
return generateEvolutionsScripts;
}

@Override
public boolean generateEvolutionsScripts(String server) {
return generateEvolutionsScriptsByServer.getOrDefault(server, generateEvolutionsScripts);
}

@Singleton
public static class EbeanConfigParser implements Provider<EbeanConfig> {

Expand Down Expand Up @@ -91,10 +107,12 @@ public EbeanConfig parse() {

EbeanParsedConfig ebeanConfig = EbeanParsedConfig.parseFromConfig(config);

for (String key : ebeanConfig.getReadOnlyDatasources().keySet()) {
Set<String> configuredServers = new HashSet<>(ebeanConfig.getReadOnlyDatasources().keySet());
configuredServers.addAll(ebeanConfig.getGenerateEvolutionsScriptsByServer().keySet());
for (String key : configuredServers) {
if (!ebeanConfig.getDatasourceModels().containsKey(key)) {
throw new ConfigException.BadValue(
"play.ebean.readOnlyDatasources." + key, "There is no Ebean server '" + key + "'");
"play.ebean.db." + key, "There is no Ebean server '" + key + "'");
}
}

Expand Down Expand Up @@ -132,7 +150,8 @@ public EbeanConfig parse() {
return new DefaultEbeanConfig(
ebeanConfig.getDefaultDatasource(),
serverConfigs,
ebeanConfig.generateEvolutionsScripts());
ebeanConfig.generateEvolutionsScripts(),
ebeanConfig.getGenerateEvolutionsScriptsByServer());
}

private void setServerConfigDataSource(String key, DatabaseConfig serverConfig) {
Expand All @@ -150,7 +169,7 @@ private void setServerConfigReadOnlyDataSource(
}
if (dbApi.getDatabase(readOnlyDatasource) == null) {
throw new ConfigException.BadValue(
"play.ebean.readOnlyDatasources." + key,
"play.ebean.db." + key + ".readOnlyDatasource",
"There is no Play database '" + readOnlyDatasource + "'");
}
// Not wrapped: Ebean's implicit read-only transactions work best with auto-commit
Expand Down
8 changes: 8 additions & 0 deletions play-ebean/src/main/java/play/db/ebean/EbeanConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,12 @@ public interface EbeanConfig {
Map<String, DatabaseConfig> serverConfigs();

boolean generateEvolutionsScripts();

/**
* @param server the name of the Ebean server
* @return whether to generate the evolutions script of the given Ebean server
*/
default boolean generateEvolutionsScripts(String server) {
return generateEvolutionsScripts();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ public void create() {
if (environment.isProd()) {
return;
}
if (!config.generateEvolutionsScripts()) {
return;
}
config
.serverConfigs()
.forEach(
(key, serverConfig) -> {
if (!config.generateEvolutionsScripts(key)) {
return;
}
String evolutionScript = generateEvolutionScript(databases.get(key));
if (evolutionScript == null) {
return;
Expand Down
50 changes: 45 additions & 5 deletions play-ebean/src/main/java/play/db/ebean/EbeanParsedConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,33 @@ public class EbeanParsedConfig {

private final boolean generateEvolutionsScripts;

private final Map<String, Boolean> generateEvolutionsScriptsByServer;

private final Map<String, String> readOnlyDatasources;

public EbeanParsedConfig(
String defaultDatasource,
Map<String, List<String>> datasourceModels,
boolean generateEvolutionsScripts,
Map<String, Boolean> generateEvolutionsScriptsByServer,
Map<String, String> readOnlyDatasources) {
this.defaultDatasource = defaultDatasource;
this.datasourceModels = datasourceModels;
this.generateEvolutionsScripts = generateEvolutionsScripts;
this.generateEvolutionsScriptsByServer = generateEvolutionsScriptsByServer;
this.readOnlyDatasources = readOnlyDatasources;
}

public EbeanParsedConfig(
String defaultDatasource,
Map<String, List<String>> datasourceModels,
boolean generateEvolutionsScripts) {
this(defaultDatasource, datasourceModels, generateEvolutionsScripts, Collections.emptyMap());
this(
defaultDatasource,
datasourceModels,
generateEvolutionsScripts,
Collections.emptyMap(),
Collections.emptyMap());
}

public EbeanParsedConfig(String defaultDatasource, Map<String, List<String>> datasourceModels) {
Expand All @@ -60,6 +69,22 @@ public boolean generateEvolutionsScripts() {
return generateEvolutionsScripts;
}

/**
* @param server the name of the Ebean server
* @return whether to generate the evolutions script of the given Ebean server
*/
public boolean generateEvolutionsScripts(String server) {
return generateEvolutionsScriptsByServer.getOrDefault(server, generateEvolutionsScripts);
}

/**
* @return the explicit settings whether to generate the evolutions scripts, keyed by Ebean server
* name.
*/
public Map<String, Boolean> getGenerateEvolutionsScriptsByServer() {
return generateEvolutionsScriptsByServer;
}

/**
* @return the names of the Play databases the Ebean servers use for read-only queries, keyed by
* Ebean server name.
Expand Down Expand Up @@ -100,14 +125,29 @@ public static EbeanParsedConfig parseFromConfig(Config config) {
datasourceModels.put(key, models);
});
}
Map<String, Boolean> generateEvolutionsScriptsByServer = new HashMap<>();
Map<String, String> readOnlyDatasources = new HashMap<>();
Config readOnlyDatasourcesConfig = playEbeanConfig.getConfig("readOnlyDatasources");
readOnlyDatasourcesConfig
Config serversConfig = playEbeanConfig.getConfig("db");
serversConfig
.root()
.keySet()
.forEach(key -> readOnlyDatasources.put(key, readOnlyDatasourcesConfig.getString(key)));
.forEach(
server -> {
Config serverConfig = serversConfig.getConfig(server);
if (serverConfig.hasPath("generateEvolutionsScripts")) {
generateEvolutionsScriptsByServer.put(
server, serverConfig.getBoolean("generateEvolutionsScripts"));
}
if (serverConfig.hasPath("readOnlyDatasource")) {
readOnlyDatasources.put(server, serverConfig.getString("readOnlyDatasource"));
}
});

return new EbeanParsedConfig(
defaultDatasource, datasourceModels, generateEvolutionsScripts, readOnlyDatasources);
defaultDatasource,
datasourceModels,
generateEvolutionsScripts,
generateEvolutionsScriptsByServer,
readOnlyDatasources);
}
}
14 changes: 9 additions & 5 deletions play-ebean/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ play {
# The name of the default ebean datasource
defaultDatasource = "default"

# The key to control the generation of the Evolutions scripts
# Whether to generate the evolutions scripts of the Ebean servers (in dev and test mode). Can be
# overridden for single Ebean servers, see `db` below.
generateEvolutionsScripts = true

# Maps Ebean servers to the Play databases their read-only queries should use (e.g. a read
# replica). For example, `default = "replica"` makes the default Ebean server run queries
# outside of transactions against the `replica` database.
readOnlyDatasources {}
# Settings of single Ebean servers, keyed by the name of the Ebean server:
# - readOnlyDatasource: the Play database the Ebean server should use for its read-only queries
# (e.g. a read replica). For example, `db.default.readOnlyDatasource = "replica"` makes the
# default Ebean server run queries outside of transactions against the `replica` database.
# - generateEvolutionsScripts: whether to generate the evolutions script of the Ebean server,
# overriding the global generateEvolutionsScripts setting above.
db {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;

import java.io.File;
import java.util.List;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import play.inject.guice.GuiceApplicationBuilder;
import play.test.Helpers;

public class EbeanEvolutionsScriptsTest {

@Rule public TemporaryFolder rootPath = new TemporaryFolder();

private GuiceApplicationBuilder appBuilder(Map<String, Object> config) {
return new GuiceApplicationBuilder()
.in(rootPath.getRoot())
.configure(
Map.of(
"db.first.driver", "org.h2.Driver",
"db.first.url", "jdbc:h2:mem:play-ebean-first",
"db.second.driver", "org.h2.Driver",
"db.second.url", "jdbc:h2:mem:play-ebean-second",
"ebean.first", List.of("play.db.ebean.models.first.*"),
"ebean.second", List.of("play.db.ebean.models.second.*"),
"play.ebean.defaultDatasource", "first",
// Generating the scripts doesn't depend on Play applying evolutions
"play.evolutions.enabled", false))
.configure(config);
}

/** Runs the application and returns for which Ebean servers it generated evolution scripts. */
private List<Boolean> generatedScripts(Map<String, Object> config) {
Helpers.running(appBuilder(config).build(), () -> {});
return List.of(scriptExists("first"), scriptExists("second"));
}

private boolean scriptExists(String server) {
return new File(rootPath.getRoot(), "conf/evolutions/" + server + "/1.sql").isFile();
}

@Test
public void generatesScriptsByDefault() {
assertEquals(List.of(true, true), generatedScripts(Map.of()));
}

@Test
public void doesNotGenerateScriptsWhenDisabled() {
assertEquals(
List.of(false, false),
generatedScripts(Map.of("play.ebean.generateEvolutionsScripts", false)));
}

@Test
public void doesNotGenerateScriptOfServerWhenDisabledForServer() {
assertEquals(
List.of(true, false),
generatedScripts(Map.of("play.ebean.db.second.generateEvolutionsScripts", false)));
}

@Test
public void generatesScriptOfServerWhenEnabledForServer() {
assertEquals(
List.of(false, true),
generatedScripts(
Map.of(
"play.ebean.generateEvolutionsScripts", false,
"play.ebean.db.second.generateEvolutionsScripts", true)));
}

@Test
public void failsForUnknownEbeanServer() {
GuiceApplicationBuilder builder =
appBuilder(Map.of("play.ebean.db.unknown.generateEvolutionsScripts", false));
Exception e = assertThrows(Exception.class, builder::build);
assertThat(e.getMessage(), containsString("play.ebean.db.unknown"));
assertThat(e.getMessage(), containsString("There is no Ebean server 'unknown'"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public void defaultConfig() {
assertThat(config.getDatasourceModels().size(), equalTo(0));
assertThat(config.generateEvolutionsScripts(), equalTo(Boolean.TRUE));
assertThat(config.getReadOnlyDatasources().size(), equalTo(0));
assertThat(config.getGenerateEvolutionsScriptsByServer().size(), equalTo(0));
}

@Test
Expand Down Expand Up @@ -61,10 +62,29 @@ public void disableGenerateEvolutionsScripts() {
assertThat(config.generateEvolutionsScripts(), equalTo(Boolean.FALSE));
}

@Test
public void disableGenerateEvolutionsScriptsForServer() {
EbeanParsedConfig config =
parse(ImmutableMap.of("play.ebean.db.other.generateEvolutionsScripts", false));
assertThat(config.generateEvolutionsScripts("default"), equalTo(Boolean.TRUE));
assertThat(config.generateEvolutionsScripts("other"), equalTo(Boolean.FALSE));
}

@Test
public void enableGenerateEvolutionsScriptsForServer() {
EbeanParsedConfig config =
parse(
ImmutableMap.of(
"play.ebean.generateEvolutionsScripts", false,
"play.ebean.db.other.generateEvolutionsScripts", true));
assertThat(config.generateEvolutionsScripts("default"), equalTo(Boolean.FALSE));
assertThat(config.generateEvolutionsScripts("other"), equalTo(Boolean.TRUE));
}

@Test
public void readOnlyDatasources() {
EbeanParsedConfig config =
parse(ImmutableMap.of("play.ebean.readOnlyDatasources.default", "replica"));
parse(ImmutableMap.of("play.ebean.db.default.readOnlyDatasource", "replica"));
assertThat(config.getReadOnlyDatasources().size(), equalTo(1));
assertThat(config.getReadOnlyDatasources().get("default"), equalTo("replica"));
}
Expand Down
Loading