001package com.avaje.ebean.dbmigration;
002
003import com.avaje.ebean.Ebean;
004import com.avaje.ebean.EbeanServer;
005import com.avaje.ebean.config.DbConstraintNaming;
006import com.avaje.ebean.config.DbMigrationConfig;
007import com.avaje.ebean.config.ServerConfig;
008import com.avaje.ebean.config.dbplatform.DB2Platform;
009import com.avaje.ebean.config.dbplatform.DatabasePlatform;
010import com.avaje.ebean.config.dbplatform.DbPlatformName;
011import com.avaje.ebean.config.dbplatform.H2Platform;
012import com.avaje.ebean.config.dbplatform.MsSqlServer2005Platform;
013import com.avaje.ebean.config.dbplatform.MySqlPlatform;
014import com.avaje.ebean.config.dbplatform.OraclePlatform;
015import com.avaje.ebean.config.dbplatform.PostgresPlatform;
016import com.avaje.ebean.config.dbplatform.SQLitePlatform;
017import com.avaje.ebean.dbmigration.ddlgeneration.DdlWrite;
018import com.avaje.ebean.dbmigration.migration.Migration;
019import com.avaje.ebean.dbmigration.migrationreader.MigrationXmlWriter;
020import com.avaje.ebean.dbmigration.model.CurrentModel;
021import com.avaje.ebean.dbmigration.model.MConfiguration;
022import com.avaje.ebean.dbmigration.model.MigrationModel;
023import com.avaje.ebean.dbmigration.model.ModelContainer;
024import com.avaje.ebean.dbmigration.model.PlatformDdlWriter;
025import com.avaje.ebean.dbmigration.model.ModelDiff;
026import com.avaje.ebeaninternal.api.SpiEbeanServer;
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029
030import java.io.File;
031import java.io.IOException;
032import java.util.ArrayList;
033import java.util.List;
034
035/**
036 * Generates DB Migration xml and sql scripts.
037 * <p>
038 * Reads the prior migrations and compares with the current model of the EbeanServer
039 * and generates a migration 'diff' in the form of xml document with the logical schema
040 * changes and a series of sql scripts to apply, rollback the applied changes if necessary
041 * and drop objects (drop tables, drop columns).
042 * </p>
043 * <p>
044 *   This does not run the migration or ddl scripts but just generates them.
045 * </p>
046 * <pre>{@code
047 *
048 *       DbMigration migration = new DbMigration();
049 *       migration.setPathToResources("src/main/resources");
050 *       migration.setPlatform(DbPlatformName.ORACLE);
051 *
052 *       migration.generateMigration();
053 *
054 * }</pre>
055 */
056public class DbMigration {
057
058  protected static final Logger logger = LoggerFactory.getLogger(DbMigration.class);
059
060  protected SpiEbeanServer server;
061
062  protected DbMigrationConfig migrationConfig;
063
064  protected String pathToResources = "src/main/resources";
065
066  protected DatabasePlatform databasePlatform;
067
068  protected List<Pair> platforms = new ArrayList<Pair>();
069
070  protected ServerConfig serverConfig;
071
072  protected DbConstraintNaming constraintNaming;
073
074  public DbMigration() {
075  }
076
077  /**
078   * Set the path from the current working directory to the application resources.
079   *
080   * This defaults to maven style 'src/main/resources'.
081   */
082  public void setPathToResources(String pathToResources) {
083    this.pathToResources = pathToResources;
084  }
085
086  /**
087   * Set the server to use to determine the current model.
088   * Typically this is not called explicitly.
089   */
090  public void setServer(EbeanServer ebeanServer) {
091    this.server = (SpiEbeanServer) ebeanServer;
092    setServerConfig(server.getServerConfig());
093  }
094
095  /**
096   * Set the serverConfig to use. Typically this is not called explicitly.
097   */
098  public void setServerConfig(ServerConfig config) {
099    if (this.serverConfig == null) {
100      this.serverConfig = config;
101    }
102    if (migrationConfig == null) {
103      this.migrationConfig = serverConfig.getMigrationConfig();
104    }
105    if (constraintNaming == null) {
106      this.constraintNaming = serverConfig.getConstraintNaming();
107    }
108  }
109
110  /**
111   * Set the specific platform to generate DDL for.
112   * <p>
113   * If not set this defaults to the platform of the default server.
114   * </p>
115   */
116  public void setPlatform(DbPlatformName platform) {
117    setPlatform(getPlatform(platform));
118  }
119
120  /**
121   * Set the specific platform to generate DDL for.
122   * <p>
123   * If not set this defaults to the platform of the default server.
124   * </p>
125   */
126  public void setPlatform(DatabasePlatform databasePlatform) {
127    this.databasePlatform = databasePlatform;
128    DbOffline.setPlatform(databasePlatform.getName());
129  }
130
131  /**
132   * Add an additional platform to write the migration DDL.
133   * <p>
134   * Use this when you want to generate sql scripts for multiple database platforms
135   * from the migration (e.g. generate migration sql for MySql, Postgres and Oracle).
136   * </p>
137   */
138  public void addPlatform(DbPlatformName platform, String prefix) {
139    if (!prefix.endsWith("-")) {
140      prefix += "-";
141    }
142    platforms.add(new Pair(getPlatform(platform), prefix));
143  }
144
145  /**
146   * Generate the next migration xml file and associated apply and rollback sql scripts.
147   * <p>
148   *   This does not run the migration or ddl scripts but just generates them.
149   * </p>
150   * <h3>Example: Run for a single specific platform</h3>
151   * <pre>{@code
152   *
153   *       DbMigration migration = new DbMigration();
154   *       migration.setPathToResources("src/main/resources");
155   *       migration.setPlatform(DbPlatformName.ORACLE);
156   *
157   *       migration.generateMigration();
158   *
159   * }</pre>
160   *
161   * <h3>Example: Run migration generating DDL for multiple platforms</h3>
162   * <pre>{@code
163   *
164   *       DbMigration migration = new DbMigration();
165   *       migration.setPathToResources("src/main/resources");
166   *
167   *       migration.addPlatform(DbPlatformName.POSTGRES, "pg");
168   *       migration.addPlatform(DbPlatformName.MYSQL, "mysql");
169   *       migration.addPlatform(DbPlatformName.ORACLE, "mysql");
170   *
171   *       migration.generateMigration();
172   *
173   * }</pre>
174   */
175  public void generateMigration() throws IOException {
176
177    // use this flag to stop other plugins like full DDL generation
178    DbOffline.setRunningMigration();
179
180    setDefaults();
181
182    try {
183      MigrationModel migrationModel = new MigrationModel(migrationConfig.getResourcePath());
184      ModelContainer migrated = migrationModel.read();
185      int nextMajorVersion = migrationModel.getNextMajorVersion();
186
187      logger.info("next migration version {}", nextMajorVersion);
188
189      CurrentModel currentModel = new CurrentModel(server, constraintNaming);
190      ModelContainer current = currentModel.read();
191
192      ModelDiff diff = new ModelDiff(migrated);
193      diff.compareTo(current);
194
195      if (diff.isEmpty()) {
196        logger.info("no changes detected - no migration written");
197        return;
198      }
199
200      // there were actually changes to write
201      Migration dbMigration = diff.getMigration();
202
203      File writePath = getWritePath();
204      logger.info("migration writing version {} to {}", nextMajorVersion, writePath.getAbsolutePath());
205      writeMigrationXml(dbMigration, writePath, nextMajorVersion);
206
207      if (databasePlatform != null) {
208        // writer needs the current model to provide table/column details for
209        // history ddl generation (triggers, history tables etc)
210        DdlWrite write = new DdlWrite(new MConfiguration(), currentModel.read());
211        PlatformDdlWriter writer = new PlatformDdlWriter(databasePlatform, serverConfig);
212        writer.processMigration(dbMigration, write, writePath, nextMajorVersion);
213      }
214
215      writeExtraPlatformDdl(nextMajorVersion, currentModel, dbMigration, writePath);
216
217    } finally {
218      DbOffline.reset();
219    }
220  }
221
222  /**
223   * Write any extra platform ddl.
224   */
225  protected void writeExtraPlatformDdl(int nextMajorVersion, CurrentModel currentModel, Migration dbMigration, File writePath) throws IOException {
226
227    for (Pair pair : platforms) {
228      DdlWrite platformBuffer = new DdlWrite(new MConfiguration(), currentModel.read());
229
230      PlatformDdlWriter platformWriter = new PlatformDdlWriter(pair.platform, serverConfig, pair.prefix);
231      platformWriter.processMigration(dbMigration, platformBuffer, writePath, nextMajorVersion);
232    }
233  }
234
235  /**
236   * Write the migration xml.
237   */
238  protected void writeMigrationXml(Migration dbMigration, File resourcePath, int migrationVersion) {
239
240    File file = new File(resourcePath, "v"+migrationVersion+".0.xml");
241    MigrationXmlWriter xmlWriter = new MigrationXmlWriter();
242    xmlWriter.write(dbMigration, file);
243  }
244
245  /**
246   * Set default server and platform if necessary.
247   */
248  protected void setDefaults() {
249    if (server == null) {
250      setServer(Ebean.getDefaultServer());
251    }
252    if (databasePlatform == null && platforms.isEmpty()) {
253      // not explicitly set not set a list of platforms so
254      // default to the platform of the default server
255      databasePlatform = server.getDatabasePlatform();
256      logger.debug("set platform to {}", databasePlatform.getName());
257    }
258  }
259
260  /**
261   * Return the file path to write the xml and sql to.
262   */
263  protected File getWritePath() {
264
265    // path to src/main/resources in typical maven project
266    File resourceRootDir = new File(pathToResources);
267
268    String resourcePath = migrationConfig.getResourcePath();
269
270    // expect to be a path to something like - src/main/resources/dbmigration/myapp
271    File path = new File(resourceRootDir, resourcePath);
272    if (!path.exists()) {
273      if (!path.mkdirs()) {
274        logger.debug("Unable to ensure migration directory exists at {}", path.getAbsolutePath());
275      }
276    }
277    return path;
278  }
279
280  /**
281   * Return the DatabasePlatform given the platform key.
282   */
283  protected DatabasePlatform getPlatform(DbPlatformName platform) {
284    switch (platform) {
285      case H2:
286        return new H2Platform();
287      case POSTGRES:
288        return new PostgresPlatform();
289      case MYSQL:
290        return new MySqlPlatform();
291      case ORACLE:
292        return new OraclePlatform();
293      case SQLSERVER:
294        return new MsSqlServer2005Platform();
295      case DB2:
296        return new DB2Platform();
297      case SQLITE:
298        return new SQLitePlatform();
299
300      default:
301        throw new IllegalArgumentException("Platform missing? " + platform);
302    }
303  }
304
305  /**
306   * Holds a platform and prefix. Used to generate multiple platform specific DDL
307   * for a single migration.
308   */
309  public static class Pair {
310
311    /**
312     * The platform to generate the DDL for.
313     */
314    public final DatabasePlatform platform;
315
316    /**
317     * A prefix included into the file/resource names indicating the platform.
318     */
319    public final String prefix;
320
321    public Pair(DatabasePlatform platform, String prefix) {
322      this.platform = platform;
323      this.prefix = prefix;
324    }
325  }
326
327}