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.createJoinTable()) { 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 // only allow Nonnull if embedded is Nonnull 179 visitScalar(p, !embedded.isNullable()); 180 } 181 if (embedded.isId()) { 182 // compound primary key 183 lastColumn.setPrimaryKey(true); 184 } 185 } 186 187 @Override 188 public void visitOneImported(BeanPropertyAssocOne<?> p) { 189 190 TableJoinColumn[] columns = p.getTableJoin().columns(); 191 if (columns.length == 0) { 192 throw new RuntimeException("No join columns for " + p.getFullBeanName()); 193 } 194 195 ImportedId importedId = p.getImportedId(); 196 197 List<MColumn> modelColumns = new ArrayList<>(columns.length); 198 199 MCompoundForeignKey compoundKey = null; 200 if (columns.length > 1) { 201 // compound foreign key 202 String refTable = p.getTargetDescriptor().getBaseTable(); 203 String fkName = foreignKeyConstraintName(p.getName()); 204 String fkIndex = foreignKeyIndexName(p.getName()); 205 compoundKey = new MCompoundForeignKey(fkName, refTable, fkIndex); 206 table.addForeignKey(compoundKey); 207 } 208 209 for (TableJoinColumn column : columns) { 210 211 String dbCol = column.getLocalDbColumn(); 212 BeanProperty importedProperty = importedId.findMatchImport(dbCol); 213 if (importedProperty == null) { 214 throw new RuntimeException("Imported BeanProperty not found?"); 215 } 216 String columnDefn = ctx.getColumnDefn(importedProperty, true); 217 String refColumn = importedProperty.getDbColumn(); 218 219 MColumn col = table.addColumn(dbCol, columnDefn, !p.isNullable()); 220 col.setDbMigrationInfos(p.getDbMigrationInfos()); 221 col.setDefaultValue(p.getDbColumnDefault()); 222 if (columns.length == 1) { 223 if (p.hasForeignKeyConstraint() && !importedProperty.getBeanDescriptor().suppressForeignKey()) { 224 // single references column (put it on the column) 225 String refTable = importedProperty.getBeanDescriptor().getBaseTable(); 226 if (refTable == null) { 227 // odd case where an EmbeddedId only has 1 property 228 refTable = p.getTargetDescriptor().getBaseTable(); 229 } 230 col.setReferences(refTable + "." + refColumn); 231 col.setForeignKeyName(foreignKeyConstraintName(col.getName())); 232 if (p.hasForeignKeyIndex()) { 233 col.setForeignKeyIndex(foreignKeyIndexName(col.getName())); 234 } 235 PropertyForeignKey foreignKey = p.getForeignKey(); 236 if (foreignKey != null) { 237 col.setForeignKeyModes(foreignKey.getOnDelete(), foreignKey.getOnUpdate()); 238 } 239 } 240 } else { 241 compoundKey.addColumnPair(dbCol, refColumn); 242 } 243 modelColumns.add(col); 244 } 245 246 if (p.isOneToOne()) { 247 // adding the unique constraint restricts the cardinality from OneToMany down to OneToOne 248 // for MsSqlServer we need different DDL to handle NULL values on this constraint 249 if (modelColumns.size() == 1) { 250 MColumn col = modelColumns.get(0); 251 indexSetAdd(col.getName()); 252 col.setUniqueOneToOne(uniqueConstraintName(col.getName())); 253 254 } else { 255 String[] cols = indexSetAdd(toColumnNames(modelColumns)); 256 String uqName = uniqueConstraintName(p.getName()); 257 table.addUniqueConstraint(new MCompoundUniqueConstraint(cols, uqName)); 258 } 259 } 260 } 261 262 @Override 263 public void visitScalar(BeanProperty p, boolean allowNonNull) { 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 (allowNonNull && (!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}