Skip to content

Commit 6d1c9be

Browse files
kobaeugeneaschauder
authored andcommitted
Add support for forein keys in schema generation within aggregates.
Closes #1599 Related tickets #756, #1600
1 parent 6736d83 commit 6d1c9be

File tree

11 files changed

+386
-36
lines changed

11 files changed

+386
-36
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.springframework.data.jdbc.core.mapping.schema;
2+
3+
import java.util.Objects;
4+
5+
/**
6+
* Models a Foreign Key for generating SQL for Schema generation.
7+
*
8+
* @author Evgenii Koba
9+
* @since 3.2
10+
*/
11+
record ForeignKey(String name, String tableName, String columnName, String referencedTableName,
12+
String referencedColumnName) {
13+
@Override
14+
public boolean equals(Object o) {
15+
if (this == o)
16+
return true;
17+
if (o == null || getClass() != o.getClass())
18+
return false;
19+
ForeignKey that = (ForeignKey) o;
20+
return Objects.equals(name, that.name);
21+
}
22+
23+
@Override
24+
public int hashCode() {
25+
return Objects.hash(name);
26+
}
27+
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriter.java

+63-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
import liquibase.change.ColumnConfig;
2222
import liquibase.change.ConstraintsConfig;
2323
import liquibase.change.core.AddColumnChange;
24+
import liquibase.change.core.AddForeignKeyConstraintChange;
2425
import liquibase.change.core.CreateTableChange;
2526
import liquibase.change.core.DropColumnChange;
27+
import liquibase.change.core.DropForeignKeyConstraintChange;
2628
import liquibase.change.core.DropTableChange;
2729
import liquibase.changelog.ChangeLogChild;
2830
import liquibase.changelog.ChangeLogParameters;
@@ -52,6 +54,7 @@
5254
import java.util.Set;
5355
import java.util.function.BiPredicate;
5456
import java.util.function.Predicate;
57+
import java.util.stream.Collectors;
5558

5659
import org.springframework.core.io.Resource;
5760
import org.springframework.data.mapping.context.MappingContext;
@@ -321,15 +324,15 @@ private ChangeSet createChangeSet(ChangeSetMetadata metadata, SchemaDiff differe
321324
private SchemaDiff initial() {
322325

323326
Tables mappedEntities = Tables.from(mappingContext.getPersistentEntities().stream().filter(schemaFilter),
324-
sqlTypeMapping, null);
327+
sqlTypeMapping, null, mappingContext);
325328
return SchemaDiff.diff(mappedEntities, Tables.empty(), nameComparator);
326329
}
327330

328331
private SchemaDiff differenceOf(Database database) throws LiquibaseException {
329332

330333
Tables existingTables = getLiquibaseModel(database);
331334
Tables mappedEntities = Tables.from(mappingContext.getPersistentEntities().stream().filter(schemaFilter),
332-
sqlTypeMapping, database.getDefaultCatalogName());
335+
sqlTypeMapping, database.getDefaultCatalogName(), mappingContext);
333336

334337
return SchemaDiff.diff(mappedEntities, existingTables, nameComparator);
335338
}
@@ -362,6 +365,13 @@ private DatabaseChangeLog getDatabaseChangeLog(File changeLogFile, @Nullable Dat
362365

363366
private void generateTableAdditionsDeletions(ChangeSet changeSet, SchemaDiff difference) {
364367

368+
for (Table table : difference.tableDeletions()) {
369+
for (ForeignKey foreignKey : table.foreignKeys()) {
370+
DropForeignKeyConstraintChange dropForeignKey = dropForeignKey(foreignKey);
371+
changeSet.addChange(dropForeignKey);
372+
}
373+
}
374+
365375
for (Table table : difference.tableAdditions()) {
366376
CreateTableChange newTable = changeTable(table);
367377
changeSet.addChange(newTable);
@@ -373,12 +383,24 @@ private void generateTableAdditionsDeletions(ChangeSet changeSet, SchemaDiff dif
373383
changeSet.addChange(dropTable(table));
374384
}
375385
}
386+
387+
for (Table table : difference.tableAdditions()) {
388+
for (ForeignKey foreignKey : table.foreignKeys()) {
389+
AddForeignKeyConstraintChange addForeignKey = addForeignKey(foreignKey);
390+
changeSet.addChange(addForeignKey);
391+
}
392+
}
376393
}
377394

378395
private void generateTableModifications(ChangeSet changeSet, SchemaDiff difference) {
379396

380397
for (TableDiff table : difference.tableDiffs()) {
381398

399+
for (ForeignKey foreignKey : table.fkToDrop()) {
400+
DropForeignKeyConstraintChange dropForeignKey = dropForeignKey(foreignKey);
401+
changeSet.addChange(dropForeignKey);
402+
}
403+
382404
if (!table.columnsToAdd().isEmpty()) {
383405
changeSet.addChange(addColumns(table));
384406
}
@@ -388,6 +410,11 @@ private void generateTableModifications(ChangeSet changeSet, SchemaDiff differen
388410
if (!deletedColumns.isEmpty()) {
389411
changeSet.addChange(dropColumns(table, deletedColumns));
390412
}
413+
414+
for (ForeignKey foreignKey : table.fkToAdd()) {
415+
AddForeignKeyConstraintChange addForeignKey = addForeignKey(foreignKey);
416+
changeSet.addChange(addForeignKey);
417+
}
391418
}
392419
}
393420

@@ -444,12 +471,27 @@ private Tables getLiquibaseModel(Database targetDatabase) throws LiquibaseExcept
444471
tableModel.columns().add(columnModel);
445472
}
446473

474+
tableModel.foreignKeys().addAll(extractForeignKeys(table));
475+
447476
existingTables.add(tableModel);
448477
}
449478

