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