001package org.avaje.dbmigration;
002
003import org.avaje.dbmigration.runner.LocalMigrationResource;
004import org.avaje.dbmigration.runner.LocalMigrationResources;
005import org.avaje.dbmigration.runner.MigrationTable;
006import org.avaje.dbmigration.runner.MigrationSchema;
007import org.avaje.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("org.avaje.dbmigration.MigrationRunner");
023
024  private final MigrationConfig migrationConfig;
025
026  public MigrationRunner(MigrationConfig migrationConfig) {
027    this.migrationConfig = migrationConfig;
028  }
029
030  /**
031   * Run by creating a DB connection from driver, url, username defined in MigrationConfig.
032   */
033  public void run() {
034
035    Connection connection = migrationConfig.createConnection();
036    run(connection);
037  }
038
039  /**
040   * Run using the connection from the DataSource.
041   */
042  public void run(DataSource dataSource) {
043    run(getConnection(dataSource));
044  }
045
046  private Connection getConnection(DataSource dataSource) {
047
048    String username = migrationConfig.getDbUsername();
049    try {
050      if (username == null) {
051        return dataSource.getConnection();
052      }
053      logger.debug("using db user [{}] to run migrations ...", username);
054      return dataSource.getConnection(username, migrationConfig.getDbPassword());
055    } catch (SQLException e) {
056      String msgSuffix = (username == null) ? "" : " using user [" + username + "]";
057      throw new IllegalArgumentException("Error trying to connect to database for DB Migration" + msgSuffix, e);
058    }
059  }
060
061  /**
062   * Run the migrations if there are any that need running.
063   */
064  public void run(Connection connection) {
065
066    LocalMigrationResources resources = new LocalMigrationResources(migrationConfig);
067    if (!resources.readResources()) {
068      logger.debug("no migrations to check");
069      return;
070    }
071
072    try {
073      connection.setAutoCommit(false);
074
075      MigrationSchema schema = new MigrationSchema(migrationConfig, connection);
076      schema.createAndSetIfNeeded();
077
078      runMigrations(resources, connection);
079
080      connection.commit();
081
082    } catch (Exception e) {
083      JdbcClose.rollback(connection);
084      throw new RuntimeException(e);
085
086    } finally {
087      JdbcClose.close(connection);
088    }
089  }
090
091  /**
092   * Run all the migrations as needed.
093   */
094  private void runMigrations(LocalMigrationResources resources, Connection connection) throws SQLException, IOException {
095
096    MigrationTable table = new MigrationTable(migrationConfig, connection);
097    table.createIfNeeded();
098
099    // get the migrations in version order
100    List<LocalMigrationResource> localVersions = resources.getVersions();
101
102    logger.info("local migrations:{}  existing migrations:{}", localVersions.size(), table.size());
103
104    LocalMigrationResource priorVersion = null;
105
106    // run migrations in order
107    for (int i = 0; i < localVersions.size(); i++) {
108      LocalMigrationResource localVersion = localVersions.get(i);
109      if (!table.shouldRun(localVersion, priorVersion)) {
110        break;
111      }
112      priorVersion = localVersion;
113      connection.commit();
114    }
115  }
116
117}