001package io.ebean.dbmigration;
002
003import io.ebean.dbmigration.runner.LocalMigrationResource;
004import io.ebean.dbmigration.runner.LocalMigrationResources;
005import io.ebean.dbmigration.runner.MigrationTable;
006import io.ebean.dbmigration.runner.MigrationSchema;
007import io.ebean.dbmigration.util.JdbcClose;
008import org.slf4j.Logger;
009import org.slf4j.LoggerFactory;
010
011import javax.sql.DataSource;
012import java.io.IOException;
013import java.sql.Connection;
014import java.sql.SQLException;
015import java.util.List;
016
017/**
018 * Runs the DB migration typically on application start.
019 */
020public class MigrationRunner {
021
022  private static final Logger logger = LoggerFactory.getLogger(MigrationRunner.class);
023
024  private final MigrationConfig migrationConfig;
025
026  /**
027   * Set to true when only checking as to what migrations will run.
028   */
029  private boolean checkStateMode;
030
031  private List<LocalMigrationResource> checkMigrations;
032
033  public MigrationRunner(MigrationConfig migrationConfig) {
034    this.migrationConfig = migrationConfig;
035  }
036
037  /**
038   * Run by creating a DB connection from driver, url, username defined in MigrationConfig.
039   */
040  public void run() {
041    this.checkStateMode = false;
042    run(migrationConfig.createConnection());
043  }
044
045  /**
046   * Return the migrations that would be applied if the migration is run.
047   */
048  public List<LocalMigrationResource> checkState() {
049    this.checkStateMode = true;
050    run(migrationConfig.createConnection());
051    return checkMigrations;
052  }
053
054  /**
055   * Run using the connection from the DataSource.
056   */
057  public void run(DataSource dataSource) {
058    run(getConnection(dataSource));
059  }
060
061  private Connection getConnection(DataSource dataSource) {
062
063    String username = migrationConfig.getDbUsername();
064    try {
065      if (username == null) {
066        return dataSource.getConnection();
067      }
068      logger.debug("using db user [{}] to run migrations ...", username);
069      return dataSource.getConnection(username, migrationConfig.getDbPassword());
070    } catch (SQLException e) {
071      String msgSuffix = (username == null) ? "" : " using user [" + username + "]";
072      throw new IllegalArgumentException("Error trying to connect to database for DB Migration" + msgSuffix, e);
073    }
074  }
075
076  /**
077   * Run the migrations if there are any that need running.
078   */
079  public void run(Connection connection) {
080
081    LocalMigrationResources resources = new LocalMigrationResources(migrationConfig);
082    if (!resources.readResources()) {
083      logger.debug("no migrations to check");
084      return;
085    }
086
087    try {
088      connection.setAutoCommit(false);
089
090      MigrationSchema schema = new MigrationSchema(migrationConfig, connection);
091      schema.createAndSetIfNeeded();
092
093      runMigrations(resources, connection);
094      connection.commit();
095
096    } catch (MigrationException e) {
097      JdbcClose.rollback(connection);
098      throw e;
099
100    } catch (Exception e) {
101      JdbcClose.rollback(connection);
102      throw new RuntimeException(e);
103
104    } finally {
105      JdbcClose.close(connection);
106    }
107  }
108
109  /**
110   * Run all the migrations as needed.
111   */
112  private void runMigrations(LocalMigrationResources resources, Connection connection) throws SQLException, IOException {
113
114    derivePlatformName(migrationConfig, connection);
115
116    MigrationTable table = new MigrationTable(migrationConfig, connection, checkStateMode);
117    table.createIfNeededAndLock();
118
119    // get the migrations in version order
120    List<LocalMigrationResource> localVersions = resources.getVersions();
121
122    logger.info("local migrations:{}  existing migrations:{}", localVersions.size(), table.size());
123
124    LocalMigrationResource priorVersion = null;
125
126    // run migrations in order
127    for (LocalMigrationResource localVersion : localVersions) {
128      if (!table.shouldRun(localVersion, priorVersion)) {
129        break;
130      }
131      priorVersion = localVersion;
132    }
133    if (checkStateMode) {
134      checkMigrations = table.ran();
135    }
136  }
137
138  /**
139   * Derive and set the platform name if required.
140   */
141  private void derivePlatformName(MigrationConfig migrationConfig, Connection connection) {
142
143    if (migrationConfig.getPlatformName() == null) {
144      migrationConfig.setPlatformName(DbNameUtil.normalise(connection));
145    }
146  }
147
148}