Provide API to run ddl and migration manually (e.g. with offline DB)

This commit is contained in:
Roland Praml
2021-12-17 12:04:27 +01:00
parent 68b7deba9d
commit eb6aacce4b
7 changed files with 249 additions and 44 deletions
@@ -1741,4 +1741,26 @@ public interface Database {
*/
void truncate(Class<?>... tables);
/**
* Runs the DDL manually. Can be used, if database is offline or if you use a
* TenantMode that does not support DDL run on startup.
* <p>
* Note: runDdl is normally executed at startup automatically, if
* <code>ebean.db.runDdl=true</code> is set. Calling this method, will not check
* that flag.
* </p>
*/
void runDdl();
/**
* Runs the migration manually. Can be used, if database is offline or if you use a
* TenantMode that does not support DDL run on startup.
* <p>
* Note: runDdl is normally executed at startup automatically, if
* <code>ebean.db.migration.run=true</code> is set. Calling this method, will not check
* that flag.
* </p>
**/
void runMigration();
}
@@ -8,8 +8,14 @@ public interface SpiDdlGenerator {
/**
* Generate and run the DDL for drop-all and create-all scripts.
* <p>
* Run based on on property settings for ebean.ddl.generate and ebean.ddl.run etc.
* Run based on on property settings for ebean.ddl.generate
*/
void execute(boolean online);
void generateDdl();
/**
* Runs the created DDL files.
* <p>
* Run based on on property settings for ebean.ddl.run etc.
*/
void runDdl();
}
@@ -246,7 +246,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
*/
public void executePlugins(boolean online) {
if (!config.isDocStoreOnly()) {
ddlGenerator.execute(online);
ddlGenerator.generateDdl();
}
for (Plugin plugin : serverPlugins) {
plugin.online(online);
@@ -359,27 +359,54 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
serverCacheManager.enabledRegions(config.getEnabledL2Regions());
}
/**
* Start any services after registering with the ClusterManager.
*/
public void start() {
if (config.isRunMigration() && TenantMode.DB != config.getTenantMode()) {
final AutoMigrationRunner migrationRunner = ServiceUtil.service(AutoMigrationRunner.class);
if (migrationRunner == null) {
throw new IllegalStateException("No AutoMigrationRunner found. Probably ebean-migration is not in the classpath?");
// check, if DB is available to run DDL and migration
boolean dbAvailable = !config.isDocStoreOnly()
&& !config.isDbOffline()
&& config.getDataSource() != null
&& config.getTenantMode().isDdlEnabled();
if (dbAvailable) {
if (config.isDdlRun()) {
runDdl();
}
final String dbSchema = config.getDbSchema();
if (dbSchema != null) {
migrationRunner.setDefaultDbSchema(dbSchema);
if (config.isRunMigration()) {
runMigration();
}
migrationRunner.setName(config.getName());
migrationRunner.setPlatform(config.getDatabasePlatform().getPlatform().base().name().toLowerCase());
migrationRunner.loadProperties(config.getProperties());
migrationRunner.run(config.getDataSource());
} else if (config.isDdlRun() || config.isRunMigration()) {
log.warn("There was a request to run DDL or migration, but this is currently not possible, as the database is not available");
}
startQueryPlanCapture();
}
@Override
public void runDdl() {
ddlGenerator.runDdl();
}
@Override
public void runMigration() {
final AutoMigrationRunner migrationRunner = ServiceUtil.service(AutoMigrationRunner.class);
if (migrationRunner == null) {
throw new IllegalStateException(
"No AutoMigrationRunner found. Probably ebean-migration is not in the classpath?");
}
final String dbSchema = config.getDbSchema();
if (dbSchema != null) {
migrationRunner.setDefaultDbSchema(dbSchema);
}
migrationRunner.setName(config.getName());
migrationRunner.setPlatform(config.getDatabasePlatform().getPlatform().base().name().toLowerCase());
migrationRunner.loadProperties(config.getProperties());
migrationRunner.run(dataSource());
}
private void startQueryPlanCapture() {
if (config.isQueryPlanCapture()) {
long secs = config.getQueryPlanCapturePeriodSecs();
@@ -610,8 +610,13 @@ public final class InternalConfiguration {
}
@Override
public void execute(boolean online) {
if (online && ddlRun) {
public void generateDdl() {
// do nothing
}
@Override
public void runDdl() {
if (ddlRun) {
CoreLog.log.error("Configured to run DDL but ebean-ddl-generator is not in the classpath (or ebean-test in the test classpath?)");
}
}
@@ -41,7 +41,6 @@ public class DdlGenerator implements SpiDdlGenerator {
private final SpiEbeanServer server;
private final boolean generateDdl;
private final boolean runDdl;
private final boolean extraDdl;
private final boolean createOnly;
private final boolean jaxbPresent;
@@ -67,14 +66,7 @@ public class DdlGenerator implements SpiDdlGenerator {
final DatabasePlatform databasePlatform = server.databasePlatform();
this.platform = databasePlatform.getPlatform();
this.platformName = platform.base().name();
if (!config.getTenantMode().isDdlEnabled() && config.isDdlRun()) {
log.warn("DDL can't be run on startup with TenantMode " + config.getTenantMode());
this.runDdl = false;
this.ddlAutoCommit = false;
} else {
this.runDdl = config.isDdlRun();
this.ddlAutoCommit = databasePlatform.isDdlAutoCommit();
}
this.ddlAutoCommit = databasePlatform.isDdlAutoCommit();
this.scriptTransform = createScriptTransform(config);
this.baseDir = initBaseDir();
}
@@ -89,18 +81,11 @@ public class DdlGenerator implements SpiDdlGenerator {
return new File(".");
}
@Override
public void execute(boolean online) {
generateDdl();
if (online) {
runDdl();
}
}
/**
* Generate the DDL drop and create scripts if the properties have been set.
*/
protected void generateDdl() {
@Override
public void generateDdl() {
if (generateDdl) {
if (!createOnly) {
writeDrop(getDropFileName());
@@ -112,16 +97,15 @@ public class DdlGenerator implements SpiDdlGenerator {
/**
* Run the DDL drop and DDL create scripts if properties have been set.
*/
protected void runDdl() {
if (runDdl) {
Connection connection = null;
try {
connection = obtainConnection();
runDdlWith(connection);
} finally {
JdbcClose.rollback(connection);
JdbcClose.close(connection);
}
@Override
public void runDdl() {
Connection connection = null;
try {
connection = obtainConnection();
runDdlWith(connection);
} finally {
JdbcClose.rollback(connection);
JdbcClose.close(connection);
}
}
@@ -0,0 +1,151 @@
package io.ebean.config;
import io.ebean.Database;
import io.ebean.DatabaseFactory;
import io.ebean.annotation.ForPlatform;
import io.ebean.annotation.Platform;
import io.ebean.datasource.DataSourceAlert;
import io.ebean.datasource.DataSourceInitialiseException;
import org.junit.jupiter.api.Test;
import org.tests.model.basic.EBasicVer;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import javax.persistence.PersistenceException;
import javax.sql.DataSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class TestServerOffline {
@Test
@ForPlatform({Platform.H2})
public void testOffline_default() throws SQLException {
String url = "jdbc:h2:mem:testoffline1";
try (Connection bootup = DriverManager.getConnection(url, "sa", "secret")) {
Properties props = props(url);
DatabaseConfig config = config(props);
assertThatThrownBy(() -> DatabaseFactory.create(config))
.isInstanceOf(DataSourceInitialiseException.class);
}
}
private static class LazyDatasourceInitializer implements DataSourceAlert {
public Database server;
private boolean initialized;
@Override
public void dataSourceUp(DataSource dataSource) {
if (!initialized) {
initDatabase();
}
}
public synchronized void initDatabase() {
if (!initialized) {
server.runDdl();
server.runMigration();
initialized = true;
}
}
@Override
public void dataSourceDown(DataSource dataSource, SQLException reason) {}
@Override
public void dataSourceWarning(DataSource dataSource, String msg) {}
}
@Test
@ForPlatform({Platform.H2})
public void testOffline_recovery() throws SQLException {
String url = "jdbc:h2:mem:testoffline3";
try (Connection bootup = DriverManager.getConnection(url, "sa", "secret")) {
Properties props = props(url);
// to bring up ebean without a database, we must disable varous things
// that happen on startup
props.setProperty("datasource.h2_offline.failOnStart", "false");
props.setProperty("ebean.h2_offline.skipDataSourceCheck", "true");
props.setProperty("ebean.h2_offline.ddl.run", "false");
DatabaseConfig config = config(props);
LazyDatasourceInitializer alert = new LazyDatasourceInitializer() ;
config.getDataSourceConfig().setAlert(alert);
config.getDataSourceConfig().setHeartbeatFreqSecs(1);
Database h2Offline = DatabaseFactory.create(config);
alert.server = h2Offline;
assertThat(h2Offline).isNotNull();
// DB is online now in offline mode
// Accessing the DB will throw a PE
assertThatThrownBy(() -> alert.initDatabase())
.isInstanceOf(PersistenceException.class)
.hasMessageContaining("Failed to obtain connection to run DDL");
assertThatThrownBy(() -> h2Offline.find(EBasicVer.class).findCount()).isInstanceOf(PersistenceException.class);
// so - reset the password so that the server can reconnect
try (Statement stmt = bootup.createStatement()) {
stmt.execute("alter user sa set password 'sa'");
}
assertThat(alert.initialized).isFalse();
// next access to ebean should bring DS online
h2Offline.find(EBasicVer.class).findCount();
assertThat(alert.initialized).isTrue();
// check if server is working (ie ddl was run)
EBasicVer bean = new EBasicVer("foo");
h2Offline.save(bean);
assertThat(h2Offline.find(EBasicVer.class).findCount()).isEqualTo(1);
h2Offline.delete(bean);
}
}
private Properties props(String url) {
Properties props = new Properties();
props.setProperty("datasource.h2_offline.username", "sa");
props.setProperty("datasource.h2_offline.password", "sa");
props.setProperty("datasource.h2_offline.url", url);
props.setProperty("datasource.h2_offline.driver", "org.h2.Driver");
props.setProperty("ebean.h2_offline.databasePlatformName", "h2");
props.setProperty("ebean.h2_offline.ddl.extra", "false");
props.setProperty("ebean.h2_offline.ddl.generate", "true");
props.setProperty("ebean.h2_offline.ddl.run", "true");
return props;
}
private DatabaseConfig config(Properties props) {
DatabaseConfig config = new DatabaseConfig();
config.setName("h2_offline");
config.loadFromProperties(props);
config.setDefaultServer(false);
config.setRegister(false);
config.getClasses().add(EBasicVer.class);
return config;
}
}
@@ -642,4 +642,14 @@ public class TDSpiServer implements SpiServer {
public void loadBean(EntityBeanIntercept ebi) {
}
@Override
public void runDdl() {
}
@Override
public void runMigration() {
}
}