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 // using non-strict mode to render the DB type such that we have a 250 // "logical" type like jsonb(200) that can map to JSONB or VARCHAR(200) 251 MColumn col = table.addColumnScalar(p.dbColumn(), ctx.getColumnDefn(p, false)); 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 if (p.isId() || p.isImportedPrimaryKey()) { 257 col.setPrimaryKey(true); 258 if (p.descriptor().isUseIdGenerator()) { 259 col.setIdentity(true); 260 } 261 TableJoin primaryKeyJoin = p.descriptor().primaryKeyJoin(); 262 if (primaryKeyJoin != null && !table.isPartitioned()) { 263 final PropertyForeignKey foreignKey = primaryKeyJoin.getForeignKey(); 264 if (foreignKey == null || !foreignKey.isNoConstraint()) { 265 TableJoinColumn[] columns = primaryKeyJoin.columns(); 266 col.setReferences(primaryKeyJoin.getTable() + "." + columns[0].getForeignDbColumn()); 267 col.setForeignKeyName(foreignKeyConstraintName(col.getName())); 268 if (foreignKey != null) { 269 col.setForeignKeyModes(foreignKey.getOnDelete(), foreignKey.getOnUpdate()); 270 } 271 } 272 } 273 } else { 274 col.setDefaultValue(p.dbColumnDefault()); 275 if (allowNonNull && (!p.isNullable() || p.isDDLNotNull())) { 276 col.setNotnull(true); 277 } 278 } 279 280 col.setDbMigrationInfos(p.dbMigrationInfos()); 281 if (p.isUnique() && !p.isId()) { 282 col.setUnique(uniqueConstraintName(col.getName())); 283 indexSetAdd(col.getName()); 284 } 285 Set<String> checkConstraintValues = p.dbCheckConstraintValues(); 286 if (checkConstraintValues != null) { 287 if (beanDescriptor.hasInheritance()) { 288 InheritInfo inheritInfo = beanDescriptor.inheritInfo(); 289 inheritInfo.appendCheckConstraintValues(p.name(), checkConstraintValues); 290 } 291 col.setCheckConstraint(buildCheckConstraint(p.dbColumn(), checkConstraintValues)); 292 col.setCheckConstraintName(checkConstraintName(col.getName())); 293 } 294 lastColumn = col; 295 } 296 297 /** 298 * Build the check constraint clause given the db column and values. 299 */ 300 private String buildCheckConstraint(String dbColumn, Set<String> checkConstraintValues) { 301 StringBuilder sb = new StringBuilder(); 302 sb.append("check ( ").append(dbColumn).append(" in ("); 303 int count = 0; 304 for (String value : checkConstraintValues) { 305 if (count++ > 0) { 306 sb.append(","); 307 } 308 sb.append(value); 309 } 310 sb.append("))"); 311 return sb.toString(); 312 } 313 314 private void indexSetAdd(String column) { 315 indexSet.add(column); 316 } 317 318 private String[] indexSetAdd(String[] cols) { 319 indexSet.add(cols); 320 return cols; 321 } 322 323 private String[] toColumnNames(List<MColumn> modelColumns) { 324 String[] cols = new String[modelColumns.size()]; 325 for (int i = 0; i < modelColumns.size(); i++) { 326 cols[i] = modelColumns.get(i).getName(); 327 } 328 return cols; 329 } 330 331 /** 332 * Return the primary key constraint name. 333 */ 334 protected String primaryKeyName() { 335 return ctx.primaryKeyName(table.getName()); 336 } 337 338 /** 339 * Return the foreign key constraint name given a single column foreign key. 340 */ 341 protected String foreignKeyConstraintName(String columnName) { 342 return ctx.foreignKeyConstraintName(table.getName(), columnName, ++countForeignKey); 343 } 344 345 protected String foreignKeyIndexName(String column) { 346 String[] cols = {column}; 347 return foreignKeyIndexName(cols); 348 } 349 350 /** 351 * Return the foreign key constraint name given a single column foreign key. 352 */ 353 protected String foreignKeyIndexName(String[] columns) { 354 return ctx.foreignKeyIndexName(table.getName(), columns, ++countIndex); 355 } 356 357 /** 358 * Return the index name given multiple columns. 359 */ 360 protected String indexName(String[] columns) { 361 return ctx.indexName(table.getName(), columns, ++countIndex); 362 } 363 364 /** 365 * Return the unique constraint name. 366 */ 367 protected String uniqueConstraintName(String columnName) { 368 return ctx.uniqueConstraintName(table.getName(), columnName, ++countUnique); 369 } 370 371 /** 372 * Return the unique constraint name. 373 */ 374 protected String uniqueConstraintName(String[] columnNames) { 375 return ctx.uniqueConstraintName(table.getName(), columnNames, ++countUnique); 376 } 377 378 /** 379 * Return the constraint name. 380 */ 381 protected String checkConstraintName(String columnName) { 382 return ctx.checkConstraintName(table.getName(), columnName, ++countCheck); 383 } 384 385 private boolean hasValue(String val) { 386 return val != null && !val.isEmpty(); 387 } 388 389}