diff --git a/src/main/java/com/avaje/ebean/config/DbMigrationConfig.java b/src/main/java/com/avaje/ebean/config/DbMigrationConfig.java index 3783ba087..95b7dd301 100644 --- a/src/main/java/com/avaje/ebean/config/DbMigrationConfig.java +++ b/src/main/java/com/avaje/ebean/config/DbMigrationConfig.java @@ -1,15 +1,75 @@ package com.avaje.ebean.config; +import com.avaje.ebean.EbeanServer; +import com.avaje.ebean.config.dbplatform.DbPlatformName; +import com.avaje.ebean.dbmigration.DbMigration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Configuration for the DB migration processing. */ public class DbMigrationConfig { + protected static final Logger logger = LoggerFactory.getLogger(DbMigrationConfig.class); + + /** + * The database platform to generate migration DDL for. + */ + protected DbPlatformName platform; + + protected boolean useSubdirectories; + + /** + * Set to true if the DB migration should be generated on server start. + */ + protected boolean generate; + + /** + * The migration version name (typically FlywayDb compatible). + *
+ * Example: 1.1.1_2 + *
+ * The version is expected to be the combination of the current pom version plus + * a 'feature' id. The combined version must be unique and ordered to work with + * FlywayDb so each developer sets a unique version so that the migration script + * generated is unique (typically just prior to being submitted as a merge request). + */ + protected String version; + + /** + * Description text that can be appended to the version to become the ddl script file name. + *
+ * So if the name is "a foo table" then the ddl script file could be: + * "1.1.1_2__a-foo-table.sql" + *
+ * When the DB migration relates to a git feature (merge request) then this description text + * is a short description of the feature. + */ + protected String name; + /** * Resource path for the migration xml and sql. * Typically you would change 'app' to be a better/more unique. */ - private String resourcePath = "dbmigration/app"; + protected String resourcePath = "dbmigration/app"; + + /** + * Return the DB platform to generate migration DDL for. + * + * We typically need to explicitly specify this as migration can often be generated + * when running against H2. + */ + public DbPlatformName getPlatform() { + return platform; + } + + /** + * Set the DB platform to generate migration DDL for. + */ + public void setPlatform(DbPlatformName platform) { + this.platform = platform; + } /** * Return the resource path for db migrations. @@ -18,6 +78,20 @@ public class DbMigrationConfig { return resourcePath; } + /** + * Return true if the 'rollback' and 'drop' scripts should be put into subdirectories. + */ + public boolean isUseSubdirectories() { + return useSubdirectories; + } + + /** + * Set to true if the 'rollback' and 'drop' scripts should be put into subdirectories. + */ + public void setUseSubdirectories(boolean useSubdirectories) { + this.useSubdirectories = useSubdirectories; + } + /** * Set the resource path for db migrations. *
@@ -30,10 +104,134 @@ public class DbMigrationConfig { this.resourcePath = resourcePath; } + /** + * Set the migration version. + *
+ * Note that version set via System property or environment variable ddl.migration.version takes precedence.
+ */
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ /**
+ * Set the migration name.
+ *
+ * Note that name set via System property or environment variable ddl.migration.name takes precedence.
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
/**
* Load the settings from the PropertiesWrapper.
*/
public void loadSettings(PropertiesWrapper properties) {
resourcePath = properties.get("migration.resourcePath", resourcePath);
+ platform = properties.getEnum(DbPlatformName.class, "migration.platform", platform);
+ generate = properties.getBoolean("migration.generate", generate);
+ version = properties.get("migration.version", version);
+ name = properties.get("migration.name", name);
+ useSubdirectories = properties.getBoolean("migration.useSubdirectories", useSubdirectories);
}
+
+ /**
+ * Return true if the migration should be generated.
+ *
+ * It is expected that when an environment variable ddl.migration.enabled
+ * is set to true then the DB migration will generate the migration DDL.
+ *
+ * If enabled this generates the migration xml and DDL scripts. + *
+ */ + public void generateOnStart(EbeanServer server) { + + if (isGenerateOnStart()) { + if (platform == null) { + logger.warn("No platform set for migration DDL generation"); + } else { + // generate the migration xml and platform specific DDL + DbMigration migration = new DbMigration(server); + migration.setPlatform(platform); + try { + migration.generateMigration(); + } catch (Exception e) { + throw new RuntimeException("Error generating DB migration", e); + } + } + } + } + + /** + * Return the migration version (typically FlywayDb compatible). + *+ * Example: 1.1.1_2 + *
+ * The version is expected to be the combination of the current pom version plus + * a 'feature' id. The combined version must be unique and ordered to work with + * FlywayDb so each developer sets a unique version so that the migration script + * generated is unique (typically just prior to being submitted as a merge request). + */ + public String getVersion() { + String envVersion = readEnvironment("ddl.migration.version"); + if (!isEmpty(envVersion)) { + return envVersion.trim(); + } + return version; + } + + /** + * Return the migration name which is short description text that can be appended to + * the migration version to become the ddl script file name. + *
+ * So if the name is "a foo table" then the ddl script file could be: + * "1.1.1_2__a-foo-table.sql" + *
+ *+ * When the DB migration relates to a git feature (merge request) then this description text + * is a short description of the feature. + *
+ */ + public String getName() { + String envName = readEnvironment("ddl.migration.name"); + if (!isEmpty(envName)) { + return envName.trim(); + } + return name; + } + + /** + * Return the system or environment property. + */ + protected String readEnvironment(String key) { + + String val = System.getProperty(key); + if (val == null) { + val = System.getenv(key); + } + return val; + } + + /** + * Return true if the string is null or empty. + */ + protected boolean isEmpty(String val) { + return val == null || val.trim().isEmpty(); + } + } diff --git a/src/main/java/com/avaje/ebean/dbmigration/DbMigration.java b/src/main/java/com/avaje/ebean/dbmigration/DbMigration.java index 805a176c0..257cb00b2 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/DbMigration.java +++ b/src/main/java/com/avaje/ebean/dbmigration/DbMigration.java @@ -21,8 +21,8 @@ import com.avaje.ebean.dbmigration.model.CurrentModel; import com.avaje.ebean.dbmigration.model.MConfiguration; import com.avaje.ebean.dbmigration.model.MigrationModel; import com.avaje.ebean.dbmigration.model.ModelContainer; -import com.avaje.ebean.dbmigration.model.PlatformDdlWriter; import com.avaje.ebean.dbmigration.model.ModelDiff; +import com.avaje.ebean.dbmigration.model.PlatformDdlWriter; import com.avaje.ebeaninternal.api.SpiEbeanServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,6 +57,13 @@ public class DbMigration { protected static final Logger logger = LoggerFactory.getLogger(DbMigration.class); + private static final String initialVersion = "1.0"; + + /** + * Set to true if DbMigration run with online EbeanServer instance. + */ + protected final boolean online; + protected SpiEbeanServer server; protected DbMigrationConfig migrationConfig; @@ -71,7 +78,19 @@ public class DbMigration { protected DbConstraintNaming constraintNaming; + /** + * Create for offline migration generation. + */ public DbMigration() { + this.online = false; + } + + /** + * Create using online EbeanServer. + */ + public DbMigration(EbeanServer server) { + this.online = true; + setServer(server); } /** @@ -125,7 +144,9 @@ public class DbMigration { */ public void setPlatform(DatabasePlatform databasePlatform) { this.databasePlatform = databasePlatform; - DbOffline.setPlatform(databasePlatform.getName()); + if (!online) { + DbOffline.setPlatform(databasePlatform.getName()); + } } /** @@ -175,16 +196,18 @@ public class DbMigration { public void generateMigration() throws IOException { // use this flag to stop other plugins like full DDL generation - DbOffline.setRunningMigration(); + if (!online) { + DbOffline.setRunningMigration(); + } setDefaults(); try { - MigrationModel migrationModel = new MigrationModel(migrationConfig.getResourcePath()); - ModelContainer migrated = migrationModel.read(); - int nextMajorVersion = migrationModel.getNextMajorVersion(); - logger.info("next migration version {}", nextMajorVersion); + File migrationDirectory = getMigrationDirectory(); + + MigrationModel migrationModel = new MigrationModel(migrationDirectory); + ModelContainer migrated = migrationModel.read(); CurrentModel currentModel = new CurrentModel(server, constraintNaming); ModelContainer current = currentModel.read(); @@ -200,46 +223,88 @@ public class DbMigration { // there were actually changes to write Migration dbMigration = diff.getMigration(); - File writePath = getWritePath(); - logger.info("migration writing version {} to {}", nextMajorVersion, writePath.getAbsolutePath()); - writeMigrationXml(dbMigration, writePath, nextMajorVersion); + String fullVersion = getFullVersion(migrationModel); - if (databasePlatform != null) { - // writer needs the current model to provide table/column details for - // history ddl generation (triggers, history tables etc) - DdlWrite write = new DdlWrite(new MConfiguration(), currentModel.read()); - PlatformDdlWriter writer = new PlatformDdlWriter(databasePlatform, serverConfig); - writer.processMigration(dbMigration, write, writePath, nextMajorVersion); + logger.info("generating migration:{}", fullVersion); + if (!writeMigrationXml(dbMigration, migrationDirectory, fullVersion)) { + logger.warn("migration already exists, not generating DDL"); + + } else { + if (databasePlatform != null) { + // writer needs the current model to provide table/column details for + // history ddl generation (triggers, history tables etc) + DdlWrite write = new DdlWrite(new MConfiguration(), currentModel.read()); + PlatformDdlWriter writer = createDdlWriter(databasePlatform, ""); + writer.processMigration(dbMigration, write, migrationDirectory, fullVersion); + } + writeExtraPlatformDdl(fullVersion, currentModel, dbMigration, migrationDirectory); } - writeExtraPlatformDdl(nextMajorVersion, currentModel, dbMigration, writePath); - } finally { - DbOffline.reset(); + if (!online) { + DbOffline.reset(); + } } } + /** + * Return the full version for the migration being generated. + */ + private String getFullVersion(MigrationModel migrationModel) { + + String version = migrationConfig.getVersion(); + if (version == null) { + version = migrationModel.getNextVersion(initialVersion); + } + + String fullVersion = version; + + String name = migrationConfig.getName(); + if (name != null) { + fullVersion += "__" + toUnderScore(name); + } + return fullVersion; + } + + /** + * Replace spaces with underscores. + */ + private String toUnderScore(String name) { + return name.replace(' ','_'); + } + /** * Write any extra platform ddl. */ - protected void writeExtraPlatformDdl(int nextMajorVersion, CurrentModel currentModel, Migration dbMigration, File writePath) throws IOException { + protected void writeExtraPlatformDdl(String fullVersion, CurrentModel currentModel, Migration dbMigration, File writePath) throws IOException { for (Pair pair : platforms) { DdlWrite platformBuffer = new DdlWrite(new MConfiguration(), currentModel.read()); - - PlatformDdlWriter platformWriter = new PlatformDdlWriter(pair.platform, serverConfig, pair.prefix); - platformWriter.processMigration(dbMigration, platformBuffer, writePath, nextMajorVersion); + PlatformDdlWriter platformWriter = createDdlWriter(pair); + platformWriter.processMigration(dbMigration, platformBuffer, writePath, fullVersion); } } + private PlatformDdlWriter createDdlWriter(Pair pair) { + return createDdlWriter(pair.platform, pair.prefix); + } + + private PlatformDdlWriter createDdlWriter(DatabasePlatform platform, String prefix) { + return new PlatformDdlWriter(platform, serverConfig, prefix, migrationConfig.isUseSubdirectories()); + } + /** * Write the migration xml. */ - protected void writeMigrationXml(Migration dbMigration, File resourcePath, int migrationVersion) { + protected boolean writeMigrationXml(Migration dbMigration, File resourcePath, String fullVersion) { - File file = new File(resourcePath, "v"+migrationVersion+".0.xml"); + File file = new File(resourcePath, fullVersion+".xml"); + if (file.exists()) { + return false; + } MigrationXmlWriter xmlWriter = new MigrationXmlWriter(); xmlWriter.write(dbMigration, file); + return true; } /** @@ -260,7 +325,7 @@ public class DbMigration { /** * Return the file path to write the xml and sql to. */ - protected File getWritePath() { + protected File getMigrationDirectory() { // path to src/main/resources in typical maven project File resourceRootDir = new File(pathToResources); diff --git a/src/main/java/com/avaje/ebean/dbmigration/migrationreader/MigrationXmlReader.java b/src/main/java/com/avaje/ebean/dbmigration/migrationreader/MigrationXmlReader.java index 95d4df445..5fce3ba11 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/migrationreader/MigrationXmlReader.java +++ b/src/main/java/com/avaje/ebean/dbmigration/migrationreader/MigrationXmlReader.java @@ -6,6 +6,9 @@ import com.avaje.ebean.dbmigration.migration.Migration; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; import java.io.InputStream; /** @@ -15,19 +18,6 @@ public class MigrationXmlReader { private static final MigrationXmlReader INSTANCE = new MigrationXmlReader(); - /** - * Read and return a Migration from an xml document at the given resource path. - */ - public static Migration readMaybe(String resourcePath) { - - InputStream is = MigrationXmlReader.class.getResourceAsStream(resourcePath); - if (is == null) { - return null; - } - - return INSTANCE.read(is); - } - /** * Read and return a Migration from an xml document at the given resource path. */ @@ -41,10 +31,27 @@ public class MigrationXmlReader { return INSTANCE.read(is); } + /** + * Read and return a Migration from a migration xml file. + */ + public static Migration read(File migrationFile) { + + try { + FileInputStream is = new FileInputStream(migrationFile); + try { + return read(is); + } finally { + is.close(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + /** * Read and return a Migration from an xml document. */ - public Migration read(InputStream is) { + public static Migration read(InputStream is) { try { JAXBContext jaxbContext = JAXBContext.newInstance(Migration.class); diff --git a/src/main/java/com/avaje/ebean/dbmigration/model/MigrationModel.java b/src/main/java/com/avaje/ebean/dbmigration/model/MigrationModel.java index cd555a71a..aacdde839 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/model/MigrationModel.java +++ b/src/main/java/com/avaje/ebean/dbmigration/model/MigrationModel.java @@ -1,12 +1,13 @@ package com.avaje.ebean.dbmigration.model; -import com.avaje.ebean.dbmigration.migration.Migration; -import com.avaje.ebean.dbmigration.migrationreader.MigrationXmlReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.LinkedHashSet; -import java.util.Set; +import java.io.File; +import java.io.FileFilter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Build the model from the series of migrations. @@ -17,26 +18,12 @@ public class MigrationModel { private final ModelContainer model = new ModelContainer(); - private final Set