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