450479
return new Tables(existingTables);
451480
}
452481

482+
private static List<ForeignKey> extractForeignKeys(liquibase.structure.core.Table table) {
483+
484+
return table.getOutgoingForeignKeys().stream().map(foreignKey -> {
485+
String tableName = foreignKey.getForeignKeyTable().getName();
486+
String columnName = foreignKey.getForeignKeyColumns().stream().findFirst()
487+
.map(liquibase.structure.core.Column::getName).get();
488+
String referencedTableName = foreignKey.getPrimaryKeyTable().getName();
489+
String referencedColumnName = foreignKey.getPrimaryKeyColumns().stream().findFirst()
490+
.map(liquibase.structure.core.Column::getName).get();
491+
return new ForeignKey(foreignKey.getName(), tableName, columnName, referencedTableName, referencedColumnName);
492+
}).collect(Collectors.toList());
493+
}
494+
453495
private static AddColumnChange addColumns(TableDiff table) {
454496

455497
AddColumnChange addColumnChange = new AddColumnChange();
@@ -532,6 +574,25 @@ private static DropTableChange dropTable(Table table) {
532574
return change;
533575
}
534576

577+
private static AddForeignKeyConstraintChange addForeignKey(ForeignKey foreignKey) {
578+
579+
AddForeignKeyConstraintChange change = new AddForeignKeyConstraintChange();
580+
change.setConstraintName(foreignKey.name());
581+
change.setBaseTableName(foreignKey.tableName());
582+
change.setBaseColumnNames(foreignKey.columnName());
583+
change.setReferencedTableName(foreignKey.referencedTableName());
584+
change.setReferencedColumnNames(foreignKey.referencedColumnName());
585+
return change;
586+
}
587+
588+
private static DropForeignKeyConstraintChange dropForeignKey(ForeignKey foreignKey) {
589+
590+
DropForeignKeyConstraintChange change = new DropForeignKeyConstraintChange();
591+
change.setConstraintName(foreignKey.name());
592+
change.setBaseTableName(foreignKey.tableName());
593+
return change;
594+
}
595+
535596
/**
536597
* Metadata for a ChangeSet.
537598
*/

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/SchemaDiff.java

+22-24
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.jdbc.core.mapping.schema;
1717

1818
import java.util.ArrayList;
19+
import java.util.Collection;
1920
import java.util.Comparator;
2021
import java.util.List;
2122
import java.util.Map;
@@ -91,43 +92,40 @@ private static List<TableDiff> diffTable(Tables mappedEntities, Map<String, Tabl
9192
TableDiff tableDiff = new TableDiff(mappedEntity);
9293

9394
Map<String, Column> mappedColumns = createMapping(mappedEntity.columns(), Column::name, nameComparator);
94-
mappedEntity.keyColumns().forEach(it -> mappedColumns.put(it.name(), it));
95-
9695
Map<String, Column> existingColumns = createMapping(existingTable.columns(), Column::name, nameComparator);
97-
existingTable.keyColumns().forEach(it -> existingColumns.put(it.name(), it));
98-
9996
// Identify deleted columns
100-
Map<String, Column> toDelete = new TreeMap<>(nameComparator);
101-
toDelete.putAll(existingColumns);
102-
mappedColumns.keySet().forEach(toDelete::remove);
103-
104-
tableDiff.columnsToDrop().addAll(toDelete.values());
105-
106-
// Identify added columns
107-
Map<String, Column> addedColumns = new TreeMap<>(nameComparator);
108-
addedColumns.putAll(mappedColumns);
109-
110-
existingColumns.keySet().forEach(addedColumns::remove);
111-
112-
// Add columns in order. This order can interleave with existing columns.
113-
for (Column column : mappedEntity.keyColumns()) {
114-
if (addedColumns.containsKey(column.name())) {
115-
tableDiff.columnsToAdd().add(column);
116-
}
117-
}
118-
97+
tableDiff.columnsToDrop().addAll(findDiffs(mappedColumns, existingColumns, nameComparator));
98+
// Identify added columns and add columns in order. This order can interleave with existing columns.
99+
List<Column> addedColumns = new ArrayList<>(findDiffs(existingColumns, mappedColumns, nameComparator));
119100
for (Column column : mappedEntity.columns()) {
120-
if (addedColumns.containsKey(column.name())) {
101+
if (addedColumns.contains(column)) {
121102
tableDiff.columnsToAdd().add(column);
122103
}
123104
}
124105

106+
Map<String, ForeignKey> mappedForeignKeys = createMapping(mappedEntity.foreignKeys(), ForeignKey::name,
107+
nameComparator);
108+
Map<String, ForeignKey> existingForeignKeys = createMapping(existingTable.foreignKeys(), ForeignKey::name,
109+
nameComparator);
110+
// Identify deleted columns
111+
tableDiff.fkToDrop().addAll(findDiffs(mappedForeignKeys, existingForeignKeys, nameComparator));
112+
// Identify added columns
113+
tableDiff.fkToAdd().addAll(findDiffs(existingForeignKeys, mappedForeignKeys, nameComparator));
114+
125115
tableDiffs.add(tableDiff);
126116
}
127117

128118
return tableDiffs;
129119
}
130120

121+
private static <T> Collection<T> findDiffs(Map<String, T> baseMapping, Map<String, T> toCompareMapping,
122+
Comparator<String> nameComparator) {
123+
Map<String, T> diff = new TreeMap<>(nameComparator);
124+
diff.putAll(toCompareMapping);
125+
baseMapping.keySet().forEach(diff::remove);
126+
return diff.values();
127+
}
128+
131129
private static <T> SortedMap<String, T> createMapping(List<T> items, Function<T, String> keyFunction,
132130
Comparator<String> nameComparator) {
133131

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Table.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
* @author Kurt Niemi
2828
* @since 3.2
2929
*/
30-
record Table(@Nullable String schema, String name, List<Column> keyColumns, List<Column> columns) {
30+
record Table(@Nullable String schema, String name, List<Column> columns, List<ForeignKey> foreignKeys) {
3131

3232
public Table(@Nullable String schema, String name) {
3333
this(schema, name, new ArrayList<>(), new ArrayList<>());

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/TableDiff.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@
2525
* @author Kurt Niemi
2626
* @since 3.2
2727
*/
28-
record TableDiff(Table table, List<Column> columnsToAdd, List<Column> columnsToDrop) {
28+
record TableDiff(Table table, List<Column> columnsToAdd, List<Column> columnsToDrop, List<ForeignKey> fkToAdd,
29+
List<ForeignKey> fkToDrop) {
2930

3031
public TableDiff(Table table) {
31-
this(table, new ArrayList<>(), new ArrayList<>());
32+
this(table, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
3233
}
3334

3435
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Tables.java

+72-7
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,19 @@
1515
*/
1616
package org.springframework.data.jdbc.core.mapping.schema;
1717

18+
import java.util.ArrayList;
1819
import java.util.Collections;
20+
import java.util.HashMap;
1921
import java.util.LinkedHashSet;
2022
import java.util.List;
23+
import java.util.Map;
2124
import java.util.Set;
2225
import java.util.stream.Collectors;
2326
import java.util.stream.Stream;
2427

2528
import org.springframework.data.annotation.Id;
29+
import org.springframework.data.mapping.context.MappingContext;
30+
import org.springframework.data.relational.core.mapping.MappedCollection;
2631
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
2732
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
2833
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@@ -37,15 +42,16 @@
3742
record Tables(List<Table> tables) {
3843

3944
public static Tables from(RelationalMappingContext context) {
40-
return from(context.getPersistentEntities().stream(), new DefaultSqlTypeMapping(), null);
45+
return from(context.getPersistentEntities().stream(), new DefaultSqlTypeMapping(), null, context);
4146
}
4247

43-
// TODO: Add support (i.e. create tickets) to support mapped collections, entities, embedded properties, and aggregate
44-
// references.
48+
// TODO: Add support (i.e. create tickets) to support entities, embedded properties, and aggregate references.
4549

4650
public static Tables from(Stream<? extends RelationalPersistentEntity<?>> persistentEntities,
47-
SqlTypeMapping sqlTypeMapping, @Nullable String defaultSchema) {
51+
SqlTypeMapping sqlTypeMapping, @Nullable String defaultSchema,
52+
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context) {
4853

54+
Map<String, List<ColumnWithForeignKey>> colAndFKByTableName = new HashMap<>();
4955
List<Table> tables = persistentEntities
5056
.filter(it -> it.isAnnotationPresent(org.springframework.data.relational.core.mapping.Table.class)) //
5157
.map(entity -> {
@@ -54,26 +60,85 @@ public static Tables from(Stream<? extends RelationalPersistentEntity<?>> persis
5460

5561
Set<RelationalPersistentProperty> identifierColumns = new LinkedHashSet<>();
5662
entity.getPersistentProperties(Id.class).forEach(identifierColumns::add);
63+
collectForeignKeysInfo(entity, context, colAndFKByTableName, sqlTypeMapping);
5764

5865
for (RelationalPersistentProperty property : entity) {
5966

6067
if (property.isEntity() && !property.isEmbedded()) {
6168
continue;
6269
}
6370

64-
String columnType = sqlTypeMapping.getRequiredColumnType(property);
65-
6671
Column column = new Column(property.getColumnName().getReference(), sqlTypeMapping.getColumnType(property),
67-
sqlTypeMapping.isNullable(property), identifierColumns.contains(property));
72+
sqlTypeMapping.isNullable(property), identifierColumns.contains(property));
6873
table.columns().add(column);
6974
}
7075
return table;
7176
}).collect(Collectors.toList());
7277

78+
applyForeignKeys(tables, colAndFKByTableName);
79+
7380
return new Tables(tables);
7481
}
7582

7683
public static Tables empty() {
7784
return new Tables(Collections.emptyList());
7885
}
86+
87+
private static void applyForeignKeys(List<Table> tables,
88+
Map<String, List<ColumnWithForeignKey>> colAndFKByTableName) {
89+
90+
colAndFKByTableName.forEach(
91+
(tableName, colsAndFK) -> tables.stream().filter(table -> table.name().equals(tableName)).forEach(table -> {
92+
93+
colsAndFK.forEach(colAndFK -> {
94+
if (!table.columns().contains(colAndFK.column())) {
95+
table.columns().add(colAndFK.column());
96+
}
97+
});
98+
99+
colsAndFK.forEach(colAndFK -> table.foreignKeys().add(colAndFK.foreignKey()));
100+
}));
101+
}
102+
103+
private static void collectForeignKeysInfo(RelationalPersistentEntity<?> entity,
104+
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context,
105+
Map<String, List<ColumnWithForeignKey>> keyColumnsByTableName, SqlTypeMapping sqlTypeMapping) {
106+
107+
RelationalPersistentProperty identifierColumn = entity.getPersistentProperty(Id.class);
108+
109+
entity.getPersistentProperties(MappedCollection.class).forEach(property -> {
110+
if (property.isEntity()) {
111+
property.getPersistentEntityTypeInformation().forEach(typeInformation -> {
112+
113+
String tableName = context.getRequiredPersistentEntity(typeInformation).getTableName().getReference();
114+
String columnName = property.getReverseColumnName(entity).getReference();
115+
String referencedTableName = entity.getTableName().getReference();
116+
String referencedColumnName = identifierColumn.getColumnName().getReference();
117+
118+
ForeignKey foreignKey = new ForeignKey(getForeignKeyName(referencedTableName, referencedColumnName),
119+
tableName, columnName, referencedTableName, referencedColumnName);
120+
Column column = new Column(columnName, sqlTypeMapping.getColumnType(identifierColumn), true, false);
121+
122+
ColumnWithForeignKey columnWithForeignKey = new ColumnWithForeignKey(column, foreignKey);
123+
keyColumnsByTableName.compute(
124+
context.getRequiredPersistentEntity(typeInformation).getTableName().getReference(), (key, value) -> {
125+
if (value == null) {
126+
return new ArrayList<>(List.of(columnWithForeignKey));
127+
} else {
128+
value.add(columnWithForeignKey);
129+
return value;
130+
}
131+
});
132+
});
133+
}
134+
});
135+
}
136+
137+
//TODO should we place it in BasicRelationalPersistentProperty/BasicRelationalPersistentEntity and generate using NamingStrategy?
138+
private static String getForeignKeyName(String referencedTableName, String referencedColumnName) {
139+
return String.format("%s_%s_fk", referencedTableName, referencedColumnName);
140+
}
141+
142+
private record ColumnWithForeignKey(Column column, ForeignKey foreignKey) {
143+
}
79144
}

0 commit comments

Comments
 (0)