001package io.ebeaninternal.dbmigration.model.build; 002 003import io.ebean.annotation.Platform; 004import io.ebeaninternal.dbmigration.ddlgeneration.platform.util.IndexSet; 005import io.ebeaninternal.dbmigration.model.MColumn; 006import io.ebeaninternal.dbmigration.model.MCompoundForeignKey; 007import io.ebeaninternal.dbmigration.model.MCompoundUniqueConstraint; 008import io.ebeaninternal.dbmigration.model.MIndex; 009import io.ebeaninternal.dbmigration.model.MTable; 010import io.ebeaninternal.server.deploy.visitor.BaseTablePropertyVisitor; 011import io.ebeaninternal.server.deploy.BeanDescriptor; 012import io.ebeaninternal.server.deploy.BeanProperty; 013import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; 014import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; 015import io.ebeaninternal.server.deploy.IndexDefinition; 016import io.ebeaninternal.server.deploy.InheritInfo; 017import io.ebeaninternal.server.deploy.PropertyForeignKey; 018import io.ebeaninternal.server.deploy.TableJoin; 019import io.ebeaninternal.server.deploy.TableJoinColumn; 020import io.ebeaninternal.server.deploy.id.ImportedId; 021 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.List; 025import java.util.Set; 026import java.util.StringJoiner; 027 028/** 029 * Used as part of ModelBuildBeanVisitor and generally adds the MColumn to the associated 030 * MTable model objects. 031 */ 032public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor { 033 034 protected final ModelBuildContext ctx; 035 036 private final MTable table; 037 038 private final BeanDescriptor<?> beanDescriptor; 039 040 private final IndexSet indexSet = new IndexSet(); 041 042 private MColumn lastColumn; 043 044 private int countForeignKey; 045 private int countIndex; 046 private int countUnique; 047 private int countCheck; 048 049 public ModelBuildPropertyVisitor(ModelBuildContext ctx, MTable table, BeanDescriptor<?> beanDescriptor) { 050 this.ctx = ctx; 051 this.table = table; 052 this.beanDescriptor = beanDescriptor; 053 addIndexes(beanDescriptor.getIndexDefinitions()); 054 } 055 056 /** 057 * Add unique constraints defined via JPA UniqueConstraint annotations. 058 */ 059 private void addIndexes(IndexDefinition[] indexes) { 060 if (indexes != null) { 061 for (IndexDefinition index : indexes) { 062 String[] columns = index.getColumns(); 063 indexSet.add(columns); 064 if (index.isUniqueConstraint()) { 065 table.addUniqueConstraint(createMUniqueConstraint(index, columns)); 066 } else { 067 // 'just' an index (not a unique constraint) 068 ctx.addIndex(createMIndex(indexName(index), table.getName(), index)); 069 } 070 } 071 } 072 } 073 074 private MCompoundUniqueConstraint createMUniqueConstraint(IndexDefinition index, String[] columns) { 075 return new MCompoundUniqueConstraint(columns, false, uniqueConstraintName(index), platforms(index.getPlatforms())); 076 } 077 078 private String uniqueConstraintName(IndexDefinition index) { 079 String uqName = index.getName(); 080 if (uqName == null || uqName.trim().isEmpty()) { 081 return uniqueConstraintName(index.getColumns()); 082 } 083 return uqName; 084 } 085 086 private String indexName(IndexDefinition index) { 087 String idxName = index.getName(); 088 if (idxName == null || idxName.trim().isEmpty()) { 089 idxName = indexName(index.getColumns()); 090 } 091 return idxName; 092 } 093 094 private MIndex createMIndex(String indexName, String tableName, IndexDefinition index) { 095 return new MIndex(indexName, tableName, index.getColumns(), platforms(index.getPlatforms()), index.isUnique(), index.isConcurrent(), index.getDefinition()); 096 } 097 098 private String platforms(Platform[] platforms) { 099 if (platforms == null || platforms.length == 0) { 100 return null; 101 } 102 StringJoiner joiner = new StringJoiner(","); 103 for (Platform platform : platforms) { 104 joiner.add(platform.name()); 105 } 106 return joiner.toString(); 107 } 108 109 @Override 110 public void visitEnd() { 111 112 // set the primary key name 113 table.setPkName(primaryKeyName()); 114 115 // check if indexes on foreign keys should be suppressed 116 for (MColumn column : table.allColumns()) { 117 if (hasValue(column.getForeignKeyIndex())) { 118 if (indexSet.contains(column.getName())) { 119 // suppress index on foreign key as there is already 120 // effectively an index (probably via unique constraint) 121 column.setForeignKeyIndex(null); 122 } 123 } 124 } 125 126 for (MCompoundForeignKey compoundKey : table.getCompoundKeys()) { 127 if (indexSet.contains(compoundKey.getColumns())) { 128 // suppress index on foreign key as there is already 129 // effectively an index (probably via unique constraint) 130 compoundKey.setIndexName(null); 131 } 132 } 133 134 addDraftTable(); 135 table.updateCompoundIndices(); 136 } 137 138 /** 139 * Create a 'draft' table that is mostly the same as the base table. 140 * It has @DraftOnly columns and adjusted primary and foreign keys. 141 */ 142 private void addDraftTable() { 143 if (beanDescriptor.isDraftable() || beanDescriptor.isDraftableElement()) { 144 // create a 'Draft' table which looks very similar (change PK, FK etc) 145 ctx.createDraft(table, !beanDescriptor.isDraftableElement()); 146 } 147 } 148 149 150 @Override 151 public void visitMany(BeanPropertyAssocMany<?> p) { 152 if (p.hasJoinTable() && p.getMappedBy() == null) { 153 // only create on other 'owning' side 154 155 // build the create table and fkey constraints 156 // putting the DDL into ctx for later output as we are 157 // in the middle of rendering the create table DDL 158 MTable intersectionTable = new ModelBuildIntersectionTable(ctx, p).build(); 159 if (p.isO2mJoinTable()) { 160 intersectionTable.clearForeignKeyIndexes(); 161 Collection<MColumn> cols = intersectionTable.allColumns(); 162 if (cols.size() == 2) { 163 // always the second column that we put the unique constraint on 164 MColumn col = new ArrayList<>(cols).get(1); 165 col.setUnique(uniqueConstraintName(col.getName())); 166 } 167 } 168 } else if (p.isElementCollection()) { 169 ModelBuildElementTable.build(ctx, p); 170 } 171 } 172 173 @Override 174 public void visitEmbeddedScalar(BeanProperty p, BeanPropertyAssocOne<?> embedded) { 175 if (p instanceof BeanPropertyAssocOne) { 176 visitOneImported((BeanPropertyAssocOne<?>)p); 177 } else { 178 visitScalar(p); 179 } 180 if (embedded.isId()) { 181 // compound primary key 182 lastColumn.setPrimaryKey(true); 183 } 184 } 185 186 @Override 187 public void visitOneImported(BeanPropertyAssocOne<?> p) { 188 189 TableJoinColumn[] columns = p.getTableJoin().columns(); 190 if (columns.length == 0) { 191 throw new RuntimeException("No join columns for " + p.getFullBeanName()); 192 } 193 194 ImportedId importedId = p.getImportedId(); 195 196 List<MColumn> modelColumns = new ArrayList<>(columns.length); 197 198 MCompoundForeignKey compoundKey = null; 199 if (columns.length > 1) { 200 // compound foreign key 201 String refTable = p.getTargetDescriptor().getBaseTable(); 202 String fkName = foreignKeyConstraintName(p.getName()); 203 String fkIndex = foreignKeyIndexName(p.getName()); 204 compoundKey = new MCompoundForeignKey(fkName, refTable, fkIndex); 205 table.addForeignKey(compoundKey); 206 } 207 208 for (TableJoinColumn column : columns) { 209 210 String dbCol = column.getLocalDbColumn(); 211 BeanProperty importedProperty = importedId.findMatchImport(dbCol); 212 if (importedProperty == null) { 213 throw new RuntimeException("Imported BeanProperty not found?"); 214 } 215 String columnDefn = ctx.getColumnDefn(importedProperty, true); 216 String refColumn = importedProperty.getDbColumn(); 217 218 MColumn col = table.addColumn(dbCol, columnDefn, !p.isNullable()); 219 col.setDbMigrationInfos(p.getDbMigrationInfos()); 220 col.setDefaultValue(p.getDbColumnDefault()); 221 if (columns.length == 1) { 222 if (p.hasForeignKeyConstraint() && !importedProperty.getBeanDescriptor().suppressForeignKey()) { 223 // single references column (put it on the column) 224 String refTable = importedProperty.getBeanDescriptor().getBaseTable(); 225 if (refTable == null) { 226 // odd case where an EmbeddedId only has 1 property 227 refTable = p.getTargetDescriptor().getBaseTable(); 228 } 229 col.setReferences(refTable + "." + refColumn); 230 col.setForeignKeyName(foreignKeyConstraintName(col.getName())); 231 if (p.hasForeignKeyIndex()) { 232 col.setForeignKeyIndex(foreignKeyIndexName(col.getName())); 233 } 234 PropertyForeignKey foreignKey = p.getForeignKey(); 235 if (foreignKey != null) { 236 col.setForeignKeyModes(foreignKey.getOnDelete(), foreignKey.getOnUpdate()); 237 } 238 } 239 } else { 240 compoundKey.addColumnPair(dbCol, refColumn); 241 } 242 modelColumns.add(col); 243 } 244 245 if (p.isOneToOne()) { 246 // adding the unique constraint restricts the cardinality from OneToMany down to OneToOne 247 // for MsSqlServer we need different DDL to handle NULL values on this constraint 248 if (modelColumns.size() == 1) { 249 MColumn col = modelColumns.get(0); 250 indexSetAdd(col.getName()); 251 col.setUniqueOneToOne(uniqueConstraintName(col.getName())); 252 253 } else { 254 String[] cols = indexSetAdd(toColumnNames(modelColumns)); 255 String uqName = uniqueConstraintName(p.getName()); 256 table.addUniqueConstraint(new MCompoundUniqueConstraint(cols, uqName)); 257 } 258 } 259 } 260 261 @Override 262 public void visitScalar(BeanProperty p) { 263 264 if (p.isSecondaryTable()) { 265 lastColumn = null; 266 return; 267 } 268 269 // using non-strict mode to render the DB type such that we have a 270 // "logical" type like jsonb(200) that can map to JSONB or VARCHAR(200) 271 MColumn col = new MColumn(p.getDbColumn(), ctx.getColumnDefn(p, false)); 272 col.setComment(p.getDbComment()); 273 col.setDraftOnly(p.isDraftOnly()); 274 col.setHistoryExclude(p.isExcludedFromHistory()); 275 276 if (p.isId()) { 277 col.setPrimaryKey(true); 278 if (p.getBeanDescriptor().isUseIdGenerator()) { 279 col.setIdentity(true); 280 } 281 TableJoin primaryKeyJoin = p.getBeanDescriptor().getPrimaryKeyJoin(); 282 if (primaryKeyJoin != null && !table.isPartitioned()) { 283 final PropertyForeignKey foreignKey = primaryKeyJoin.getForeignKey(); 284 if (foreignKey == null || !foreignKey.isNoConstraint()) { 285 TableJoinColumn[] columns = primaryKeyJoin.columns(); 286 col.setReferences(primaryKeyJoin.getTable() + "." + columns[0].getForeignDbColumn()); 287 col.setForeignKeyName(foreignKeyConstraintName(col.getName())); 288 if (foreignKey != null) { 289 col.setForeignKeyModes(foreignKey.getOnDelete(), foreignKey.getOnUpdate()); 290 } 291 } 292 } 293 } else { 294 col.setDefaultValue(p.getDbColumnDefault()); 295 if (!p.isNullable() || p.isDDLNotNull()) { 296 col.setNotnull(true); 297 } 298 } 299 300 col.setDbMigrationInfos(p.getDbMigrationInfos()); 301 302 if (p.isUnique() && !p.isId()) { 303 col.setUnique(uniqueConstraintName(col.getName())); 304 indexSetAdd(col.getName()); 305 } 306 Set<String> checkConstraintValues = p.getDbCheckConstraintValues(); 307 if (checkConstraintValues != null) { 308 if (beanDescriptor.hasInheritance()) { 309 InheritInfo inheritInfo = beanDescriptor.getInheritInfo(); 310 inheritInfo.appendCheckConstraintValues(p.getName(), checkConstraintValues); 311 } 312 col.setCheckConstraint(buildCheckConstraint(p.getDbColumn(), checkConstraintValues)); 313 col.setCheckConstraintName(checkConstraintName(col.getName())); 314 } 315 316 lastColumn = col; 317 table.addColumn(col); 318 } 319 320 /** 321 * Build the check constraint clause given the db column and values. 322 */ 323 private String buildCheckConstraint(String dbColumn, Set<String> checkConstraintValues) { 324 StringBuilder sb = new StringBuilder(); 325 sb.append("check ( ").append(dbColumn).append(" in ("); 326 int count = 0; 327 for (String value : checkConstraintValues) { 328 if (count++ > 0) { 329 sb.append(","); 330 } 331 sb.append(value); 332 } 333 sb.append("))"); 334 return sb.toString(); 335 } 336 337 private void indexSetAdd(String column) { 338 indexSet.add(column); 339 } 340 341 private String[] indexSetAdd(String[] cols) { 342 indexSet.add(cols); 343 return cols; 344 } 345 346 private String[] toColumnNames(List<MColumn> modelColumns) { 347 String[] cols = new String[modelColumns.size()]; 348 for (int i = 0; i < modelColumns.size(); i++) { 349 cols[i] = modelColumns.get(i).getName(); 350 } 351 return cols; 352 } 353 354 /** 355 * Return the primary key constraint name. 356 */ 357 protected String primaryKeyName() { 358 return ctx.primaryKeyName(table.getName()); 359 } 360 361 /** 362 * Return the foreign key constraint name given a single column foreign key. 363 */ 364 protected String foreignKeyConstraintName(String columnName) { 365 return ctx.foreignKeyConstraintName(table.getName(), columnName, ++countForeignKey); 366 } 367 368 protected String foreignKeyIndexName(String column) { 369 String[] cols = {column}; 370 return foreignKeyIndexName(cols); 371 } 372 373 /** 374 * Return the foreign key constraint name given a single column foreign key. 375 */ 376 protected String foreignKeyIndexName(String[] columns) { 377 return ctx.foreignKeyIndexName(table.getName(), columns, ++countIndex); 378 } 379 380 /** 381 * Return the index name given multiple columns. 382 */ 383 protected String indexName(String[] columns) { 384 return ctx.indexName(table.getName(), columns, ++countIndex); 385 } 386 387 /** 388 * Return the unique constraint name. 389 */ 390 protected String uniqueConstraintName(String columnName) { 391 return ctx.uniqueConstraintName(table.getName(), columnName, ++countUnique); 392 } 393 394 /** 395 * Return the unique constraint name. 396 */ 397 protected String uniqueConstraintName(String[] columnNames) { 398 return ctx.uniqueConstraintName(table.getName(), columnNames, ++countUnique); 399 } 400 401 /** 402 * Return the constraint name. 403 */ 404 protected String checkConstraintName(String columnName) { 405 return ctx.checkConstraintName(table.getName(), columnName, ++countCheck); 406 } 407 408 private boolean hasValue(String val) { 409 return val != null && !val.isEmpty(); 410 } 411 412}