001package io.ebeaninternal.dbmigration.model; 002 003import io.ebeaninternal.dbmigration.ddlgeneration.platform.DdlHelp; 004import io.ebeaninternal.dbmigration.migration.AddColumn; 005import io.ebeaninternal.dbmigration.migration.AddHistoryTable; 006import io.ebeaninternal.dbmigration.migration.AddTableComment; 007import io.ebeaninternal.dbmigration.migration.AlterColumn; 008import io.ebeaninternal.dbmigration.migration.Column; 009import io.ebeaninternal.dbmigration.migration.CreateTable; 010import io.ebeaninternal.dbmigration.migration.DropColumn; 011import io.ebeaninternal.dbmigration.migration.DropHistoryTable; 012import io.ebeaninternal.dbmigration.migration.DropTable; 013import io.ebeaninternal.dbmigration.migration.ForeignKey; 014import io.ebeaninternal.dbmigration.migration.RenameColumn; 015import io.ebeaninternal.dbmigration.migration.UniqueConstraint; 016import io.ebeaninternal.server.deploy.BeanDescriptor; 017import io.ebeaninternal.server.deploy.BeanProperty; 018import io.ebeaninternal.server.deploy.IdentityMode; 019import io.ebeaninternal.server.deploy.PartitionMeta; 020import org.slf4j.Logger; 021import org.slf4j.LoggerFactory; 022 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.HashSet; 026import java.util.LinkedHashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030 031import static io.ebeaninternal.dbmigration.ddlgeneration.platform.SplitColumns.split; 032import static io.ebeaninternal.dbmigration.model.MTableIdentity.fromCreateTable; 033import static io.ebeaninternal.dbmigration.model.MTableIdentity.toCreateTable; 034 035/** 036 * Holds the logical model for a given Table and everything associated to it. 037 * <p> 038 * This effectively represents a table, its columns and all associated 039 * constraints, foreign keys and indexes. 040 * <p> 041 * Migrations can be applied to this such that it represents the state 042 * of a given table after various migrations have been applied. 043 * <p> 044 * This table model can also be derived from the EbeanServer bean descriptor 045 * and associated properties. 046 */ 047public class MTable { 048 049 private static final Logger logger = LoggerFactory.getLogger(MTable.class); 050 051 /** 052 * Table name. 053 */ 054 private final String name; 055 056 /** 057 * The associated draft table. 058 */ 059 private MTable draftTable; 060 061 /** 062 * Marked true for draft tables. These need to have their FK references adjusted 063 * after all the draft tables have been identified. 064 */ 065 private boolean draft; 066 067 private PartitionMeta partitionMeta; 068 069 /** 070 * Primary key name. 071 */ 072 private String pkName; 073 074 /** 075 * Table comment. 076 */ 077 private String comment; 078 079 /** 080 * Tablespace to use. 081 */ 082 private String tablespace; 083 084 private String storageEngine; 085 086 /** 087 * Tablespace to use for indexes on this table. 088 */ 089 private String indexTablespace; 090 091 private IdentityMode identityMode; 092 093 /** 094 * If set to true this table should has history support. 095 */ 096 private boolean withHistory; 097 098 /** 099 * The columns on the table. 100 */ 101 private Map<String, MColumn> columns = new LinkedHashMap<>(); 102 103 /** 104 * Compound unique constraints. 105 */ 106 private List<MCompoundUniqueConstraint> uniqueConstraints = new ArrayList<>(); 107 108 /** 109 * Compound foreign keys. 110 */ 111 private List<MCompoundForeignKey> compoundKeys = new ArrayList<>(); 112 113 /** 114 * Column name for the 'When created' column. This can be used for the initial effective start date when adding 115 * history to an existing table and maps to a @WhenCreated or @CreatedTimestamp column. 116 */ 117 private String whenCreatedColumn; 118 119 /** 120 * Temporary - holds addColumn settings. 121 */ 122 private AddColumn addColumn; 123 124 private List<String> droppedColumns = new ArrayList<>(); 125 126 public MTable(BeanDescriptor<?> descriptor) { 127 this.name = descriptor.getBaseTable(); 128 this.identityMode = descriptor.getIdentityMode(); 129 this.storageEngine = descriptor.getStorageEngine(); 130 this.partitionMeta = descriptor.getPartitionMeta(); 131 this.comment = descriptor.getDbComment(); 132 if (descriptor.isHistorySupport()) { 133 withHistory = true; 134 BeanProperty whenCreated = descriptor.getWhenCreatedProperty(); 135 if (whenCreated != null) { 136 whenCreatedColumn = whenCreated.getDbColumn(); 137 } 138 } 139 } 140 141 /** 142 * Construct for element collection or intersection table. 143 */ 144 public MTable(String name) { 145 this.name = name; 146 this.identityMode = IdentityMode.NONE; 147 } 148 149 /** 150 * Create a copy of this table structure as a 'draft' table. 151 * <p> 152 * Note that both tables contain @DraftOnly MColumns and these are filtered out 153 * later when creating the CreateTable object. 154 */ 155 public MTable createDraftTable() { 156 157 draftTable = new MTable(name + "_draft"); 158 draftTable.draft = true; 159 draftTable.whenCreatedColumn = whenCreatedColumn; 160 // compoundKeys 161 // compoundUniqueConstraints 162 draftTable.identityMode = identityMode; 163 164 for (MColumn col : allColumns()) { 165 draftTable.addColumn(col.copyForDraft()); 166 } 167 168 return draftTable; 169 } 170 171 /** 172 * Construct for migration. 173 */ 174 public MTable(CreateTable createTable) { 175 this.name = createTable.getName(); 176 this.pkName = createTable.getPkName(); 177 this.comment = createTable.getComment(); 178 this.storageEngine = createTable.getStorageEngine(); 179 this.tablespace = createTable.getTablespace(); 180 this.indexTablespace = createTable.getIndexTablespace(); 181 this.withHistory = Boolean.TRUE.equals(createTable.isWithHistory()); 182 this.draft = Boolean.TRUE.equals(createTable.isDraft()); 183 this.identityMode = fromCreateTable(createTable); 184 List<Column> cols = createTable.getColumn(); 185 for (Column column : cols) { 186 addColumn(column); 187 } 188 for (UniqueConstraint uq : createTable.getUniqueConstraint()) { 189 uniqueConstraints.add(new MCompoundUniqueConstraint(uq)); 190 } 191 192 for (ForeignKey fk : createTable.getForeignKey()) { 193 if (DdlHelp.isDropForeignKey(fk.getColumnNames())) { 194 removeForeignKey(fk.getName()); 195 } else { 196 addForeignKey(fk.getName(), fk.getRefTableName(), fk.getIndexName(), fk.getColumnNames(), fk.getRefColumnNames()); 197 } 198 } 199 } 200 201 public void addForeignKey(String name, String refTableName, String indexName, String columnNames, String refColumnNames) { 202 MCompoundForeignKey foreignKey = new MCompoundForeignKey(name, refTableName, indexName); 203 String[] cols = split(columnNames); 204 String[] refCols = split(refColumnNames); 205 for (int i = 0; i < cols.length && i < refCols.length; i++) { 206 foreignKey.addColumnPair(cols[i], refCols[i]); 207 } 208 addForeignKey(foreignKey); 209 } 210 211 /** 212 * Return the DropTable migration for this table. 213 */ 214 public DropTable dropTable() { 215 DropTable dropTable = new DropTable(); 216 dropTable.setName(name); 217 // we must add pk col name & sequence name, as we have to delete the sequence also. 218 if (identityMode.isDatabaseIdentity()) { 219 String pkCol = null; 220 for (MColumn column : columns.values()) { 221 if (column.isPrimaryKey()) { 222 if (pkCol == null) { 223 pkCol = column.getName(); 224 } else { // multiple pk cols -> no sequence 225 pkCol = null; 226 break; 227 } 228 } 229 } 230 if (pkCol != null) { 231 dropTable.setSequenceCol(pkCol); 232 dropTable.setSequenceName(identityMode.getSequenceName()); 233 } 234 } 235 return dropTable; 236 } 237 238 /** 239 * Return the CreateTable migration for this table. 240 */ 241 public CreateTable createTable() { 242 243 CreateTable createTable = new CreateTable(); 244 createTable.setName(name); 245 createTable.setPkName(pkName); 246 createTable.setComment(comment); 247 if (partitionMeta != null) { 248 createTable.setPartitionMode(partitionMeta.getMode().name()); 249 createTable.setPartitionColumn(partitionMeta.getProperty()); 250 } 251 createTable.setStorageEngine(storageEngine); 252 createTable.setTablespace(tablespace); 253 createTable.setIndexTablespace(indexTablespace); 254 toCreateTable(identityMode, createTable); 255 if (withHistory) { 256 createTable.setWithHistory(Boolean.TRUE); 257 } 258 if (draft) { 259 createTable.setDraft(Boolean.TRUE); 260 } 261 262 for (MColumn column : allColumns()) { 263 // filter out draftOnly columns from the base table 264 if (draft || !column.isDraftOnly()) { 265 createTable.getColumn().add(column.createColumn()); 266 } 267 } 268 269 for (MCompoundForeignKey compoundKey : compoundKeys) { 270 createTable.getForeignKey().add(compoundKey.createForeignKey()); 271 } 272 273 for (MCompoundUniqueConstraint constraint : uniqueConstraints) { 274 createTable.getUniqueConstraint().add(constraint.getUniqueConstraint()); 275 } 276 277 return createTable; 278 } 279 280 /** 281 * Compare to another version of the same table to perform a diff. 282 */ 283 public void compare(ModelDiff modelDiff, MTable newTable) { 284 285 if (withHistory != newTable.withHistory) { 286 if (withHistory) { 287 DropHistoryTable dropHistoryTable = new DropHistoryTable(); 288 dropHistoryTable.setBaseTable(name); 289 modelDiff.addDropHistoryTable(dropHistoryTable); 290 291 } else { 292 AddHistoryTable addHistoryTable = new AddHistoryTable(); 293 addHistoryTable.setBaseTable(name); 294 modelDiff.addAddHistoryTable(addHistoryTable); 295 } 296 } 297 298 compareColumns(modelDiff, newTable); 299 300 if (MColumn.different(comment, newTable.comment)) { 301 AddTableComment addTableComment = new AddTableComment(); 302 addTableComment.setName(name); 303 if (newTable.comment == null) { 304 addTableComment.setComment(DdlHelp.DROP_COMMENT); 305 } else { 306 addTableComment.setComment(newTable.comment); 307 } 308 modelDiff.addTableComment(addTableComment); 309 } 310 311 312 compareCompoundKeys(modelDiff, newTable); 313 compareUniqueKeys(modelDiff, newTable); 314 } 315 316 private void compareColumns(ModelDiff modelDiff, MTable newTable) { 317 addColumn = null; 318 319 Map<String, MColumn> newColumnMap = newTable.getColumns(); 320 321 // compare newColumns to existing columns (look for new and diff columns) 322 for (MColumn newColumn : newColumnMap.values()) { 323 MColumn localColumn = getColumn(newColumn.getName()); 324 if (localColumn == null) { 325 // can ignore if draftOnly column and non-draft table 326 if (!newColumn.isDraftOnly() || draft) { 327 diffNewColumn(newColumn); 328 } 329 } else { 330 localColumn.compare(modelDiff, this, newColumn); 331 } 332 } 333 334 // compare existing columns (look for dropped columns) 335 for (MColumn existingColumn : allColumns()) { 336 MColumn newColumn = newColumnMap.get(existingColumn.getName()); 337 if (newColumn == null) { 338 diffDropColumn(modelDiff, existingColumn); 339 } else if (newColumn.isDraftOnly() && !draft) { 340 // effectively a drop column (draft only column on a non-draft table) 341 logger.trace("... drop column {} from table {} as now draftOnly", newColumn.getName(), name); 342 diffDropColumn(modelDiff, existingColumn); 343 } 344 } 345 346 if (addColumn != null) { 347 modelDiff.addAddColumn(addColumn); 348 } 349 } 350 351 352 private void compareCompoundKeys(ModelDiff modelDiff, MTable newTable) { 353 List<MCompoundForeignKey> newKeys = new ArrayList<>(newTable.getCompoundKeys()); 354 List<MCompoundForeignKey> currentKeys = new ArrayList<>(getCompoundKeys()); 355 356 // remove keys that have not changed 357 currentKeys.removeAll(newTable.getCompoundKeys()); 358 newKeys.removeAll(getCompoundKeys()); 359 360 for (MCompoundForeignKey currentKey : currentKeys) { 361 modelDiff.addAlterForeignKey(currentKey.dropForeignKey(name)); 362 } 363 364 for (MCompoundForeignKey newKey : newKeys) { 365 modelDiff.addAlterForeignKey(newKey.addForeignKey(name)); 366 } 367 } 368 369 private void compareUniqueKeys(ModelDiff modelDiff, MTable newTable) { 370 List<MCompoundUniqueConstraint> newKeys = new ArrayList<>(newTable.getUniqueConstraints()); 371 List<MCompoundUniqueConstraint> currentKeys = new ArrayList<>(getUniqueConstraints()); 372 373 // remove keys that have not changed 374 currentKeys.removeAll(newTable.getUniqueConstraints()); 375 newKeys.removeAll(getUniqueConstraints()); 376 377 for (MCompoundUniqueConstraint currentKey: currentKeys) { 378 modelDiff.addUniqueConstraint(currentKey.dropUniqueConstraint(name)); 379 } 380 for (MCompoundUniqueConstraint newKey: newKeys) { 381 modelDiff.addUniqueConstraint(newKey.addUniqueConstraint(name)); 382 } 383 } 384 385 /** 386 * Apply AddColumn migration. 387 */ 388 public void apply(AddColumn addColumn) { 389 checkTableName(addColumn.getTableName()); 390 for (Column column : addColumn.getColumn()) { 391 addColumn(column); 392 } 393 } 394 395 /** 396 * Apply AddColumn migration. 397 */ 398 public void apply(AlterColumn alterColumn) { 399 checkTableName(alterColumn.getTableName()); 400 String columnName = alterColumn.getColumnName(); 401 MColumn existingColumn = getColumn(columnName); 402 if (existingColumn == null) { 403 throw new IllegalStateException("Column [" + columnName + "] does not exist for AlterColumn change?"); 404 } 405 existingColumn.apply(alterColumn); 406 } 407 408 /** 409 * Apply DropColumn migration. 410 */ 411 public void apply(DropColumn dropColumn) { 412 checkTableName(dropColumn.getTableName()); 413 MColumn removed = columns.remove(dropColumn.getColumnName()); 414 if (removed == null) { 415 throw new IllegalStateException("Column [" + dropColumn.getColumnName() + "] does not exist for DropColumn change on table [" + dropColumn.getTableName() + "]?"); 416 } 417 } 418 419 public void apply(RenameColumn renameColumn) { 420 checkTableName(renameColumn.getTableName()); 421 MColumn column = columns.remove(renameColumn.getOldName()); 422 if (column == null) { 423 throw new IllegalStateException("Column [" + renameColumn.getOldName() + "] does not exist for RenameColumn change on table [" + renameColumn.getTableName() + "]?"); 424 } 425 addColumn(column.rename(renameColumn.getNewName())); 426 } 427 428 public String getName() { 429 return name; 430 } 431 432 public String getSchema() { 433 int pos = name.indexOf('.'); 434 return pos == -1 ? null : name.substring(0, pos); 435 } 436 437 /** 438 * Return true if this table is a 'Draft' table. 439 */ 440 public boolean isDraft() { 441 return draft; 442 } 443 444 /** 445 * Return true if this table is partitioned. 446 */ 447 public boolean isPartitioned() { 448 return partitionMeta != null; 449 } 450 451 /** 452 * Return the partition meta for this table. 453 */ 454 public PartitionMeta getPartitionMeta() { 455 return partitionMeta; 456 } 457 458 public String getPkName() { 459 return pkName; 460 } 461 462 public void setPkName(String pkName) { 463 this.pkName = pkName; 464 } 465 466 public String getComment() { 467 return comment; 468 } 469 470 public void setComment(String comment) { 471 this.comment = comment; 472 } 473 474 public String getTablespace() { 475 return tablespace; 476 } 477 478 public String getIndexTablespace() { 479 return indexTablespace; 480 } 481 482 public boolean isWithHistory() { 483 return withHistory; 484 } 485 486 public MTable setWithHistory(boolean withHistory) { 487 this.withHistory = withHistory; 488 return this; 489 } 490 491 public List<String> allHistoryColumns(boolean includeDropped) { 492 493 List<String> columnNames = new ArrayList<>(columns.size()); 494 for (MColumn column : columns.values()) { 495 if (column.isIncludeInHistory()) { 496 columnNames.add(column.getName()); 497 } 498 } 499 if (includeDropped && !droppedColumns.isEmpty()) { 500 columnNames.addAll(droppedColumns); 501 } 502 return columnNames; 503 } 504 505 /** 506 * Return all the columns (excluding columns marked as dropped). 507 */ 508 public Collection<MColumn> allColumns() { 509 return columns.values(); 510 } 511 512 /** 513 * Return the column by name. 514 */ 515 public MColumn getColumn(String name) { 516 return columns.get(name); 517 } 518 519 private Map<String, MColumn> getColumns() { 520 return columns; 521 } 522 523 public List<MCompoundUniqueConstraint> getUniqueConstraints() { 524 return uniqueConstraints; 525 } 526 527 public List<MCompoundForeignKey> getCompoundKeys() { 528 return compoundKeys; 529 } 530 531 public String getWhenCreatedColumn() { 532 return whenCreatedColumn; 533 } 534 535 /** 536 * Return the list of columns that make the primary key. 537 */ 538 public List<MColumn> primaryKeyColumns() { 539 List<MColumn> pk = new ArrayList<>(3); 540 for (MColumn column : allColumns()) { 541 if (column.isPrimaryKey()) { 542 pk.add(column); 543 } 544 } 545 return pk; 546 } 547 548 /** 549 * Return the primary key column if it is a simple primary key. 550 */ 551 public String singlePrimaryKey() { 552 List<MColumn> columns = primaryKeyColumns(); 553 if (columns.size() == 1) { 554 return columns.get(0).getName(); 555 } 556 return null; 557 } 558 559 private void checkTableName(String tableName) { 560 if (!name.equals(tableName)) { 561 throw new IllegalArgumentException("addColumn tableName [" + tableName + "] does not match [" + name + "]"); 562 } 563 } 564 565 /** 566 * Add a column via migration data. 567 */ 568 private void addColumn(Column column) { 569 columns.put(column.getName(), new MColumn(column)); 570 } 571 572 /** 573 * Add a model column (typically from EbeanServer meta data). 574 */ 575 public void addColumn(MColumn column) { 576 columns.put(column.getName(), column); 577 } 578 579 /** 580 * Add a unique constraint. 581 */ 582 public void addUniqueConstraint(MCompoundUniqueConstraint uniqueConstraint) { 583 uniqueConstraints.add(uniqueConstraint); 584 } 585 586 /** 587 * Add a compound foreign key. 588 */ 589 public void addForeignKey(MCompoundForeignKey compoundKey) { 590 compoundKeys.add(compoundKey); 591 } 592 593 /** 594 * Add a column checking if it already exists and if so return the existing column. 595 * Sometimes the case for a primaryKey that is also a foreign key. 596 */ 597 public MColumn addColumn(String dbCol, String columnDefn, boolean notnull) { 598 599 MColumn existingColumn = getColumn(dbCol); 600 if (existingColumn != null) { 601 if (notnull) { 602 existingColumn.setNotnull(true); 603 } 604 return existingColumn; 605 } 606 607 MColumn newCol = new MColumn(dbCol, columnDefn, notnull); 608 addColumn(newCol); 609 return newCol; 610 } 611 612 /** 613 * Add a 'new column' to the AddColumn migration object. 614 */ 615 private void diffNewColumn(MColumn newColumn) { 616 617 if (addColumn == null) { 618 addColumn = new AddColumn(); 619 addColumn.setTableName(name); 620 if (withHistory) { 621 // These addColumns need to occur on the history 622 // table as well as the base table 623 addColumn.setWithHistory(Boolean.TRUE); 624 } 625 } 626 627 addColumn.getColumn().add(newColumn.createColumn()); 628 } 629 630 /** 631 * Add a 'drop column' to the diff. 632 */ 633 private void diffDropColumn(ModelDiff modelDiff, MColumn existingColumn) { 634 635 DropColumn dropColumn = new DropColumn(); 636 dropColumn.setTableName(name); 637 dropColumn.setColumnName(existingColumn.getName()); 638 if (withHistory) { 639 // These dropColumns should occur on the history 640 // table as well as the base table 641 dropColumn.setWithHistory(Boolean.TRUE); 642 } 643 644 modelDiff.addDropColumn(dropColumn); 645 } 646 647 /** 648 * Register a pending un-applied drop column. 649 * <p> 650 * This means this column still needs to be included in history views/triggers etc even 651 * though it is not part of the current model. 652 */ 653 public void registerPendingDropColumn(String columnName) { 654 droppedColumns.add(columnName); 655 } 656 657 /** 658 * Check if there are duplicate foreign keys. 659 * <p> 660 * This can occur when an ManyToMany relates back to itself. 661 * </p> 662 */ 663 public void checkDuplicateForeignKeys() { 664 665 if (hasDuplicateForeignKeys()) { 666 int counter = 1; 667 for (MCompoundForeignKey fk : compoundKeys) { 668 fk.addNameSuffix(counter++); 669 } 670 } 671 } 672 673 /** 674 * Return true if the foreign key names are not unique. 675 */ 676 private boolean hasDuplicateForeignKeys() { 677 Set<String> fkNames = new HashSet<>(); 678 for (MCompoundForeignKey fk : compoundKeys) { 679 if (!fkNames.add(fk.getName())) { 680 return true; 681 } 682 } 683 return false; 684 } 685 686 /** 687 * Adjust the references (FK) if it should relate to a draft table. 688 */ 689 public void adjustReferences(ModelContainer modelContainer) { 690 691 Collection<MColumn> cols = allColumns(); 692 for (MColumn col : cols) { 693 String references = col.getReferences(); 694 if (references != null) { 695 String baseTable = extractBaseTable(references); 696 MTable refBaseTable = modelContainer.getTable(baseTable); 697 if (refBaseTable.draftTable != null) { 698 // change references to another associated 'draft' table 699 String newReferences = deriveReferences(references, refBaseTable.draftTable.getName()); 700 col.setReferences(newReferences); 701 } 702 } 703 } 704 } 705 706 /** 707 * Return the base table name from references (table.column). 708 */ 709 private String extractBaseTable(String references) { 710 int lastDot = references.lastIndexOf('.'); 711 return references.substring(0, lastDot); 712 } 713 714 /** 715 * Return the new references using the given draftTableName. 716 * (The referenced column is the same as before). 717 */ 718 private String deriveReferences(String references, String draftTableName) { 719 int lastDot = references.lastIndexOf('.'); 720 return draftTableName + "." + references.substring(lastDot + 1); 721 } 722 723 /** 724 * This method adds information which columns are nullable or not to the compound indices. 725 */ 726 public void updateCompoundIndices() { 727 for (MCompoundUniqueConstraint uniq : uniqueConstraints) { 728 List<String> nullableColumns = new ArrayList<>(); 729 for (String columnName : uniq.getColumns()) { 730 MColumn col = getColumn(columnName); 731 if (col != null && !col.isNotnull()) { 732 nullableColumns.add(columnName); 733 } 734 } 735 uniq.setNullableColumns(nullableColumns.toArray(new String[0])); 736 } 737 } 738 739 public void removeForeignKey(String name) { 740 compoundKeys.removeIf(fk -> fk.getName().equals(name)); 741 } 742 743 /** 744 * Clear the indexes on the foreign keys as they are covered by unique constraints. 745 */ 746 public void clearForeignKeyIndexes() { 747 for (MCompoundForeignKey compoundKey : compoundKeys) { 748 compoundKey.setIndexName(null); 749 } 750 } 751 752 /** 753 * Clear foreign key as this element collection table logically references 754 * back to multiple tables. 755 */ 756 public MIndex setReusedElementCollection() { 757 MIndex index = null; 758 for (MColumn column : columns.values()) { 759 final String references = column.getReferences(); 760 if (references != null) { 761 index = new MIndex(column.getForeignKeyIndex(), name, column.getName()); 762 column.clearForeignKey(); 763 } 764 } 765 return index; 766 } 767}