-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Refactor jdbc migration and use service loader #151
base: master
Are you sure you want to change the base?
Conversation
/** | ||
* Set the configuration being used. | ||
*/ | ||
void setMigrationConfig(MigrationConfig config); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is removed. I want the JdbcMigrations stateless
*/ | ||
void migrate(Connection connection); | ||
void migrate(Connection connection, MigrationConfig config); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This causes an API break.
I could avoid this by "default" methods, but then I think no one reads the changelog and the java migrations will not work anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. Yes I agree that people generally don't read release notes.
*/ | ||
boolean readResources() { | ||
if (readFromIndex()) { | ||
// automatically enable earlyChecksumMode when using index file with pre-computed checksums | ||
migrationConfig.setEarlyChecksumMode(true); | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've changed the semantic of "readFromIndex" here.
It returns true, if there is an index file, although if it is empty - so do not fall back to class path scan. We can still have java migrations
Hello @rbygrave, we did some tests with our staging apps and this would be the way we want to go. It simplifies also the code a lot, as we do not have a dedicated java class for each platform in the src/main/java/dbmigration/PLATFORM packages. Although by default, this PR uses the ServiceLoader (#90) we use a custom factory (see below) So this PR should make #90 and #149 obsolete. We also have a good workaround for Problem 2 mentioned in #150 Problem 1 in #150 is more theoretical. We regenerate the idx file on each release, so we should notice, when someone has touched existing migrations. In future, we can think about, if ebean-migration should validate the real checksum in the file against the one in the index file, when a migration is executed. So if we get this merged, we do not need a fork anymore and we are happy for now :) FYI: We use spring. And spring performs a "Component"/Classpath scan on startup. For the migration, we wrote a custom factory, that reuses this list and provides instances for the JdbcMigrations. This way we do not need an other Avaje classpath scan. We do not use the serviceLoader Roland |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting close. Looks like there are a few small things to look at here though.
- "Factory" is more a "collection of JdbcMigration"
- Should there be 1 more level of indirection in terms of service loading many JdbcMigration (I think so)
- Should there be an explicit getVersion() method on JdbcMigration
- Should there be a JdbcMigrationContext interface rather than connection + migrationConfig
*/ | ||
void migrate(Connection connection); | ||
void migrate(Connection connection, MigrationConfig config); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. Yes I agree that people generally don't read release notes.
while (iterator.hasNext()) { | ||
JdbcMigration jdbcMigration = iterator.next(); | ||
if (jdbcMigration.matches(migrationConfig)) { | ||
final var version = MigrationVersion.parse(jdbcMigration.getName()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure that getName() should be used for version. I'm thinking there should be an explicit getVersion() method on the JdbcMigration interface (that implementations must provide a version).
ebean-migration/src/main/java/io/ebean/migration/runner/LocalMigrationResources.java
Show resolved
Hide resolved
throw new IllegalArgumentException(className + " is not a valid JdbcMigration", e); | ||
} | ||
public Iterator<JdbcMigration> iterator() { | ||
return ServiceLoader.load(JdbcMigration.class, getClassLoader()).iterator(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about service loading JdbcMigration (each migration). I have in my mind that it would be better to add 1 level of indirection (a JdbcMigrationSupplier or JdbcMigrationFactory) ... then typically only that 1 supplier/factory would need to be registered for service loading rather than each JdbcMigration.
Hmmm.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have the first level of indirection in the migrationConfig. You can set your jdbcMigrationFactory/Collection by a property. So you can implement your own search strategy (we did this with our spring-classpath scan)
I think, just Service-loading the factory is not a good idea, if you have multiple ebean-servers to different databases with different schemas or even different platforms. So the jdbcMigrations have to be configured the same way as the jdbcMigration-directory
|
||
@Override | ||
public void migrate(Connection connection) { | ||
public void migrate(Connection connection, MigrationConfig config) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a more ideal API some interface would be here. It would be read-only (only getters/accessors).
e.g. JdbcMigrationContext
with methods like:
- Connection connection()
- Transaction transaction() // return the connection wrapped as a ebean transaction
- Platform platform() // ??
What is in MigrationConfig that JdbcMigration implementations need to access?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. We need connection
and platform
Don't know how you mean that with transaction. We have built a mechanism, that we have a "working" ebean-server:
private static final ThreadLocal<Database> DB_Threadlocal = new ThreadLocal();
public void migrate(final Connection connection, MigrationConfig config) {
SpiEbeanServer defaultServer = (SpiEbeanServer)DB_Threadlocal .get();
assert defaultServer != null;
TransactionManager transactionManager = (TransactionManager)defaultServer.transactionManager();
SpiTransaction txn = transactionManager.wrapExternalConnection(connection);
transactionManager.externalBeginTransaction(txn, TxScope.notSupported());
try {
this.migrate(defaultServer);
txn.flush();
} finally {
transactionManager.externalRemoveTransaction();
}
}
Let's focus on connection and platform at the first step. Maybe I'll extend the context in the next PR
@@ -418,17 +423,34 @@ public void setClassLoader(ClassLoader classLoader) { | |||
/** | |||
* Return the jdbcMigrationFactory. | |||
*/ | |||
public JdbcMigrationFactory getJdbcMigrationFactory() { | |||
public Iterable<JdbcMigration> getJdbcMigrationFactory() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have a name mismatch in that getJdbcMigrationFactory()
returns a "collection of migrations".
So I get it but it seems off. Migrations need to be sorted into version order etc. This doesn't quite look right yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree, I'll rename it to getJdbcMigrationCollection
Migrations need to be sorted into version order etc. This doesn't quite look right yet.
I don't get you here. They are sorted later here https://github.com/ebean-orm/ebean-migration/pull/151/files#diff-10ad52c1950c7a3f96d7f7286cee908ff73e79324b8414c53ce3ad17204ca9e1R57 and they have to be sorted between the SQL migrations
@rob-bygrave thanks for feedback. I'll update the PR the next days...
then we can discuss the left open points. Roland |
Yeah great. This is awesome by the way, thanks for taking this on!! |
I hope to look at this over the next day. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello @rbygrave I found some time to continue the work here.
This is not yet ready to merge, but I have one question, that we should clarify.
- Do you think, it is a good idea to use the plugin-api to inject the migration run process in ebean?
- Or should I better rely on the AutoMigrationRunner service, which means, that I have to change the
void run(DataSource dataSource)
to effectivelyvoid run(Database ebeanServer)
which means, that we will get a dependency to ebean here (and ebean has a dependency to AutoMigrationRunner)
public interface MigrationContextDb extends MigrationContext { | ||
public Transaction transaction(); | ||
|
||
public Database database(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CHECKME: Should I add "Transaction" and "Database" in MigrationContext (means that ebean-migration will get a dependency to ebean-api)
@Override | ||
public void configure(SpiServer server) { | ||
config.setName(server.name()); | ||
config.load(server.config().getProperties()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CHECKME:
Ebean transfers some values form the DatabaseConfig
final String dbSchema = config.getDbSchema();
migrationRunner.setDefaultDbSchema(dbSchema); // mig: schema, setCurrentSchema
Platform platform = config.getDatabasePlatform().platform();
migrationRunner.setBasePlatform(platform.base().name().toLowerCase()); // mig: basePlatform
migrationRunner.setPlatform(platform.name().toLowerCase()); // mig: platform or platformName
Would mean that some properties have to be migrated
@Override | ||
public void online(boolean online) { | ||
if (online && config.isPluginRun()) { | ||
new MigrationRunnerDb(config, server).run(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CHECKME: online might be too early. If we decide to implement this as plugin we might need some lifecycles in the Plugin api (e.g. a postOnline
method)
Hello @rbygrave The goal is, to avoid classpath-scanning and provide a mechnanism, that an application can define, how migrations are discovered. This can be done by
An other goal is, that the EbeanServer is accessible during JDBC-migrations. The current API provides only a connection. To make some progress here, I would clarify, to which API I should rely in the future.
Can you tell me, what are your preferences?
Side-note: We need to run migrations also during runtime, not only on startup. We use ebean in the MULTI_TENANT_DB_WITH_MASTER mode. So we have one table in our master DB, where all tenants are registered. So when we detect, that a new tenant was entered, we start a new connection pool with the credentials and run the migration for this tenant. |
Hello Rob,
I've tried to implement #90
This PR removes the old java migration class path scanning.
With this change JdbcMigrations are fully provided with JdbcMigrationFactory. This is implemented by default as ServiceLoader.
It is possible to specify your own factory (which could just be a list with migration instances)
We will test this the next day here in our code base and I'll give feedback if it works.
It would be great, if I get some feedback from you, if this is going in the right direction.
Roland