001package io.ebeaninternal.dbmigration.ddlgeneration.platform; 002 003import io.ebean.config.DatabaseConfig; 004import io.ebean.config.DbConstraintNaming; 005import io.ebeaninternal.dbmigration.ddlgeneration.DdlBuffer; 006import io.ebeaninternal.dbmigration.ddlgeneration.DdlWrite; 007import io.ebeaninternal.dbmigration.migration.AddHistoryTable; 008import io.ebeaninternal.dbmigration.migration.DropHistoryTable; 009import io.ebeaninternal.dbmigration.model.MColumn; 010import io.ebeaninternal.dbmigration.model.MTable; 011 012import java.io.IOException; 013import java.util.Collection; 014import java.util.List; 015 016/** 017 * Uses DB triggers to maintain a history table. 018 */ 019public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl { 020 021 protected DbConstraintNaming constraintNaming; 022 023 protected PlatformDdl platformDdl; 024 025 protected String sysPeriod; 026 protected String sysPeriodStart; 027 protected String sysPeriodEnd; 028 029 protected String viewSuffix; 030 protected String historySuffix; 031 032 protected String sysPeriodType = "datetime(6)"; 033 protected String now = "now(6)"; 034 protected String sysPeriodEndValue = "now(6)"; 035 036 DbTriggerBasedHistoryDdl() { 037 } 038 039 @Override 040 public void configure(DatabaseConfig config, PlatformDdl platformDdl) { 041 this.platformDdl = platformDdl; 042 this.sysPeriod = config.getAsOfSysPeriod(); 043 this.viewSuffix = config.getAsOfViewSuffix(); 044 this.historySuffix = config.getHistoryTableSuffix(); 045 this.constraintNaming = config.getConstraintNaming(); 046 047 this.sysPeriodStart = sysPeriod + "_start"; 048 this.sysPeriodEnd = sysPeriod + "_end"; 049 } 050 051 @Override 052 public void updateTriggers(DdlWrite writer, HistoryTableUpdate update) throws IOException { 053 054 MTable table = writer.getTable(update.getBaseTable()); 055 if (table == null) { 056 throw new IllegalStateException("MTable " + update.getBaseTable() + " not found in writer? (required for history DDL)"); 057 } 058 updateTriggers(writer, table, update); 059 } 060 061 /** 062 * Replace the existing triggers/stored procedures/views for history table support given the included columns. 063 */ 064 protected abstract void updateHistoryTriggers(DbTriggerUpdate triggerUpdate) throws IOException; 065 066 /** 067 * Process the HistoryTableUpdate which can result in changes to the apply, rollback 068 * and drop scripts. 069 */ 070 protected void updateTriggers(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException { 071 072 writer.applyHistoryTrigger().append("-- changes: ").append(update.description()).newLine(); 073 074 updateHistoryTriggers(createDbTriggerUpdate(writer, table)); 075 } 076 077 protected DbTriggerUpdate createDbTriggerUpdate(DdlWrite writer, MTable table) { 078 079 List<String> columns = columnNamesForApply(table); 080 String baseTableName = table.getName(); 081 String historyTableName = historyTableName(baseTableName); 082 return new DbTriggerUpdate(baseTableName, historyTableName, writer, columns); 083 } 084 085 @Override 086 public void dropHistoryTable(DdlWrite writer, DropHistoryTable dropHistoryTable) throws IOException { 087 088 String baseTable = dropHistoryTable.getBaseTable(); 089 090 // drop in appropriate order 091 dropTriggers(writer.applyDropDependencies(), baseTable); 092 dropHistoryTableEtc(writer.applyDropDependencies(), baseTable); 093 } 094 095 @Override 096 public void addHistoryTable(DdlWrite writer, AddHistoryTable addHistoryTable) throws IOException { 097 098 String baseTable = addHistoryTable.getBaseTable(); 099 MTable table = writer.getTable(baseTable); 100 if (table == null) { 101 throw new IllegalStateException("MTable " + baseTable + " not found in writer? (required for history DDL)"); 102 } 103 104 createWithHistory(writer, table); 105 } 106 107 @Override 108 public void createWithHistory(DdlWrite writer, MTable table) throws IOException { 109 110 String baseTable = table.getName(); 111 String whenCreatedColumn = table.getWhenCreatedColumn(); 112 113 dropTriggers(writer.dropAll(), baseTable); 114 dropHistoryTableEtc(writer.dropAll(), baseTable); 115 116 addHistoryTable(writer, table, whenCreatedColumn); 117 createStoredFunction(writer, table); 118 createTriggers(writer, table); 119 } 120 121 protected abstract void createTriggers(DdlWrite writer, MTable table) throws IOException; 122 123 protected abstract void dropTriggers(DdlBuffer buffer, String baseTable) throws IOException; 124 125 protected void createStoredFunction(DdlWrite writer, MTable table) throws IOException { 126 // do nothing 127 } 128 129 protected String normalise(String tableName) { 130 return constraintNaming.normaliseTable(tableName); 131 } 132 133 protected String historyTableName(String baseTableName) { 134 return baseTableName + historySuffix; 135 } 136 137 protected String procedureName(String baseTableName) { 138 return baseTableName + "_history_version"; 139 } 140 141 protected String triggerName(String baseTableName) { 142 return normalise(baseTableName) + "_history_upd"; 143 } 144 145 protected String updateTriggerName(String baseTableName) { 146 return normalise(baseTableName) + "_history_upd"; 147 } 148 149 protected String deleteTriggerName(String baseTableName) { 150 return normalise(baseTableName) + "_history_del"; 151 } 152 153 protected void addHistoryTable(DdlWrite writer, MTable table, String whenCreatedColumn) throws IOException { 154 155 String baseTableName = table.getName(); 156 157 DdlBuffer apply = writer.applyHistoryView(); 158 159 addSysPeriodColumns(apply, baseTableName, whenCreatedColumn); 160 createHistoryTable(apply, table); 161 createWithHistoryView(apply, baseTableName); 162 } 163 164 protected void addSysPeriodColumns(DdlBuffer apply, String baseTableName, String whenCreatedColumn) throws IOException { 165 166 apply.append("alter table ").append(baseTableName).append(" add column ") 167 .append(sysPeriodStart).append(" ").append(sysPeriodType).append(" default ").append(now).endOfStatement(); 168 apply.append("alter table ").append(baseTableName).append(" add column ") 169 .append(sysPeriodEnd).append(" ").append(sysPeriodType).endOfStatement(); 170 171 if (whenCreatedColumn != null) { 172 apply.append("update ").append(baseTableName).append(" set ").append(sysPeriodStart).append(" = ").append(whenCreatedColumn).endOfStatement(); 173 } 174 } 175 176 protected void createHistoryTable(DdlBuffer apply, MTable table) throws IOException { 177 178 apply.append(platformDdl.getCreateTableCommandPrefix()).append(" ").append(table.getName()).append(historySuffix).append("(").newLine(); 179 180 Collection<MColumn> cols = table.allColumns(); 181 for (MColumn column : cols) { 182 if (!column.isDraftOnly()) { 183 writeColumnDefinition(apply, column.getName(), column.getType()); 184 apply.append(",").newLine(); 185 } 186 } 187 writeColumnDefinition(apply, sysPeriodStart, sysPeriodType); 188 apply.append(",").newLine(); 189 writeColumnDefinition(apply, sysPeriodEnd, sysPeriodType); 190 apply.newLine().append(")").endOfStatement(); 191 } 192 193 /** 194 * Write the column definition to the create table statement. 195 */ 196 protected void writeColumnDefinition(DdlBuffer buffer, String columnName, String type) throws IOException { 197 198 String platformType = platformDdl.convert(type); 199 buffer.append(" "); 200 buffer.append(platformDdl.lowerColumnName(columnName), 29); 201 buffer.append(platformType); 202 } 203 204 protected void createWithHistoryView(DdlBuffer apply, String baseTableName) throws IOException { 205 206 apply 207 .append("create view ").append(baseTableName).append(viewSuffix) 208 .append(" as select * from ").append(baseTableName) 209 .append(" union all select * from ").append(baseTableName).append(historySuffix) 210 .endOfStatement().end(); 211 } 212 213 214 /** 215 * For postgres/h2/mysql we need to drop and recreate the view. Well, we could add columns to the end of the view 216 * but otherwise we need to drop and create it. 217 */ 218 protected void recreateHistoryView(DbTriggerUpdate update) throws IOException { 219 220 DdlBuffer buffer = update.dropDependencyBuffer(); 221 // we need to drop the view early/first before any changes to the tables etc 222 buffer.append("drop view if exists ").append(update.getBaseTable()).append(viewSuffix).endOfStatement(); 223 224 // recreate the view after all ddl modifications - the view requires ALL columns, also the historyExclude ones. 225 createWithHistoryView(update.historyViewBuffer(), update.getBaseTable()); 226 } 227 228 protected void appendSysPeriodColumns(DdlBuffer apply, String prefix) throws IOException { 229 appendColumnName(apply, prefix, sysPeriodStart); 230 appendColumnName(apply, prefix, sysPeriodEnd); 231 } 232 233 protected void dropHistoryTableEtc(DdlBuffer buffer, String baseTableName) throws IOException { 234 235 buffer.append("drop view ").append(baseTableName).append(viewSuffix).endOfStatement(); 236 dropSysPeriodColumns(buffer, baseTableName); 237 buffer.append("drop table ").append(baseTableName).append(historySuffix).endOfStatement().end(); 238 } 239 240 protected void dropSysPeriodColumns(DdlBuffer buffer, String baseTableName) throws IOException { 241 platformDdl.alterTableDropColumn(buffer, baseTableName, sysPeriodStart); 242 platformDdl.alterTableDropColumn(buffer, baseTableName, sysPeriodEnd); 243 } 244 245 protected void appendInsertIntoHistory(DdlBuffer buffer, String historyTable, List<String> columns) throws IOException { 246 247 buffer.append(" insert into ").append(historyTable).append(" (").append(sysPeriodStart).append(",").append(sysPeriodEnd).append(","); 248 appendColumnNames(buffer, columns, ""); 249 buffer.append(") values (OLD.").append(sysPeriodStart).append(", ").append(sysPeriodEndValue).append(","); 250 appendColumnNames(buffer, columns, "OLD."); 251 buffer.append(");").newLine(); 252 } 253 254 void appendColumnNames(DdlBuffer buffer, List<String> columns, String columnPrefix) throws IOException { 255 for (int i = 0; i < columns.size(); i++) { 256 if (i > 0) { 257 buffer.append(", "); 258 } 259 buffer.append(columnPrefix); 260 buffer.append(columns.get(i)); 261 } 262 } 263 264 /** 265 * Append a single column to the buffer if it is not null. 266 */ 267 void appendColumnName(DdlBuffer buffer, String prefix, String columnName) throws IOException { 268 if (columnName != null) { 269 buffer.append(prefix).append(columnName); 270 } 271 } 272 273 /** 274 * Return the column names included in history for the apply script. 275 * <p> 276 * Note that dropped columns are actually still included at this point as they are going 277 * to be removed from the history handling when the drop script runs that also deletes 278 * the column. 279 * </p> 280 */ 281 List<String> columnNamesForApply(MTable table) { 282 return table.allHistoryColumns(true); 283 } 284 285}