ebean
ebean copied to clipboard
Provide API to run ddl and migration manually (e.g. with offline DB)
This change provides API methods to run DDL and migration manually. This is useful, if you have a setup where you
- use TenantMode.DB / SCHEMA or CATALOG
- expect, that the DB may be not available on startup
How to use
If you use a multi-tenancy mode, you will have to iterate over the available tenants and call dbMigration for each tenant. (You'll have to set the tenant in the currentTenantProvider
, so that the multiTenantDbSupplier will switch to the correct tenant
for (int tenant : tenants) {
currentTenantProvider.set(tenant)
db.runMigration();
}
Note: Ebean and Ebean-datasource will recover properly, if there are connection problems after the first successful connect. But the default configuration does not allow you, to construct a io.ebean.Database instance, when the first connect fails.
If you want to start an ebean server where the datasource could be unreachable, you will have to disable all DB access and checks at startup (datasource.failOnStart=false, skipDataSourceCheck=true, ddl.run=false and migration.run=false) in this case, ebean will also come up, if the database is not accessible. This allows you, to show a "Database not available" message when you start your application.
Sample code
If you use io.ebean.datasource
you may register a dataSourceAlert and do the migration when the database comes up the first time. Refer to the "TestServerOffline" or the following code draft, how to use this feature.
class MyDbFactory implements DataSourceAlert {
boolean initialized = false;
public Database create() {
...
dataSourceConfig.setAlert(this);
...
db = DatabaseFactory.create(config)
if (((ConnectionPool)db.dataSource()).isDataSourceUp()) {
boot();
}
return db;
}
void synchronized boot() { // synchronized: dataSourceUp is invoked from different thread
if (init) return;
db.runMigration();
init = true; // run only once after success
}
public void dataSourceUp(final DataSource dataSource) {
boot();
}
}
We use Ebean in multi tenant mode and here DDL/DbMigration has to be completely done by the application, because ebean does neither run DbMigration on start up, nor provide an API to initialize the database for a new tenant.
We have the use case, that tenants are created (and also removed) while the application runs. Our app performs the following steps:
- Admin enters the new DB credentials for the tenant in a web-form,
- The connection is registered in our MultiTenantDataSourceProvider
- we have a CurrentTenantProvider where we set the new tenant (a ThreadLocal) and run ddl-generation by invoking
server.start()
for this thread. - it is no option for us to do tenant initializion on ebean startup
I don't think they can become plugins due to ordering ... the ddl ought to run before any/all "plugins" are run etc
that contradicts itself when a multi-tenant mode is active. So plugins (at least the default ones) have to deal with that situation
For the "ordering" problem, I've introduced a new lifecycle method start
in Plugin, so that online
will called once, when the server is created and start
is called for each tenant. This will work for our use cases.
I also extracted DDL run and DbMigration run in separate plugins in our branch, so I've the following plugins in the classpath
-
DdlPlugin generates in the
execute
method the DDL and applies the DDL in thestart
method (for each tenant) -
DbMigrationPlugin generates the DbMigration in the
online
method. (for this, I've made DbMigration to be fully configurable from properties. See https://github.com/FOCONIS/ebean/commit/9d1f4194aa2d5a9c248f958bf633115caddc422b) - DbRunMigrationPlugin applies the DDL scripts
This would allow to remove all DdlGenerator and MigrationRun code from DefaultServer https://github.com/FOCONIS/ebean/commit/699f7a8449d09c84f73c0a4730a5ba31e0430b1b#diff-0804968369579e408eea483a9e582485cd7faf94e5d78dc8f7a918c729962a70 https://github.com/FOCONIS/ebean/commit/208e987ab542f83f377dd2957f0a7895f848f1da#diff-0804968369579e408eea483a9e582485cd7faf94e5d78dc8f7a918c729962a70
So if we introduce more lifecycle methods in the Plugin
interface we could achieve that we can offload all the DDL stuff from defaultserver.
How would I change the plugin interface:
public interface Plugin {
// called after Server is constructed. Tasks that can be done here
// - read configs etc from server
// - this.server = server
void configure(SpiServer server);
// called when the server comes 'online', means
// - all plugins are configured
// - you can use all features of the server, like background executor
// - you can not access the database (at least when multi-tenant mode is used)
void online(boolean online);
// called when the datasource is available. You may access the database the first time here
// this is called for each tenant.
default void start() { };
// called once, when the server is shut down
void shutdown();
}
Of course, I see the ordering problem. So we can either add an int order()
method and sort the plugins or add a third lifecycle method e.g. void initDatabase()
. (or do both). So the initializion workflow could look like this pseudocode
plugins = discoverPluginsViaServiceLocator()
sort(plugins, Plugin::order)
for (plugin : plugins)
plugin.configure(server)
for (plugin : plugins)
plugin.online(online)
for (tenant : tenants) // is done by application when multi tenant is active
server.start()
// this does effectively
for (plugin : plugins)
plugin.initDatabase() // migration plugins can apply DDL or whatever
for (plugin : plugins)
plugin.start() // can fully access the database
// when server stops
for (plugin : plugins)
plugin.shutdown()
When we add the default
methods, we would not break the Plugin-API.
We would only introduce the breaking change that the DB is not fully initialized in online
method (which is also the case when multi-tenant mode is used) - as far as I see the existing plugins will have no problems with that.
@rbygrave I sometimes find it difficult to convey the overall picture and may need some feedback if my text is understandable and what you think about this change. Maybe you can give me some tips, how I should proceed with this PR.
Note to myself: This is still on my to-do list ;)
This PR is probably obsolete