diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/ForeignKey.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/ForeignKey.java new file mode 100644 index 0000000000..d431e1529e --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/ForeignKey.java @@ -0,0 +1,27 @@ +package org.springframework.data.jdbc.core.mapping.schema; + +import java.util.Objects; + +/** + * Models a Foreign Key for generating SQL for Schema generation. + * + * @author Evgenii Koba + * @since 3.2 + */ +record ForeignKey(String name, String tableName, String columnName, String referencedTableName, + String referencedColumnName) { + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ForeignKey that = (ForeignKey) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriter.java index b935127547..ac0afc567b 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriter.java @@ -21,8 +21,10 @@ import liquibase.change.ColumnConfig; import liquibase.change.ConstraintsConfig; import liquibase.change.core.AddColumnChange; +import liquibase.change.core.AddForeignKeyConstraintChange; import liquibase.change.core.CreateTableChange; import liquibase.change.core.DropColumnChange; +import liquibase.change.core.DropForeignKeyConstraintChange; import liquibase.change.core.DropTableChange; import liquibase.changelog.ChangeLogChild; import liquibase.changelog.ChangeLogParameters; @@ -52,6 +54,7 @@ import java.util.Set; import java.util.function.BiPredicate; import java.util.function.Predicate; +import java.util.stream.Collectors; import org.springframework.core.io.Resource; import org.springframework.data.mapping.context.MappingContext; @@ -321,7 +324,7 @@ private ChangeSet createChangeSet(ChangeSetMetadata metadata, SchemaDiff differe private SchemaDiff initial() { Tables mappedEntities = Tables.from(mappingContext.getPersistentEntities().stream().filter(schemaFilter), - sqlTypeMapping, null); + sqlTypeMapping, null, mappingContext); return SchemaDiff.diff(mappedEntities, Tables.empty(), nameComparator); } @@ -329,7 +332,7 @@ private SchemaDiff differenceOf(Database database) throws LiquibaseException { Tables existingTables = getLiquibaseModel(database); Tables mappedEntities = Tables.from(mappingContext.getPersistentEntities().stream().filter(schemaFilter), - sqlTypeMapping, database.getDefaultCatalogName()); + sqlTypeMapping, database.getDefaultCatalogName(), mappingContext); return SchemaDiff.diff(mappedEntities, existingTables, nameComparator); } @@ -362,6 +365,13 @@ private DatabaseChangeLog getDatabaseChangeLog(File changeLogFile, @Nullable Dat private void generateTableAdditionsDeletions(ChangeSet changeSet, SchemaDiff difference) { + for (Table table : difference.tableDeletions()) { + for (ForeignKey foreignKey : table.foreignKeys()) { + DropForeignKeyConstraintChange dropForeignKey = dropForeignKey(foreignKey); + changeSet.addChange(dropForeignKey); + } + } + for (Table table : difference.tableAdditions()) { CreateTableChange newTable = changeTable(table); changeSet.addChange(newTable); @@ -373,12 +383,24 @@ private void generateTableAdditionsDeletions(ChangeSet changeSet, SchemaDiff dif changeSet.addChange(dropTable(table)); } } + + for (Table table : difference.tableAdditions()) { + for (ForeignKey foreignKey : table.foreignKeys()) { + AddForeignKeyConstraintChange addForeignKey = addForeignKey(foreignKey); + changeSet.addChange(addForeignKey); + } + } } private void generateTableModifications(ChangeSet changeSet, SchemaDiff difference) { for (TableDiff table : difference.tableDiffs()) { + for (ForeignKey foreignKey : table.fkToDrop()) { + DropForeignKeyConstraintChange dropForeignKey = dropForeignKey(foreignKey); + changeSet.addChange(dropForeignKey); + } + if (!table.columnsToAdd().isEmpty()) { changeSet.addChange(addColumns(table)); } @@ -388,6 +410,11 @@ private void generateTableModifications(ChangeSet changeSet, SchemaDiff differen if (!deletedColumns.isEmpty()) { changeSet.addChange(dropColumns(table, deletedColumns)); } + + for (ForeignKey foreignKey : table.fkToAdd()) { + AddForeignKeyConstraintChange addForeignKey = addForeignKey(foreignKey); + changeSet.addChange(addForeignKey); + } } } @@ -444,12 +471,27 @@ private Tables getLiquibaseModel(Database targetDatabase) throws LiquibaseExcept tableModel.columns().add(columnModel); } + tableModel.foreignKeys().addAll(extractForeignKeys(table)); + existingTables.add(tableModel); } return new Tables(existingTables); } + private static List extractForeignKeys(liquibase.structure.core.Table table) { + + return table.getOutgoingForeignKeys().stream().map(foreignKey -> { + String tableName = foreignKey.getForeignKeyTable().getName(); + String columnName = foreignKey.getForeignKeyColumns().stream().findFirst() + .map(liquibase.structure.core.Column::getName).get(); + String referencedTableName = foreignKey.getPrimaryKeyTable().getName(); + String referencedColumnName = foreignKey.getPrimaryKeyColumns().stream().findFirst() + .map(liquibase.structure.core.Column::getName).get(); + return new ForeignKey(foreignKey.getName(), tableName, columnName, referencedTableName, referencedColumnName); + }).collect(Collectors.toList()); + } + private static AddColumnChange addColumns(TableDiff table) { AddColumnChange addColumnChange = new AddColumnChange(); @@ -532,6 +574,25 @@ private static DropTableChange dropTable(Table table) { return change; } + private static AddForeignKeyConstraintChange addForeignKey(ForeignKey foreignKey) { + + AddForeignKeyConstraintChange change = new AddForeignKeyConstraintChange(); + change.setConstraintName(foreignKey.name()); + change.setBaseTableName(foreignKey.tableName()); + change.setBaseColumnNames(foreignKey.columnName()); + change.setReferencedTableName(foreignKey.referencedTableName()); + change.setReferencedColumnNames(foreignKey.referencedColumnName()); + return change; + } + + private static DropForeignKeyConstraintChange dropForeignKey(ForeignKey foreignKey) { + + DropForeignKeyConstraintChange change = new DropForeignKeyConstraintChange(); + change.setConstraintName(foreignKey.name()); + change.setBaseTableName(foreignKey.tableName()); + return change; + } + /** * Metadata for a ChangeSet. */ diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/SchemaDiff.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/SchemaDiff.java index 079c40dde1..576badd92d 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/SchemaDiff.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/SchemaDiff.java @@ -16,6 +16,7 @@ package org.springframework.data.jdbc.core.mapping.schema; import java.util.ArrayList; +import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -91,43 +92,40 @@ private static List diffTable(Tables mappedEntities, Map mappedColumns = createMapping(mappedEntity.columns(), Column::name, nameComparator); - mappedEntity.keyColumns().forEach(it -> mappedColumns.put(it.name(), it)); - Map existingColumns = createMapping(existingTable.columns(), Column::name, nameComparator); - existingTable.keyColumns().forEach(it -> existingColumns.put(it.name(), it)); - // Identify deleted columns - Map toDelete = new TreeMap<>(nameComparator); - toDelete.putAll(existingColumns); - mappedColumns.keySet().forEach(toDelete::remove); - - tableDiff.columnsToDrop().addAll(toDelete.values()); - - // Identify added columns - Map addedColumns = new TreeMap<>(nameComparator); - addedColumns.putAll(mappedColumns); - - existingColumns.keySet().forEach(addedColumns::remove); - - // Add columns in order. This order can interleave with existing columns. - for (Column column : mappedEntity.keyColumns()) { - if (addedColumns.containsKey(column.name())) { - tableDiff.columnsToAdd().add(column); - } - } - + tableDiff.columnsToDrop().addAll(findDiffs(mappedColumns, existingColumns, nameComparator)); + // Identify added columns and add columns in order. This order can interleave with existing columns. + List addedColumns = new ArrayList<>(findDiffs(existingColumns, mappedColumns, nameComparator)); for (Column column : mappedEntity.columns()) { - if (addedColumns.containsKey(column.name())) { + if (addedColumns.contains(column)) { tableDiff.columnsToAdd().add(column); } } + Map mappedForeignKeys = createMapping(mappedEntity.foreignKeys(), ForeignKey::name, + nameComparator); + Map existingForeignKeys = createMapping(existingTable.foreignKeys(), ForeignKey::name, + nameComparator); + // Identify deleted columns + tableDiff.fkToDrop().addAll(findDiffs(mappedForeignKeys, existingForeignKeys, nameComparator)); + // Identify added columns + tableDiff.fkToAdd().addAll(findDiffs(existingForeignKeys, mappedForeignKeys, nameComparator)); + tableDiffs.add(tableDiff); } return tableDiffs; } + private static Collection findDiffs(Map baseMapping, Map toCompareMapping, + Comparator nameComparator) { + Map diff = new TreeMap<>(nameComparator); + diff.putAll(toCompareMapping); + baseMapping.keySet().forEach(diff::remove); + return diff.values(); + } + private static SortedMap createMapping(List items, Function keyFunction, Comparator nameComparator) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Table.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Table.java index 43b465d9a7..bd31afebf4 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Table.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Table.java @@ -27,7 +27,7 @@ * @author Kurt Niemi * @since 3.2 */ -record Table(@Nullable String schema, String name, List keyColumns, List columns) { +record Table(@Nullable String schema, String name, List columns, List foreignKeys) { public Table(@Nullable String schema, String name) { this(schema, name, new ArrayList<>(), new ArrayList<>()); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/TableDiff.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/TableDiff.java index 5ff5e01e71..189a9edd9d 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/TableDiff.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/TableDiff.java @@ -25,10 +25,11 @@ * @author Kurt Niemi * @since 3.2 */ -record TableDiff(Table table, List columnsToAdd, List columnsToDrop) { +record TableDiff(Table table, List columnsToAdd, List columnsToDrop, List fkToAdd, + List fkToDrop) { public TableDiff(Table table) { - this(table, new ArrayList<>(), new ArrayList<>()); + this(table, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Tables.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Tables.java index 12a35ce535..01a3da3fe0 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Tables.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Tables.java @@ -15,14 +15,19 @@ */ package org.springframework.data.jdbc.core.mapping.schema; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.data.annotation.Id; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -37,15 +42,16 @@ record Tables(List tables) { public static Tables from(RelationalMappingContext context) { - return from(context.getPersistentEntities().stream(), new DefaultSqlTypeMapping(), null); + return from(context.getPersistentEntities().stream(), new DefaultSqlTypeMapping(), null, context); } - // TODO: Add support (i.e. create tickets) to support mapped collections, entities, embedded properties, and aggregate - // references. + // TODO: Add support (i.e. create tickets) to support entities, embedded properties, and aggregate references. public static Tables from(Stream> persistentEntities, - SqlTypeMapping sqlTypeMapping, @Nullable String defaultSchema) { + SqlTypeMapping sqlTypeMapping, @Nullable String defaultSchema, + MappingContext, ? extends RelationalPersistentProperty> context) { + Map> colAndFKByTableName = new HashMap<>(); List
tables = persistentEntities .filter(it -> it.isAnnotationPresent(org.springframework.data.relational.core.mapping.Table.class)) // .map(entity -> { @@ -54,6 +60,7 @@ public static Tables from(Stream> persis Set identifierColumns = new LinkedHashSet<>(); entity.getPersistentProperties(Id.class).forEach(identifierColumns::add); + collectForeignKeysInfo(entity, context, colAndFKByTableName, sqlTypeMapping); for (RelationalPersistentProperty property : entity) { @@ -61,19 +68,77 @@ public static Tables from(Stream> persis continue; } - String columnType = sqlTypeMapping.getRequiredColumnType(property); - Column column = new Column(property.getColumnName().getReference(), sqlTypeMapping.getColumnType(property), - sqlTypeMapping.isNullable(property), identifierColumns.contains(property)); + sqlTypeMapping.isNullable(property), identifierColumns.contains(property)); table.columns().add(column); } return table; }).collect(Collectors.toList()); + applyForeignKeys(tables, colAndFKByTableName); + return new Tables(tables); } public static Tables empty() { return new Tables(Collections.emptyList()); } + + private static void applyForeignKeys(List
tables, + Map> colAndFKByTableName) { + + colAndFKByTableName.forEach( + (tableName, colsAndFK) -> tables.stream().filter(table -> table.name().equals(tableName)).forEach(table -> { + + colsAndFK.forEach(colAndFK -> { + if (!table.columns().contains(colAndFK.column())) { + table.columns().add(colAndFK.column()); + } + }); + + colsAndFK.forEach(colAndFK -> table.foreignKeys().add(colAndFK.foreignKey())); + })); + } + + private static void collectForeignKeysInfo(RelationalPersistentEntity entity, + MappingContext, ? extends RelationalPersistentProperty> context, + Map> keyColumnsByTableName, SqlTypeMapping sqlTypeMapping) { + + RelationalPersistentProperty identifierColumn = entity.getPersistentProperty(Id.class); + + entity.getPersistentProperties(MappedCollection.class).forEach(property -> { + if (property.isEntity()) { + property.getPersistentEntityTypeInformation().forEach(typeInformation -> { + + String tableName = context.getRequiredPersistentEntity(typeInformation).getTableName().getReference(); + String columnName = property.getReverseColumnName(entity).getReference(); + String referencedTableName = entity.getTableName().getReference(); + String referencedColumnName = identifierColumn.getColumnName().getReference(); + + ForeignKey foreignKey = new ForeignKey(getForeignKeyName(referencedTableName, referencedColumnName), + tableName, columnName, referencedTableName, referencedColumnName); + Column column = new Column(columnName, sqlTypeMapping.getColumnType(identifierColumn), true, false); + + ColumnWithForeignKey columnWithForeignKey = new ColumnWithForeignKey(column, foreignKey); + keyColumnsByTableName.compute( + context.getRequiredPersistentEntity(typeInformation).getTableName().getReference(), (key, value) -> { + if (value == null) { + return new ArrayList<>(List.of(columnWithForeignKey)); + } else { + value.add(columnWithForeignKey); + return value; + } + }); + }); + } + }); + } + + //TODO should we place it in BasicRelationalPersistentProperty/BasicRelationalPersistentEntity and generate using NamingStrategy? + private static String getForeignKeyName(String referencedTableName, String referencedColumnName) { + return String.format("%s_%s_fk", referencedTableName, referencedColumnName); + } + + private record ColumnWithForeignKey(Column column, ForeignKey foreignKey) { + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriterIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriterIntegrationTests.java index d27e59a37e..805cf85256 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriterIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriterIntegrationTests.java @@ -20,7 +20,9 @@ import liquibase.change.AddColumnConfig; import liquibase.change.ColumnConfig; import liquibase.change.core.AddColumnChange; +import liquibase.change.core.AddForeignKeyConstraintChange; import liquibase.change.core.DropColumnChange; +import liquibase.change.core.DropForeignKeyConstraintChange; import liquibase.change.core.DropTableChange; import liquibase.changelog.ChangeSet; import liquibase.changelog.DatabaseChangeLog; @@ -41,6 +43,7 @@ import org.springframework.core.io.FileSystemResource; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.mapping.schema.LiquibaseChangeSetWriter.ChangeSetMetadata; +import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.util.Predicates; @@ -202,6 +205,93 @@ void shouldAppendToChangeLog(@TempDir File tempDir) { }); } + @Test // GH-1599 + void dropAndCreateTableWithRightOrderOfFkChanges() { + + withEmbeddedDatabase("drop-and-create-table-with-fk.sql", c -> { + + H2Database h2Database = new H2Database(); + h2Database.setConnection(new JdbcConnection(c)); + + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(contextOf(GroupOfPersons.class)); + writer.setDropTableFilter(Predicates.isTrue()); + + ChangeSet changeSet = writer.createChangeSet(ChangeSetMetadata.create(), h2Database, new DatabaseChangeLog()); + + assertThat(changeSet.getChanges()).hasSize(4); + assertThat(changeSet.getChanges().get(0)).isInstanceOf(DropForeignKeyConstraintChange.class); + assertThat(changeSet.getChanges().get(3)).isInstanceOf(AddForeignKeyConstraintChange.class); + + DropForeignKeyConstraintChange dropForeignKey = (DropForeignKeyConstraintChange) changeSet.getChanges().get(0); + assertThat(dropForeignKey.getConstraintName()).isEqualToIgnoringCase("fk_to_drop"); + assertThat(dropForeignKey.getBaseTableName()).isEqualToIgnoringCase("table_to_drop"); + + AddForeignKeyConstraintChange addForeignKey = (AddForeignKeyConstraintChange) changeSet.getChanges().get(3); + assertThat(addForeignKey.getBaseTableName()).isEqualToIgnoringCase("person"); + assertThat(addForeignKey.getBaseColumnNames()).isEqualToIgnoringCase("group_id"); + assertThat(addForeignKey.getReferencedTableName()).isEqualToIgnoringCase("group_of_persons"); + assertThat(addForeignKey.getReferencedColumnNames()).isEqualToIgnoringCase("id"); + }); + } + + @Test // GH-1599 + void dropAndCreateFkInRightOrder() { + + withEmbeddedDatabase("drop-and-create-fk.sql", c -> { + + H2Database h2Database = new H2Database(); + h2Database.setConnection(new JdbcConnection(c)); + + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(contextOf(GroupOfPersons.class)); + writer.setDropColumnFilter((s, s2) -> true); + + ChangeSet changeSet = writer.createChangeSet(ChangeSetMetadata.create(), h2Database, new DatabaseChangeLog()); + + assertThat(changeSet.getChanges()).hasSize(3); + assertThat(changeSet.getChanges().get(0)).isInstanceOf(DropForeignKeyConstraintChange.class); + assertThat(changeSet.getChanges().get(2)).isInstanceOf(AddForeignKeyConstraintChange.class); + + DropForeignKeyConstraintChange dropForeignKey = (DropForeignKeyConstraintChange) changeSet.getChanges().get(0); + assertThat(dropForeignKey.getConstraintName()).isEqualToIgnoringCase("fk_to_drop"); + assertThat(dropForeignKey.getBaseTableName()).isEqualToIgnoringCase("person"); + + AddForeignKeyConstraintChange addForeignKey = (AddForeignKeyConstraintChange) changeSet.getChanges().get(2); + assertThat(addForeignKey.getBaseTableName()).isEqualToIgnoringCase("person"); + assertThat(addForeignKey.getBaseColumnNames()).isEqualToIgnoringCase("group_id"); + assertThat(addForeignKey.getReferencedTableName()).isEqualToIgnoringCase("group_of_persons"); + assertThat(addForeignKey.getReferencedColumnNames()).isEqualToIgnoringCase("id"); + }); + } + + @Test // GH-1599 + void fieldForFkWillBeCreated() { + + withEmbeddedDatabase("create-fk-with-field.sql", c -> { + + H2Database h2Database = new H2Database(); + h2Database.setConnection(new JdbcConnection(c)); + + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(contextOf(GroupOfPersons.class)); + + ChangeSet changeSet = writer.createChangeSet(ChangeSetMetadata.create(), h2Database, new DatabaseChangeLog()); + + assertThat(changeSet.getChanges()).hasSize(2); + assertThat(changeSet.getChanges().get(0)).isInstanceOf(AddColumnChange.class); + assertThat(changeSet.getChanges().get(1)).isInstanceOf(AddForeignKeyConstraintChange.class); + + AddColumnChange addColumn = (AddColumnChange) changeSet.getChanges().get(0); + assertThat(addColumn.getTableName()).isEqualToIgnoringCase("person"); + assertThat(addColumn.getColumns()).hasSize(1); + assertThat(addColumn.getColumns()).extracting(AddColumnConfig::getName).containsExactly("group_id"); + + AddForeignKeyConstraintChange addForeignKey = (AddForeignKeyConstraintChange) changeSet.getChanges().get(1); + assertThat(addForeignKey.getBaseTableName()).isEqualToIgnoringCase("person"); + assertThat(addForeignKey.getBaseColumnNames()).isEqualToIgnoringCase("group_id"); + assertThat(addForeignKey.getReferencedTableName()).isEqualToIgnoringCase("group_of_persons"); + assertThat(addForeignKey.getReferencedColumnNames()).isEqualToIgnoringCase("id"); + }); + } + RelationalMappingContext contextOf(Class... classes) { RelationalMappingContext context = new RelationalMappingContext(); @@ -246,4 +336,10 @@ static class DifferentPerson { String hello; } + @Table + static class GroupOfPersons { + @Id int id; + @MappedCollection(idColumn = "group_id") Set persons; + } + } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriterUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriterUnitTests.java index 314bbea8f4..73f377625e 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriterUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriterUnitTests.java @@ -15,9 +15,15 @@ */ package org.springframework.data.jdbc.core.mapping.schema; +import java.util.List; +import java.util.Optional; +import java.util.Set; + import static org.assertj.core.api.Assertions.*; +import liquibase.change.Change; import liquibase.change.ColumnConfig; +import liquibase.change.core.AddForeignKeyConstraintChange; import liquibase.change.core.CreateTableChange; import liquibase.changelog.ChangeSet; import liquibase.changelog.DatabaseChangeLog; @@ -25,6 +31,7 @@ import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.mapping.schema.LiquibaseChangeSetWriter.ChangeSetMetadata; +import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.core.mapping.RelationalMappingContext; /** @@ -73,6 +80,45 @@ void shouldApplySchemaFilter() { assertThat(createTable.getTableName()).isEqualTo("other_table"); } + @Test // GH-1599 + void createForeignKeyWithNewTable() { + + RelationalMappingContext context = new RelationalMappingContext(); + context.getRequiredPersistentEntity(Tables.class); + + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context); + + ChangeSet changeSet = writer.createChangeSet(ChangeSetMetadata.create(), new DatabaseChangeLog()); + + AddForeignKeyConstraintChange addForeignKey = (AddForeignKeyConstraintChange) changeSet.getChanges().get(2); + + assertThat(addForeignKey.getBaseTableName()).isEqualTo("other_table"); + assertThat(addForeignKey.getBaseColumnNames()).isEqualTo("tables"); + assertThat(addForeignKey.getReferencedTableName()).isEqualTo("tables"); + assertThat(addForeignKey.getReferencedColumnNames()).isEqualTo("id"); + + } + + @Test // GH-1599 + void fieldForFkShouldNotBeCreatedTwice() { + + RelationalMappingContext context = new RelationalMappingContext(); + context.getRequiredPersistentEntity(DifferentTables.class); + + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context); + + ChangeSet changeSet = writer.createChangeSet(ChangeSetMetadata.create(), new DatabaseChangeLog()); + + Optional tableWithFk = changeSet.getChanges().stream().filter(change -> { + return change instanceof CreateTableChange && ((CreateTableChange) change).getTableName() + .equals("table_with_fk_field"); + }).findFirst(); + assertThat(tableWithFk.isPresent()).isEqualTo(true); + + List columns = ((CreateTableChange) tableWithFk.get()).getColumns(); + assertThat(columns).extracting(ColumnConfig::getName).containsExactly("id", "tables_id"); + } + @org.springframework.data.relational.core.mapping.Table static class VariousTypes { @Id long id; @@ -88,4 +134,24 @@ static class OtherTable { @Id long id; } + @org.springframework.data.relational.core.mapping.Table + static class Tables { + @Id int id; + @MappedCollection + Set tables; + } + + @org.springframework.data.relational.core.mapping.Table + static class DifferentTables { + @Id int id; + @MappedCollection(idColumn = "tables_id") + Set tables; + } + + @org.springframework.data.relational.core.mapping.Table + static class TableWithFkField { + @Id int id; + int tablesId; + } + } diff --git a/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/create-fk-with-field.sql b/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/create-fk-with-field.sql new file mode 100644 index 0000000000..15b912bed9 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/create-fk-with-field.sql @@ -0,0 +1,11 @@ +CREATE TABLE group_of_persons +( + id int primary key +); + +CREATE TABLE person +( + id int, + first_name varchar(255), + last_name varchar(255) +); diff --git a/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/drop-and-create-fk.sql b/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/drop-and-create-fk.sql new file mode 100644 index 0000000000..030599f566 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/drop-and-create-fk.sql @@ -0,0 +1,14 @@ +CREATE TABLE group_of_persons +( + id int primary key +); + +CREATE TABLE person +( + id int, + first_name varchar(255), + last_name varchar(255), + group_id int, + group_id_to_drop int, + constraint fk_to_drop foreign key (group_id_to_drop) references group_of_persons(id) +); diff --git a/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/drop-and-create-table-with-fk.sql b/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/drop-and-create-table-with-fk.sql new file mode 100644 index 0000000000..9a81131180 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/drop-and-create-table-with-fk.sql @@ -0,0 +1,11 @@ +CREATE TABLE group_of_persons +( + id int primary key +); + +CREATE TABLE table_to_drop +( + id int primary key, + persons int, + constraint fk_to_drop foreign key (persons) references group_of_persons(id) +);