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}