mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
#711 - Refactor: Extract DB Migration runner as separate module (called avaje-dbmigration)
This commit is contained in:
@@ -3,6 +3,8 @@ 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.avaje.dbmigration.MigrationConfig;
|
||||
import org.avaje.dbmigration.MigrationRunner;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -494,4 +496,20 @@ public class DbMigrationConfig {
|
||||
return val == null || val.trim().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the MigrationRunner to run migrations if necessary.
|
||||
*/
|
||||
public MigrationRunner createRunner(ClassLoader classLoader) {
|
||||
|
||||
MigrationConfig runnerConfig = new MigrationConfig();
|
||||
runnerConfig.setMetaTable(metaTable);
|
||||
runnerConfig.setApplySuffix(applySuffix);
|
||||
runnerConfig.setMigrationPath(migrationPath);
|
||||
runnerConfig.setRunPlaceholderMap(runPlaceholderMap);
|
||||
runnerConfig.setRunPlaceholders(runPlaceholders);
|
||||
runnerConfig.setDbUsername(getDbUser());
|
||||
runnerConfig.setDbPassword(getDbPassword());
|
||||
runnerConfig.setClassLoader(classLoader);
|
||||
return new MigrationRunner(runnerConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.avaje.ebean.config.ServerConfig;
|
||||
import com.avaje.ebean.dbmigration.model.CurrentModel;
|
||||
import com.avaje.ebeaninternal.api.SpiEbeanServer;
|
||||
import com.avaje.ebeaninternal.extraddl.model.ExtraDdlXmlReader;
|
||||
import org.avaje.dbmigration.ddl.DdlRunner;
|
||||
|
||||
import javax.persistence.PersistenceException;
|
||||
import java.io.File;
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
package com.avaje.ebean.dbmigration;
|
||||
|
||||
import javax.persistence.PersistenceException;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Parses string content into separate SQL/DDL statements.
|
||||
*/
|
||||
public class DdlParser {
|
||||
|
||||
/**
|
||||
* Break up the sql in reader into a list of statements using the semi-colon and $$ delimiters;
|
||||
*/
|
||||
public List<String> parse(StringReader reader) {
|
||||
|
||||
try {
|
||||
BufferedReader br = new BufferedReader(reader);
|
||||
StatementsSeparator statements = new StatementsSeparator();
|
||||
|
||||
String s;
|
||||
while ((s = br.readLine()) != null) {
|
||||
s = s.trim();
|
||||
statements.nextLine(s);
|
||||
}
|
||||
|
||||
return statements.statements;
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new PersistenceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Local utility used to detect the end of statements / separate statements.
|
||||
* This is often just the semicolon character but for trigger/procedures this
|
||||
* detects the $$ demarcation used in the history DDL generation for MySql and
|
||||
* Postgres.
|
||||
*/
|
||||
static class StatementsSeparator {
|
||||
|
||||
ArrayList<String> statements = new ArrayList<String>();
|
||||
|
||||
boolean trimDelimiter;
|
||||
|
||||
boolean inDbProcedure;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
void lineContainsDollars(String line) {
|
||||
if (inDbProcedure) {
|
||||
if (trimDelimiter) {
|
||||
line = line.replace("$$","");
|
||||
}
|
||||
endOfStatement(line);
|
||||
} else {
|
||||
// MySql style delimiter needs to be trimmed/removed
|
||||
trimDelimiter = line.equals("delimiter $$");
|
||||
if (!trimDelimiter) {
|
||||
sb.append(line).append(" ");
|
||||
}
|
||||
}
|
||||
inDbProcedure = !inDbProcedure;
|
||||
}
|
||||
|
||||
void endOfStatement(String line) {
|
||||
// end of Db procedure
|
||||
sb.append(line);
|
||||
statements.add(sb.toString().trim());
|
||||
sb = new StringBuilder();
|
||||
}
|
||||
|
||||
void nextLine(String line) {
|
||||
|
||||
if (line.contains("$$")) {
|
||||
lineContainsDollars(line);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sb.length() == 0 && (line.isEmpty() || line.startsWith("--"))) {
|
||||
// ignore leading empty lines and sql comments
|
||||
return;
|
||||
}
|
||||
|
||||
if (inDbProcedure) {
|
||||
sb.append(line).append(" ");
|
||||
return;
|
||||
}
|
||||
|
||||
int semiPos = line.indexOf(';');
|
||||
if (semiPos == -1) {
|
||||
sb.append(line).append(" ");
|
||||
|
||||
} else if (semiPos == line.length() - 1) {
|
||||
// semicolon at end of line
|
||||
endOfStatement(line);
|
||||
|
||||
} else {
|
||||
// semicolon in middle of line
|
||||
String preSemi = line.substring(0, semiPos);
|
||||
endOfStatement(preSemi);
|
||||
sb.append(line.substring(semiPos + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package com.avaje.ebean.dbmigration;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Runs DDL scripts.
|
||||
*/
|
||||
public class DdlRunner {
|
||||
|
||||
protected static final Logger logger = LoggerFactory.getLogger(DdlRunner.class);
|
||||
|
||||
private DdlParser ddlParser = new DdlParser();
|
||||
|
||||
private final String scriptName;
|
||||
|
||||
private final boolean expectErrors;
|
||||
|
||||
/**
|
||||
* Construct with a script name (for logging) and flag indicating if errors are expected.
|
||||
*/
|
||||
public DdlRunner(boolean expectErrors, String scriptName) {
|
||||
this.expectErrors = expectErrors;
|
||||
this.scriptName = scriptName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the content into sql statements and execute them in a transaction.
|
||||
*/
|
||||
public int runAll(String content, Connection connection) throws SQLException {
|
||||
|
||||
List<String> statements = ddlParser.parse(new StringReader(content));
|
||||
return runStatements(statements, connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all the statements in a single transaction.
|
||||
*/
|
||||
private int runStatements(List<String> statements, Connection connection) throws SQLException {
|
||||
|
||||
List<String> noDuplicates = new ArrayList<String>();
|
||||
|
||||
for (String statement : statements) {
|
||||
if (!noDuplicates.contains(statement)) {
|
||||
noDuplicates.add(statement);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Executing {} - {} statements", scriptName, noDuplicates.size());
|
||||
|
||||
for (int i = 0; i < noDuplicates.size(); i++) {
|
||||
String xOfy = (i + 1) + " of " + noDuplicates.size();
|
||||
runStatement(expectErrors, xOfy, noDuplicates.get(i), connection);
|
||||
}
|
||||
|
||||
return noDuplicates.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the statement.
|
||||
*/
|
||||
private void runStatement(boolean expectErrors, String oneOf, String stmt, Connection c) throws SQLException {
|
||||
|
||||
PreparedStatement pstmt = null;
|
||||
try {
|
||||
|
||||
// trim and remove trailing ; or /
|
||||
stmt = stmt.trim();
|
||||
if (stmt.endsWith(";")) {
|
||||
stmt = stmt.substring(0, stmt.length() - 1);
|
||||
} else if (stmt.endsWith("/")) {
|
||||
stmt = stmt.substring(0, stmt.length() - 1);
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("executing " + oneOf + " " + getSummary(stmt));
|
||||
}
|
||||
|
||||
pstmt = c.prepareStatement(stmt);
|
||||
pstmt.execute();
|
||||
|
||||
} catch (SQLException e) {
|
||||
if (expectErrors) {
|
||||
logger.debug(" ... ignoring error executing " + getSummary(stmt) + " error: " + e.getMessage());
|
||||
} else {
|
||||
String msg = "Error executing stmt[" + stmt + "] error[" + e.getMessage() + "]";
|
||||
throw new SQLException(msg, e);
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (pstmt != null) {
|
||||
try {
|
||||
pstmt.close();
|
||||
} catch (SQLException e) {
|
||||
logger.error("Error closing pstmt", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getSummary(String s) {
|
||||
if (s.length() > 80) {
|
||||
return s.substring(0, 80).trim() + "...";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package com.avaje.ebean.dbmigration;
|
||||
|
||||
import com.avaje.ebean.EbeanServer;
|
||||
import com.avaje.ebean.config.DbMigrationConfig;
|
||||
import com.avaje.ebean.config.ServerConfig;
|
||||
import com.avaje.ebean.dbmigration.runner.LocalMigrationResource;
|
||||
import com.avaje.ebean.dbmigration.runner.LocalMigrationResources;
|
||||
import com.avaje.ebean.dbmigration.runner.MigrationTable;
|
||||
import com.avaje.ebeaninternal.util.JdbcClose;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.IOException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Runs the DB migration typically on application start.
|
||||
*/
|
||||
public class MigrationRunner {
|
||||
|
||||
public static final Logger logger = LoggerFactory.getLogger("org.avaje.ebean.DbMigration");
|
||||
|
||||
private final EbeanServer server;
|
||||
|
||||
private final ServerConfig config;
|
||||
|
||||
private final DbMigrationConfig migrationConfig;
|
||||
|
||||
public MigrationRunner(EbeanServer server, DbMigrationConfig migrationConfig) {
|
||||
this.server = server;
|
||||
this.config = server.getPluginApi().getServerConfig();
|
||||
this.migrationConfig = migrationConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the migrations if there are any that need running.
|
||||
*/
|
||||
public void run() {
|
||||
|
||||
LocalMigrationResources resources = new LocalMigrationResources(config, migrationConfig);
|
||||
if (!resources.readResources()) {
|
||||
logger.debug("no migrations to check");
|
||||
return;
|
||||
}
|
||||
|
||||
String migrationUser = migrationConfig.getDbUser();
|
||||
String migrationPwd = migrationConfig.getDbPassword();
|
||||
if (migrationUser == null) {
|
||||
throw new IllegalStateException("No DB migration user specified (to run the db migration) ?");
|
||||
}
|
||||
|
||||
DataSource dataSource = server.getPluginApi().getDataSource();
|
||||
if (dataSource == null) {
|
||||
throw new IllegalStateException("No dataSource when trying to run migration? "
|
||||
+"Maybe trying to generate DBMigration when ebean.migration.run=true is set? "
|
||||
+"Perhaps need to set ebean.migration.run=false in test-ebean.properties?");
|
||||
}
|
||||
|
||||
Connection connection;
|
||||
try {
|
||||
connection = dataSource.getConnection(migrationUser, migrationPwd);
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalArgumentException("Error trying to connect to database using DB Migration user [" + migrationUser + "]", e);
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug("using db user [{}] to run migrations ...");
|
||||
connection.setAutoCommit(false);
|
||||
runMigrations(resources, connection);
|
||||
|
||||
connection.commit();
|
||||
|
||||
} catch (Exception e) {
|
||||
JdbcClose.rollback(connection);
|
||||
throw new RuntimeException(e);
|
||||
|
||||
} finally {
|
||||
JdbcClose.close(connection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all the migrations as needed.
|
||||
*/
|
||||
private void runMigrations(LocalMigrationResources resources, Connection connection) throws SQLException, IOException {
|
||||
|
||||
MigrationTable table = new MigrationTable(server, migrationConfig, connection);
|
||||
table.createIfNeeded();
|
||||
|
||||
// get the migrations in version order
|
||||
List<LocalMigrationResource> localVersions = resources.getVersions();
|
||||
|
||||
logger.info("local migrations:{} existing migrations:{}", localVersions.size(), table.size());
|
||||
|
||||
LocalMigrationResource priorVersion = null;
|
||||
|
||||
// run migrations in order
|
||||
for (int i = 0; i < localVersions.size(); i++) {
|
||||
LocalMigrationResource localVersion = localVersions.get(i);
|
||||
if (!table.shouldRun(localVersion, priorVersion)) {
|
||||
break;
|
||||
}
|
||||
priorVersion = localVersion;
|
||||
connection.commit();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.avaje.ebean.dbmigration.runner;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
/**
|
||||
* Calculates the checksum for the given string content.
|
||||
*/
|
||||
public class Checksum {
|
||||
|
||||
/**
|
||||
* Returns the checksum of this string.
|
||||
*/
|
||||
public static int calculate(String str) {
|
||||
|
||||
final CRC32 crc32 = new CRC32();
|
||||
|
||||
BufferedReader bufferedReader = new BufferedReader(new StringReader(str));
|
||||
try {
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
crc32.update(line.getBytes("UTF-8"));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to calculate checksum", e);
|
||||
}
|
||||
|
||||
return (int) crc32.getValue();
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package com.avaje.ebean.dbmigration.runner;
|
||||
|
||||
import com.avaje.ebean.dbmigration.model.MigrationVersion;
|
||||
import org.avaje.classpath.scanner.Resource;
|
||||
|
||||
/**
|
||||
* A DB migration resource (DDL script with version).
|
||||
*/
|
||||
public class LocalMigrationResource implements Comparable<LocalMigrationResource> {
|
||||
|
||||
/**
|
||||
* Code for repeatable migrations.
|
||||
*/
|
||||
private static final String REPEAT_TYPE = "R";
|
||||
|
||||
/**
|
||||
* Code for version migrations.
|
||||
*/
|
||||
private static final String VERSION_TYPE = "V";
|
||||
|
||||
private final MigrationVersion version;
|
||||
|
||||
private final String location;
|
||||
|
||||
private final Resource resource;
|
||||
|
||||
/**
|
||||
* Construct with version and resource.
|
||||
*/
|
||||
public LocalMigrationResource(MigrationVersion version, String location, Resource resource) {
|
||||
this.version = version;
|
||||
this.location = location;
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return version.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the underlying version is "repeatable".
|
||||
*/
|
||||
public boolean isRepeatable() {
|
||||
return version.isRepeatable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the "key" that identifies the migration.
|
||||
*/
|
||||
public String key() {
|
||||
if (isRepeatable()) {
|
||||
return version.getComment().toLowerCase();
|
||||
} else {
|
||||
return version.normalised();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the migration comment.
|
||||
*/
|
||||
public String getComment() {
|
||||
String comment = version.getComment();
|
||||
return (comment == null || comment.isEmpty()) ? "-" : comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default ordering by version.
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(LocalMigrationResource o) {
|
||||
return version.compareTo(o.version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the underlying migration version.
|
||||
*/
|
||||
public MigrationVersion getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the resource location.
|
||||
*/
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the content for the migration apply ddl script.
|
||||
*/
|
||||
public String getContent() {
|
||||
return resource.loadAsString("UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type code ("R" or "V") for this migration.
|
||||
*/
|
||||
public String getType() {
|
||||
return isRepeatable() ? REPEAT_TYPE : VERSION_TYPE;
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package com.avaje.ebean.dbmigration.runner;
|
||||
|
||||
import com.avaje.ebean.config.DbMigrationConfig;
|
||||
import com.avaje.ebean.config.ServerConfig;
|
||||
import com.avaje.ebean.dbmigration.model.MigrationVersion;
|
||||
import org.avaje.classpath.scanner.Resource;
|
||||
import org.avaje.classpath.scanner.ResourceFilter;
|
||||
import org.avaje.classpath.scanner.Scanner;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Loads the DB migration resources and sorts them into execution order.
|
||||
*/
|
||||
public class LocalMigrationResources {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(LocalMigrationResources.class);
|
||||
|
||||
private final ServerConfig serverConfig;
|
||||
|
||||
private final DbMigrationConfig migrationConfig;
|
||||
|
||||
private final List<LocalMigrationResource> versions = new ArrayList<LocalMigrationResource>();
|
||||
|
||||
/**
|
||||
* Construct with configuration options.
|
||||
*/
|
||||
public LocalMigrationResources(ServerConfig serverConfig, DbMigrationConfig migrationConfig) {
|
||||
this.serverConfig = serverConfig;
|
||||
this.migrationConfig = migrationConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all the migration resources (SQL scripts) returning true if there are versions.
|
||||
*/
|
||||
public boolean readResources() {
|
||||
|
||||
String migrationPath = migrationConfig.getMigrationPath();
|
||||
|
||||
ClassLoader classLoader = serverConfig.getClassLoadConfig().getClassLoader();
|
||||
|
||||
Scanner scanner = new Scanner(classLoader);
|
||||
List<Resource> resourceList = scanner.scanForResources(migrationPath, new Match(migrationConfig));
|
||||
|
||||
logger.debug("resources: {}", resourceList);
|
||||
|
||||
for (Resource resource : resourceList) {
|
||||
String filename = resource.getFilename();
|
||||
if (filename.endsWith(migrationConfig.getApplySuffix())) {
|
||||
int pos = filename.lastIndexOf(migrationConfig.getApplySuffix());
|
||||
String mainName = filename.substring(0, pos);
|
||||
|
||||
MigrationVersion migrationVersion = MigrationVersion.parse(mainName);
|
||||
LocalMigrationResource res = new LocalMigrationResource(migrationVersion, resource.getLocation(), resource);
|
||||
versions.add(res);
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(versions);
|
||||
return !versions.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of migration resources in version order.
|
||||
*/
|
||||
public List<LocalMigrationResource> getVersions() {
|
||||
return versions;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter used to find the migration scripts.
|
||||
*/
|
||||
private static class Match implements ResourceFilter {
|
||||
|
||||
private final DbMigrationConfig migrationConfig;
|
||||
|
||||
Match(DbMigrationConfig migrationConfig) {
|
||||
this.migrationConfig = migrationConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMatch(String name) {
|
||||
return name.endsWith(migrationConfig.getApplySuffix());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package com.avaje.ebean.dbmigration.runner;
|
||||
|
||||
import com.avaje.ebean.SqlRow;
|
||||
import com.avaje.ebean.SqlUpdate;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
|
||||
/**
|
||||
* Bean holding migration execution details stored in the migration table.
|
||||
*/
|
||||
class MigrationMetaRow {
|
||||
|
||||
private int id;
|
||||
|
||||
private String type;
|
||||
|
||||
private String version;
|
||||
|
||||
private String comment;
|
||||
|
||||
private int checksum;
|
||||
|
||||
private Timestamp runOn;
|
||||
|
||||
private String runBy;
|
||||
|
||||
private long runTime;
|
||||
|
||||
/**
|
||||
* Construct for inserting into table.
|
||||
*/
|
||||
MigrationMetaRow(int id, String type, String version, String comment, int checksum, String runBy, Timestamp runOn, long runTime) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.version = version;
|
||||
this.checksum = checksum;
|
||||
this.comment = comment;
|
||||
this.runBy = runBy;
|
||||
this.runOn = runOn;
|
||||
this.runTime = runTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct from the SqlRow (read from table).
|
||||
*/
|
||||
MigrationMetaRow(SqlRow row) {
|
||||
id = row.getInteger("id");
|
||||
type = row.getString("mtype");
|
||||
version = row.getString("mversion");
|
||||
comment = row.getString("mcomment");
|
||||
checksum = row.getInteger("mchecksum");
|
||||
runBy = row.getString("run_by");
|
||||
runTime = row.getLong("run_time");
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "id:" + id + " type:" + type + " runVersion:" + version + " comment:" + comment + " runOn:" + runOn + " runBy:" + runBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the id for this migration.
|
||||
*/
|
||||
int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the normalised version for this migration.
|
||||
*/
|
||||
String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the checksum for this migration.
|
||||
*/
|
||||
int getChecksum() {
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to the insert statement.
|
||||
*/
|
||||
void bindInsert(SqlUpdate insert) {
|
||||
insert.setParameter(1, id);
|
||||
insert.setParameter(2, type);
|
||||
insert.setParameter(3, "SUCCESS");
|
||||
insert.setParameter(4, version);
|
||||
insert.setParameter(5, comment);
|
||||
insert.setParameter(6, checksum);
|
||||
insert.setParameter(7, runOn);
|
||||
insert.setParameter(8, runBy);
|
||||
insert.setParameter(9, runTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SQL insert given the table migration meta data is stored in.
|
||||
*/
|
||||
static String insertSql(String table) {
|
||||
return "insert into " + table
|
||||
+ " (id, mtype, mstatus, mversion, mcomment, mchecksum, run_on, run_by, run_time)"
|
||||
+ " values (?,?,?,?,?,?,?,?,?)";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.avaje.ebean.dbmigration.runner;
|
||||
|
||||
import com.avaje.ebean.dbmigration.DdlRunner;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Runs the DDL migration scripts.
|
||||
*/
|
||||
public class MigrationScriptRunner {
|
||||
|
||||
private final Connection connection;
|
||||
|
||||
/**
|
||||
* Construct with a given connection.
|
||||
*/
|
||||
public MigrationScriptRunner(Connection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all the DDL statements in the script.
|
||||
*/
|
||||
int runScript(boolean expectErrors, String content, String scriptName) throws SQLException {
|
||||
|
||||
DdlRunner runner = new DdlRunner(expectErrors, scriptName);
|
||||
return runner.runAll(content, connection);
|
||||
}
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
package com.avaje.ebean.dbmigration.runner;
|
||||
|
||||
import com.avaje.ebean.EbeanServer;
|
||||
import com.avaje.ebean.SqlQuery;
|
||||
import com.avaje.ebean.SqlRow;
|
||||
import com.avaje.ebean.SqlUpdate;
|
||||
import com.avaje.ebean.config.DbMigrationConfig;
|
||||
import com.avaje.ebean.config.ServerConfig;
|
||||
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
|
||||
import com.avaje.ebean.dbmigration.MigrationRunner;
|
||||
import com.avaje.ebean.plugin.SpiServer;
|
||||
import com.avaje.ebeaninternal.server.transaction.ExternalJdbcTransaction;
|
||||
import com.avaje.ebeaninternal.util.IOUtils;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Manages the migration table.
|
||||
*/
|
||||
public class MigrationTable {
|
||||
|
||||
private static final Logger logger = MigrationRunner.logger;
|
||||
|
||||
private final Connection connection;
|
||||
|
||||
private final EbeanServer server;
|
||||
|
||||
private final DatabasePlatform databasePlatform;
|
||||
|
||||
private final String catalog;
|
||||
private final String schema;
|
||||
private final String table;
|
||||
private final ServerConfig serverConfig;
|
||||
private final String envUserName;
|
||||
|
||||
private final Timestamp runOn = new Timestamp(System.currentTimeMillis());
|
||||
|
||||
private final ScriptTransform scriptTransform;
|
||||
|
||||
private final String insertSql;
|
||||
|
||||
private final LinkedHashMap<String, MigrationMetaRow> migrations;
|
||||
|
||||
private MigrationMetaRow lastMigration;
|
||||
|
||||
/**
|
||||
* Construct with server, configuration and jdbc connection (DB admin user).
|
||||
*/
|
||||
public MigrationTable(EbeanServer server, DbMigrationConfig migrationConfig, Connection connection) {
|
||||
this.connection = connection;
|
||||
this.server = server;
|
||||
this.migrations = new LinkedHashMap<String, MigrationMetaRow>();
|
||||
|
||||
SpiServer pluginApi = server.getPluginApi();
|
||||
this.serverConfig = pluginApi.getServerConfig();
|
||||
this.databasePlatform = pluginApi.getDatabasePlatform();
|
||||
this.catalog = null;
|
||||
this.schema = null;
|
||||
this.table = migrationConfig.getMetaTable();
|
||||
this.insertSql = MigrationMetaRow.insertSql(table);
|
||||
this.scriptTransform = createScriptTransform(migrationConfig);
|
||||
this.envUserName = System.getProperty("user.name");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of migrations in the DB migration table.
|
||||
*/
|
||||
public int size() {
|
||||
return migrations.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the ScriptTransform for placeholder key/value replacement.
|
||||
*/
|
||||
private ScriptTransform createScriptTransform(DbMigrationConfig config) {
|
||||
|
||||
Map<String, String> map = PlaceholderBuilder.build(config.getRunPlaceholders(), config.getRunPlaceholderMap());
|
||||
return new ScriptTransform(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the table is it does not exist.
|
||||
*/
|
||||
public void createIfNeeded() throws SQLException, IOException {
|
||||
|
||||
if (!tableExists(connection)) {
|
||||
createTable(connection);
|
||||
}
|
||||
|
||||
ExternalJdbcTransaction t = new ExternalJdbcTransaction(connection);
|
||||
SqlQuery sqlQuery = server.createSqlQuery("select * from " + table + " order by id for update");
|
||||
List<SqlRow> metaRows = server.findList(sqlQuery, t);
|
||||
|
||||
for (SqlRow row : metaRows) {
|
||||
MigrationMetaRow metaRow = new MigrationMetaRow(row);
|
||||
addMigration(metaRow.getVersion(), metaRow);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void createTable(Connection connection) throws IOException, SQLException {
|
||||
|
||||
String script = ScriptTransform.table(table, getCreateTableScript());
|
||||
|
||||
MigrationScriptRunner run = new MigrationScriptRunner(connection);
|
||||
run.runScript(false, script, "create migration table");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the create table script.
|
||||
*/
|
||||
private String getCreateTableScript() throws IOException {
|
||||
// supply a script to override the default table create script
|
||||
String script = readResource("migration-support/create-table.sql");
|
||||
if (script == null) {
|
||||
// no, just use the default script
|
||||
script = readResource("migration-support/default-create-table.sql");
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
private String readResource(String location) throws IOException {
|
||||
|
||||
Enumeration<URL> resources = serverConfig.getClassLoadConfig().getResources(location);
|
||||
if (resources.hasMoreElements()) {
|
||||
URL url = resources.nextElement();
|
||||
return IOUtils.readUtf8(url.openStream());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the table exists.
|
||||
*/
|
||||
private boolean tableExists(Connection connection) throws SQLException {
|
||||
boolean exists = databasePlatform.tableExists(connection, catalog, schema, table);
|
||||
if (!exists) {
|
||||
exists = databasePlatform.tableExists(connection, catalog, schema, table.toUpperCase());
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the migration ran successfully and false if the migration failed.
|
||||
*/
|
||||
public boolean shouldRun(LocalMigrationResource localVersion, LocalMigrationResource priorVersion) throws SQLException {
|
||||
|
||||
if (priorVersion != null && !localVersion.isRepeatable()) {
|
||||
if (!migrationExists(priorVersion)) {
|
||||
logger.error("Migration {} requires prior migration {} which has not been run", localVersion.getVersion(), priorVersion.getVersion());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
MigrationMetaRow existing = migrations.get(localVersion.key());
|
||||
return runMigration(localVersion, existing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the migration script.
|
||||
*
|
||||
* @param local The local migration resource
|
||||
* @param existing The information for this migration existing in the table
|
||||
*
|
||||
* @return True if the migrations should continue
|
||||
*/
|
||||
private boolean runMigration(LocalMigrationResource local, MigrationMetaRow existing) throws SQLException {
|
||||
|
||||
String script = convertScript(local.getContent());
|
||||
int checksum = Checksum.calculate(script);
|
||||
|
||||
if (existing != null) {
|
||||
|
||||
boolean matchChecksum = (existing.getChecksum() == checksum);
|
||||
|
||||
if (!local.isRepeatable()) {
|
||||
if (!matchChecksum) {
|
||||
logger.error("Checksum mismatch on migration {}", local.getLocation());
|
||||
}
|
||||
return true;
|
||||
|
||||
} else if (matchChecksum) {
|
||||
logger.trace("... skip unchanged repeatable migration {}", local.getLocation());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
runMigration(local, script, checksum);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a migration script as new migration or update on existing repeatable migration.
|
||||
*/
|
||||
private void runMigration(LocalMigrationResource local, String script, int checksum) throws SQLException {
|
||||
|
||||
logger.debug("run migration {}", local.getLocation());
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
MigrationScriptRunner run = new MigrationScriptRunner(connection);
|
||||
run.runScript(false, script, "run migration version: " + local.getVersion());
|
||||
|
||||
long exeMillis = System.currentTimeMillis() - start;
|
||||
// insert new migration row
|
||||
SqlUpdate insert = server.createSqlUpdate(insertSql);
|
||||
MigrationMetaRow metaRow = createMetaRow(local, checksum, exeMillis);
|
||||
metaRow.bindInsert(insert);
|
||||
server.execute(insert, new ExternalJdbcTransaction(connection));
|
||||
|
||||
addMigration(local.key(), metaRow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the MigrationMetaRow for this migration.
|
||||
*/
|
||||
private MigrationMetaRow createMetaRow(LocalMigrationResource migration, int checksum, long exeMillis) {
|
||||
|
||||
int nextId = 1;
|
||||
if (lastMigration != null) {
|
||||
nextId = lastMigration.getId() + 1;
|
||||
}
|
||||
|
||||
String type = migration.getType();
|
||||
String runVersion = migration.key();
|
||||
String comment = migration.getComment();
|
||||
|
||||
return new MigrationMetaRow(nextId, type, runVersion, comment, checksum, envUserName, runOn, exeMillis);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the migration exists.
|
||||
*/
|
||||
private boolean migrationExists(LocalMigrationResource priorVersion) {
|
||||
return migrations.containsKey(priorVersion.key());
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the placeholder key/value replacement on the script.
|
||||
*/
|
||||
private String convertScript(String script) {
|
||||
return scriptTransform.transform(script);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the successfully executed migration (to allow dependant scripts to run).
|
||||
*/
|
||||
private void addMigration(String key, MigrationMetaRow metaRow) {
|
||||
lastMigration = metaRow;
|
||||
if (metaRow.getVersion() == null) {
|
||||
throw new IllegalStateException("No runVersion in db migration table row? " + metaRow);
|
||||
}
|
||||
migrations.put(key, metaRow);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.avaje.ebean.dbmigration.runner;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Joins placeholder map and comma/equals delimited string.
|
||||
*/
|
||||
class PlaceholderBuilder {
|
||||
|
||||
private final Map<String,String> map = new HashMap<String,String>();
|
||||
|
||||
/**
|
||||
* Create with raw comma and equals delimited pairs plus map of key value pairs.
|
||||
*/
|
||||
public static Map<String,String> build(String commaDelimited, Map<String,String> placeholders) {
|
||||
|
||||
PlaceholderBuilder builder = new PlaceholderBuilder();
|
||||
builder.add(commaDelimited);
|
||||
builder.add(placeholders);
|
||||
|
||||
return builder.map;
|
||||
}
|
||||
|
||||
private PlaceholderBuilder() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a comma and equals delimited string to parse for key value pairs.
|
||||
*/
|
||||
public void add(String commaDelimited) {
|
||||
|
||||
if (commaDelimited != null) {
|
||||
String[] split = commaDelimited.split("[,;]");
|
||||
for (String keyValue : split) {
|
||||
String[] pair = keyValue.split("=");
|
||||
if (pair.length == 2) {
|
||||
map.put(pair[0].trim(), pair[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a map of key value placeholder pairs.
|
||||
*/
|
||||
public void add(Map<String,String> placeholders) {
|
||||
if (placeholders != null) {
|
||||
map.putAll(placeholders);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package com.avaje.ebean.dbmigration.runner;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Transforms a SQL script given a map of key/value substitutions.
|
||||
*/
|
||||
class ScriptTransform {
|
||||
|
||||
/**
|
||||
* Transform just ${table} with the table name.
|
||||
*/
|
||||
public static String table(String tableName, String script) {
|
||||
return script.replace("${table}", tableName);
|
||||
}
|
||||
|
||||
private final Map<String,String> placeholders = new HashMap<String, String>();
|
||||
|
||||
ScriptTransform(Map<String,String> map) {
|
||||
for (Map.Entry<String, String> entry : map.entrySet()) {
|
||||
placeholders.put(wrapKey(entry.getKey()), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private String wrapKey(String key) {
|
||||
return "${"+key+"}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this contains no placeholders.
|
||||
*/
|
||||
boolean isEmpty() {
|
||||
return placeholders.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the script replacing placeholders in the form <code>${key}</code> with <code>value</code>.
|
||||
*/
|
||||
String transform(String source) {
|
||||
|
||||
for (Map.Entry<String, String> entry : placeholders.entrySet()) {
|
||||
source = source.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return source;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import com.avaje.ebean.config.EncryptKeyManager;
|
||||
import com.avaje.ebean.config.ServerConfig;
|
||||
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
|
||||
import com.avaje.ebean.dbmigration.DdlGenerator;
|
||||
import com.avaje.ebean.dbmigration.MigrationRunner;
|
||||
import com.avaje.ebean.event.BeanPersistController;
|
||||
import com.avaje.ebean.event.readaudit.ReadAuditLogger;
|
||||
import com.avaje.ebean.event.readaudit.ReadAuditPrepare;
|
||||
@@ -65,6 +64,7 @@ import com.avaje.ebeaninternal.server.transaction.TransactionScopeManager;
|
||||
import com.avaje.ebeaninternal.util.ParamTypeHelper;
|
||||
import com.avaje.ebeaninternal.util.ParamTypeHelper.TypeInfo;
|
||||
import com.avaje.ebeanservice.docstore.api.DocStoreIntegration;
|
||||
import org.avaje.dbmigration.MigrationRunner;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -344,10 +344,13 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
DbMigrationConfig migrationConfig = serverConfig.getMigrationConfig();
|
||||
if (migrationConfig != null) {
|
||||
migrationConfig.generateOnStart(this);
|
||||
}
|
||||
|
||||
if (migrationConfig.isRunMigration()) {
|
||||
new MigrationRunner(this, migrationConfig).run();
|
||||
if (migrationConfig.isRunMigration()) {
|
||||
// classLoader used to load resources
|
||||
ClassLoader classLoader = serverConfig.getClassLoadConfig().getClassLoader();
|
||||
MigrationRunner runner = migrationConfig.createRunner(classLoader);
|
||||
runner.run(serverConfig.getDataSource());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user