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.MigrationVersion; 024import com.avaje.ebean.dbmigration.model.ModelContainer; 025import com.avaje.ebean.dbmigration.model.ModelDiff; 026import com.avaje.ebean.dbmigration.model.PlatformDdlWriter; 027import com.avaje.ebeaninternal.api.SpiEbeanServer; 028import com.avaje.ebeaninternal.extraddl.model.DdlScript; 029import com.avaje.ebeaninternal.extraddl.model.ExtraDdl; 030import com.avaje.ebeaninternal.extraddl.model.ExtraDdlXmlReader; 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033 034import java.io.File; 035import java.io.FileWriter; 036import java.io.IOException; 037import java.util.ArrayList; 038import java.util.List; 039 040/** 041 * Generates DB Migration xml and sql scripts. 042 * <p> 043 * Reads the prior migrations and compares with the current model of the EbeanServer 044 * and generates a migration 'diff' in the form of xml document with the logical schema 045 * changes and a series of sql scripts to apply, rollback the applied changes if necessary 046 * and drop objects (drop tables, drop columns). 047 * </p> 048 * <p> 049 * This does not run the migration or ddl scripts but just generates them. 050 * </p> 051 * <pre>{@code 052 * 053 * DbMigration migration = new DbMigration(); 054 * migration.setPathToResources("src/main/resources"); 055 * migration.setPlatform(DbPlatformName.ORACLE); 056 * 057 * migration.generateMigration(); 058 * 059 * }</pre> 060 */ 061public class DbMigration { 062 063 protected static final Logger logger = LoggerFactory.getLogger(DbMigration.class); 064 065 private static final String initialVersion = "1.0"; 066 067 private static final String GENERATED_COMMENT = "THIS IS A GENERATED FILE - DO NOT MODIFY"; 068 069 /** 070 * Set to true if DbMigration run with online EbeanServer instance. 071 */ 072 protected final boolean online; 073 074 protected SpiEbeanServer server; 075 076 protected DbMigrationConfig migrationConfig; 077 078 protected String pathToResources = "src/main/resources"; 079 080 protected DatabasePlatform databasePlatform; 081 082 protected List<Pair> platforms = new ArrayList<Pair>(); 083 084 protected ServerConfig serverConfig; 085 086 protected DbConstraintNaming constraintNaming; 087 088 /** 089 * Create for offline migration generation. 090 */ 091 public DbMigration() { 092 this.online = false; 093 } 094 095 /** 096 * Create using online EbeanServer. 097 */ 098 public DbMigration(EbeanServer server) { 099 this.online = true; 100 setServer(server); 101 } 102 103 /** 104 * Set the path from the current working directory to the application resources. 105 * <p> 106 * This defaults to maven style 'src/main/resources'. 107 */ 108 public void setPathToResources(String pathToResources) { 109 this.pathToResources = pathToResources; 110 } 111 112 /** 113 * Set the server to use to determine the current model. 114 * Typically this is not called explicitly. 115 */ 116 public void setServer(EbeanServer ebeanServer) { 117 this.server = (SpiEbeanServer) ebeanServer; 118 setServerConfig(server.getServerConfig()); 119 } 120 121 /** 122 * Set the serverConfig to use. Typically this is not called explicitly. 123 */ 124 public void setServerConfig(ServerConfig config) { 125 if (this.serverConfig == null) { 126 this.serverConfig = config; 127 } 128 if (migrationConfig == null) { 129 this.migrationConfig = serverConfig.getMigrationConfig(); 130 } 131 if (constraintNaming == null) { 132 this.constraintNaming = serverConfig.getConstraintNaming(); 133 } 134 } 135 136 /** 137 * Set the specific platform to generate DDL for. 138 * <p> 139 * If not set this defaults to the platform of the default server. 140 * </p> 141 */ 142 public void setPlatform(DbPlatformName platform) { 143 setPlatform(getPlatform(platform)); 144 } 145 146 /** 147 * Set the specific platform to generate DDL for. 148 * <p> 149 * If not set this defaults to the platform of the default server. 150 * </p> 151 */ 152 public void setPlatform(DatabasePlatform databasePlatform) { 153 this.databasePlatform = databasePlatform; 154 if (!online) { 155 DbOffline.setPlatform(databasePlatform.getName()); 156 } 157 } 158 159 /** 160 * Add an additional platform to write the migration DDL. 161 * <p> 162 * Use this when you want to generate sql scripts for multiple database platforms 163 * from the migration (e.g. generate migration sql for MySql, Postgres and Oracle). 164 * </p> 165 */ 166 public void addPlatform(DbPlatformName platform, String prefix) { 167 platforms.add(new Pair(getPlatform(platform), prefix)); 168 } 169 170 /** 171 * Generate the next migration xml file and associated apply and rollback sql scripts. 172 * <p> 173 * This does not run the migration or ddl scripts but just generates them. 174 * </p> 175 * <h3>Example: Run for a single specific platform</h3> 176 * <pre>{@code 177 * 178 * DbMigration migration = new DbMigration(); 179 * migration.setPathToResources("src/main/resources"); 180 * migration.setPlatform(DbPlatformName.ORACLE); 181 * 182 * migration.generateMigration(); 183 * 184 * }</pre> 185 * <p> 186 * <h3>Example: Run migration generating DDL for multiple platforms</h3> 187 * <pre>{@code 188 * 189 * DbMigration migration = new DbMigration(); 190 * migration.setPathToResources("src/main/resources"); 191 * 192 * migration.addPlatform(DbPlatformName.POSTGRES, "pg"); 193 * migration.addPlatform(DbPlatformName.MYSQL, "mysql"); 194 * migration.addPlatform(DbPlatformName.ORACLE, "mysql"); 195 * 196 * migration.generateMigration(); 197 * 198 * }</pre> 199 */ 200 public void generateMigration() throws IOException { 201 202 // use this flag to stop other plugins like full DDL generation 203 if (!online) { 204 DbOffline.setGenerateMigration(); 205 if (databasePlatform == null && !platforms.isEmpty()) { 206 // for multiple platform generation set the general platform 207 // to H2 so that it runs offline without DB connection 208 setPlatform(platforms.get(0).platform); 209 } 210 } 211 setDefaults(); 212 try { 213 Request request = createRequest(); 214 215 if (platforms.isEmpty()) { 216 generateExtraDdl(request.migrationDir, databasePlatform); 217 } 218 219 String pendingVersion = generatePendingDrop(); 220 if (pendingVersion != null) { 221 generatePendingDrop(request, pendingVersion); 222 } else { 223 generateDiff(request); 224 } 225 226 } finally { 227 if (!online) { 228 DbOffline.reset(); 229 } 230 } 231 } 232 233 /** 234 * Generate "repeatable" migration scripts. 235 * <p> 236 * These take scrips from extra-dll.xml (typically views) and outputs "repeatable" 237 * migration scripts (starting with "R__") to be run by FlywayDb or Ebean's own 238 * migration runner. 239 * </p> 240 */ 241 private void generateExtraDdl(File migrationDir, DatabasePlatform dbPlatform) throws IOException { 242 243 if (dbPlatform != null) { 244 ExtraDdl extraDdl = ExtraDdlXmlReader.read("/extra-ddl.xml"); 245 if (extraDdl != null) { 246 List<DdlScript> ddlScript = extraDdl.getDdlScript(); 247 for (DdlScript script : ddlScript) { 248 if (ExtraDdlXmlReader.matchPlatform(dbPlatform.getName(), script.getPlatforms())) { 249 writeExtraDdl(migrationDir, script); 250 } 251 } 252 } 253 } 254 } 255 256 /** 257 * Write (or override) the "repeatable" migration script. 258 */ 259 private void writeExtraDdl(File migrationDir, DdlScript script) throws IOException { 260 261 String fullName = repeatableMigrationName(script.getName()); 262 263 logger.info("writing repeatable script {}", fullName); 264 265 File file = new File(migrationDir, fullName); 266 FileWriter writer = new FileWriter(file); 267 writer.write(script.getValue()); 268 writer.flush(); 269 writer.close(); 270 } 271 272 private String repeatableMigrationName(String scriptName) { 273 return "R__" + scriptName.replace(' ', '_') + migrationConfig.getApplySuffix(); 274 } 275 276 /** 277 * Generate the diff migration. 278 */ 279 private void generateDiff(Request request) throws IOException { 280 281 List<String> pendingDrops = request.getPendingDrops(); 282 if (!pendingDrops.isEmpty()) { 283 logger.info("Pending un-applied drops in versions {}", pendingDrops); 284 } 285 286 Migration migration = request.createDiffMigration(); 287 if (migration == null) { 288 logger.info("no changes detected - no migration written"); 289 } else { 290 // there were actually changes to write 291 generateMigration(request, migration, null); 292 } 293 } 294 295 /** 296 * Generate the migration based on the pendingDrops from a prior version. 297 */ 298 private void generatePendingDrop(Request request, String pendingVersion) throws IOException { 299 300 Migration migration = request.migrationForPendingDrop(pendingVersion); 301 302 generateMigration(request, migration, pendingVersion); 303 304 List<String> pendingDrops = request.getPendingDrops(); 305 if (!pendingDrops.isEmpty()) { 306 logger.info("... remaining pending un-applied drops in versions {}", pendingDrops); 307 } 308 } 309 310 private Request createRequest() { 311 return new Request(); 312 } 313 314 private class Request { 315 316 final File migrationDir; 317 final File modelDir; 318 final MigrationModel migrationModel; 319 final CurrentModel currentModel; 320 final ModelContainer migrated; 321 final ModelContainer current; 322 323 private Request() { 324 this.migrationDir = getMigrationDirectory(); 325 this.modelDir = getModelDirectory(migrationDir); 326 this.migrationModel = new MigrationModel(modelDir, migrationConfig.getModelSuffix()); 327 this.migrated = migrationModel.read(); 328 this.currentModel = new CurrentModel(server, constraintNaming); 329 this.current = currentModel.read(); 330 } 331 332 /** 333 * Return the migration for the pending drops for a given version. 334 */ 335 public Migration migrationForPendingDrop(String pendingVersion) { 336 337 Migration migration = migrated.migrationForPendingDrop(pendingVersion); 338 339 // register any remaining pending drops 340 migrated.registerPendingHistoryDropColumns(current); 341 return migration; 342 } 343 344 /** 345 * Return the list of versions that have pending un-applied drops. 346 */ 347 public List<String> getPendingDrops() { 348 return migrated.getPendingDrops(); 349 } 350 351 /** 352 * Create and return the diff of the current model to the migration model. 353 */ 354 public Migration createDiffMigration() { 355 ModelDiff diff = new ModelDiff(migrated); 356 diff.compareTo(current); 357 return diff.isEmpty() ? null : diff.getMigration(); 358 } 359 } 360 361 private void generateMigration(Request request, Migration dbMigration, String dropsFor) throws IOException { 362 363 String fullVersion = getFullVersion(request.migrationModel, dropsFor); 364 365 logger.info("generating migration:{}", fullVersion); 366 if (!writeMigrationXml(dbMigration, request.modelDir, fullVersion)) { 367 logger.warn("migration already exists, not generating DDL"); 368 369 } else { 370 if (!platforms.isEmpty()) { 371 writeExtraPlatformDdl(fullVersion, request.currentModel, dbMigration, request.migrationDir); 372 373 } else if (databasePlatform != null) { 374 // writer needs the current model to provide table/column details for 375 // history ddl generation (triggers, history tables etc) 376 DdlWrite write = new DdlWrite(new MConfiguration(), request.current); 377 PlatformDdlWriter writer = createDdlWriter(databasePlatform, ""); 378 writer.processMigration(dbMigration, write, request.migrationDir, fullVersion); 379 } 380 } 381 } 382 383 /** 384 * Return true if the next pending drop changeSet should be generated as the next migration. 385 */ 386 private String generatePendingDrop() { 387 388 String nextDrop = System.getProperty("ddl.migration.pendingDropsFor"); 389 if (nextDrop != null) { 390 return nextDrop; 391 } 392 return migrationConfig.getGeneratePendingDrop(); 393 } 394 395 /** 396 * Return the full version for the migration being generated. 397 * <p> 398 * The full version can contain a comment suffix after a "__" double underscore. 399 */ 400 private String getFullVersion(MigrationModel migrationModel, String dropsFor) { 401 402 String version = migrationConfig.getVersion(); 403 if (version == null) { 404 version = migrationModel.getNextVersion(initialVersion); 405 } 406 407 String fullVersion = migrationConfig.getApplyPrefix() + version; 408 if (migrationConfig.getName() != null) { 409 fullVersion += "__" + toUnderScore(migrationConfig.getName()); 410 411 } else if (dropsFor != null) { 412 fullVersion += "__" + toUnderScore("dropsFor_" + MigrationVersion.trim(dropsFor)); 413 414 } else if (version.equals(initialVersion)) { 415 fullVersion += "__initial"; 416 } 417 return fullVersion; 418 } 419 420 /** 421 * Replace spaces with underscores. 422 */ 423 private String toUnderScore(String name) { 424 return name.replace(' ', '_'); 425 } 426 427 /** 428 * Write any extra platform ddl. 429 */ 430 protected void writeExtraPlatformDdl(String fullVersion, CurrentModel currentModel, Migration dbMigration, File writePath) throws IOException { 431 432 for (Pair pair : platforms) { 433 DdlWrite platformBuffer = new DdlWrite(new MConfiguration(), currentModel.read()); 434 PlatformDdlWriter platformWriter = createDdlWriter(pair); 435 File subPath = platformWriter.subPath(writePath, pair.prefix); 436 platformWriter.processMigration(dbMigration, platformBuffer, subPath, fullVersion); 437 438 generateExtraDdl(subPath, pair.platform); 439 } 440 } 441 442 private PlatformDdlWriter createDdlWriter(Pair pair) { 443 return createDdlWriter(pair.platform, pair.prefix); 444 } 445 446 private PlatformDdlWriter createDdlWriter(DatabasePlatform platform, String prefix) { 447 return new PlatformDdlWriter(platform, serverConfig, prefix, migrationConfig); 448 } 449 450 /** 451 * Write the migration xml. 452 */ 453 protected boolean writeMigrationXml(Migration dbMigration, File resourcePath, String fullVersion) { 454 455 String modelFile = fullVersion + migrationConfig.getModelSuffix(); 456 File file = new File(resourcePath, modelFile); 457 if (file.exists()) { 458 return false; 459 } 460 String comment = migrationConfig.isIncludeGeneratedFileComment() ? GENERATED_COMMENT : null; 461 MigrationXmlWriter xmlWriter = new MigrationXmlWriter(comment); 462 xmlWriter.write(dbMigration, file); 463 return true; 464 } 465 466 /** 467 * Set default server and platform if necessary. 468 */ 469 protected void setDefaults() { 470 if (server == null) { 471 setServer(Ebean.getDefaultServer()); 472 } 473 if (databasePlatform == null && platforms.isEmpty()) { 474 // not explicitly set not set a list of platforms so 475 // default to the platform of the default server 476 databasePlatform = server.getDatabasePlatform(); 477 logger.debug("set platform to {}", databasePlatform.getName()); 478 } 479 } 480 481 /** 482 * Return the file path to write the xml and sql to. 483 */ 484 protected File getMigrationDirectory() { 485 486 // path to src/main/resources in typical maven project 487 File resourceRootDir = new File(pathToResources); 488 String resourcePath = migrationConfig.getMigrationPath(); 489 490 // expect to be a path to something like - src/main/resources/dbmigration/model 491 File path = new File(resourceRootDir, resourcePath); 492 if (!path.exists()) { 493 if (!path.mkdirs()) { 494 logger.debug("Unable to ensure migration directory exists at {}", path.getAbsolutePath()); 495 } 496 } 497 return path; 498 } 499 500 /** 501 * Return the model directory (relative to the migration directory). 502 */ 503 protected File getModelDirectory(File migrationDirectory) { 504 String modelPath = migrationConfig.getModelPath(); 505 if (modelPath == null || modelPath.isEmpty()) { 506 return migrationDirectory; 507 } 508 File modelDir = new File(migrationDirectory, migrationConfig.getModelPath()); 509 if (!modelDir.exists() && !modelDir.mkdirs()) { 510 logger.debug("Unable to ensure migration model directory exists at {}", modelDir.getAbsolutePath()); 511 } 512 return modelDir; 513 } 514 515 /** 516 * Return the DatabasePlatform given the platform key. 517 */ 518 protected DatabasePlatform getPlatform(DbPlatformName platform) { 519 switch (platform) { 520 case H2: 521 return new H2Platform(); 522 case POSTGRES: 523 return new PostgresPlatform(); 524 case MYSQL: 525 return new MySqlPlatform(); 526 case ORACLE: 527 return new OraclePlatform(); 528 case SQLSERVER: 529 return new MsSqlServer2005Platform(); 530 case DB2: 531 return new DB2Platform(); 532 case SQLITE: 533 return new SQLitePlatform(); 534 535 default: 536 throw new IllegalArgumentException("Platform missing? " + platform); 537 } 538 } 539 540 /** 541 * Holds a platform and prefix. Used to generate multiple platform specific DDL 542 * for a single migration. 543 */ 544 public static class Pair { 545 546 /** 547 * The platform to generate the DDL for. 548 */ 549 public final DatabasePlatform platform; 550 551 /** 552 * A prefix included into the file/resource names indicating the platform. 553 */ 554 public final String prefix; 555 556 public Pair(DatabasePlatform platform, String prefix) { 557 this.platform = platform; 558 this.prefix = prefix; 559 } 560 } 561 562}