From f5fa5026899d4492ff54ad00998640bc098684c5 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Thu, 13 Apr 2023 07:59:28 -0400 Subject: [PATCH 01/32] Initial Data Model for Schema SQL Generation (#1481) Initial Data Model for Schema SQL Generation that can generate SQL for a simple entity. Closes #1478 --- .../mapping/RelationalMappingContext.java | 3 +- .../schemasqlgeneration/BaseTypeMapper.java | 34 ++++++ .../schemasqlgeneration/ColumnModel.java | 58 +++++++++ .../ForeignKeyColumnModel.java | 50 ++++++++ .../SchemaSQLGenerationDataModel.java | 77 ++++++++++++ .../SchemaSQLGenerator.java | 113 ++++++++++++++++++ .../schemasqlgeneration/TableModel.java | 66 ++++++++++ .../SchemaSQLGenerationDataModelTests.java | 87 ++++++++++++++ 8 files changed, 487 insertions(+), 1 deletion(-) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ForeignKeyColumnModel.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerator.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/schemasqlgeneration/SchemaSQLGenerationDataModelTests.java diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java index c6712a4c9a..4a20ba7080 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java @@ -22,6 +22,8 @@ import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; +import java.util.Iterator; + /** * {@link MappingContext} implementation. * @@ -101,5 +103,4 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert public NamingStrategy getNamingStrategy() { return this.namingStrategy; } - } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java new file mode 100644 index 0000000000..9e8f5335e0 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping.schemasqlgeneration; + +import java.util.HashMap; + +public class BaseTypeMapper { + + final HashMap,String> mapClassToDatabaseType = new HashMap,String>(); + + public BaseTypeMapper() { + mapClassToDatabaseType.put(String.class, "VARCHAR(255)"); + mapClassToDatabaseType.put(Boolean.class, "TINYINT"); + mapClassToDatabaseType.put(Double.class, "DOUBLE"); + mapClassToDatabaseType.put(Float.class, "FLOAT"); + mapClassToDatabaseType.put(Integer.class, "INT"); + } + public String databaseTypeFromClass(Class type) { + return mapClassToDatabaseType.get(type); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java new file mode 100644 index 0000000000..18cd0d12c1 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java @@ -0,0 +1,58 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping.schemasqlgeneration; + +import org.springframework.data.relational.core.sql.SqlIdentifier; + +import java.io.Serial; +import java.io.Serializable; + +/** + * Class that models a Column for generating SQL for Schema generation. + * + * @author Kurt Niemi + */ +public class ColumnModel implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private final SqlIdentifier name; + private final String type; + private final boolean nullable; + + public ColumnModel(SqlIdentifier name, String type, boolean nullable) { + this.name = name; + this.type = type; + this.nullable = nullable; + } + + public ColumnModel(SqlIdentifier name, String type) { + this.name = name; + this.type = type; + this.nullable = false; + } + + public SqlIdentifier getName() { + return name; + } + + public String getType() { + return type; + } + + public boolean isNullable() { + return nullable; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ForeignKeyColumnModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ForeignKeyColumnModel.java new file mode 100644 index 0000000000..04bc3d46f0 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ForeignKeyColumnModel.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping.schemasqlgeneration; + +import java.io.Serial; +import java.io.Serializable; + +/** + * Class that models a Foreign Key relationship for generating SQL for Schema generation. + * + * @author Kurt Niemi + */ +public class ForeignKeyColumnModel implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private final TableModel foreignTable; + private final ColumnModel foreignColumn; + private final ColumnModel column; + + public ForeignKeyColumnModel(TableModel foreignTable, ColumnModel foreignColumn, ColumnModel column) { + this.foreignTable = foreignTable; + this.foreignColumn = foreignColumn; + this.column = column; + } + + public TableModel getForeignTable() { + return foreignTable; + } + + public ColumnModel getForeignColumn() { + return foreignColumn; + } + + public ColumnModel getColumn() { + return column; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java new file mode 100644 index 0000000000..aa31a656f5 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java @@ -0,0 +1,77 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping.schemasqlgeneration; + +import org.springframework.data.relational.core.mapping.BasicRelationalPersistentProperty; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +/** + * Model class that contains Table/Column information that can be used + * to generate SQL for Schema generation. + * + * @author Kurt Niemi + */ +public class SchemaSQLGenerationDataModel implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private final List tableData = new ArrayList(); + BaseTypeMapper typeMapper; + + /** + * Default constructor so that we can deserialize a model + */ + public SchemaSQLGenerationDataModel() { + } + + /** + * Create model from a RelationalMappingContext + */ + public SchemaSQLGenerationDataModel(RelationalMappingContext context) { + + if (typeMapper == null) { + typeMapper = new BaseTypeMapper(); + } + + for (RelationalPersistentEntity entity : context.getPersistentEntities()) { + TableModel tableModel = new TableModel(entity.getTableName()); + + Iterator iter = + entity.getPersistentProperties(Column.class).iterator(); + + while (iter.hasNext()) { + BasicRelationalPersistentProperty p = iter.next(); + ColumnModel columnModel = new ColumnModel(p.getColumnName(), + typeMapper.databaseTypeFromClass(p.getActualType()), + true); + tableModel.getColumns().add(columnModel); + } + tableData.add(tableModel); + } + } + + public List getTableData() { + return tableData; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerator.java new file mode 100644 index 0000000000..8b9c8a77a0 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerator.java @@ -0,0 +1,113 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping.schemasqlgeneration; + +import org.springframework.data.relational.core.sql.IdentifierProcessing; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class SchemaSQLGenerator { + + private final IdentifierProcessing identifierProcssing; + public SchemaSQLGenerator(IdentifierProcessing identifierProcessing) { + this.identifierProcssing = identifierProcessing; + } + + List> reorderTablesInHierarchy(SchemaSQLGenerationDataModel dataModel) { + + // ::TODO:: Take Parent/Child relationships into account (i.e if a child table has + // a Foreign Key to a table, that parent table needs to be created first. + + // For now this method will simple put the tables in the same level + List> orderedTables = new ArrayList>(); + List tables = new ArrayList(); + + for (TableModel table : dataModel.getTableData()) { + tables.add(table); + } + orderedTables.add(tables); + + return orderedTables; + } + + HashMap,String> mapClassToSQLType = null; + + public String generateSQL(ColumnModel column) { + + StringBuilder sql = new StringBuilder(); + sql.append(column.getName().toSql(identifierProcssing)); + sql.append(" "); + + sql.append(column.getType()); + + if (!column.isNullable()) { + sql.append(" NOT NULL"); + } + + return sql.toString(); + } + + public String generatePrimaryKeySQL(TableModel table) { + // ::TODO:: Implement + return ""; + } + + public String generateForeignKeySQL(TableModel table) { + // ::TODO:: Implement + return ""; + } + + public String generateSQL(TableModel table) { + + StringBuilder sql = new StringBuilder(); + + sql.append("CREATE TABLE "); + sql.append(table.getName().toSql(identifierProcssing)); + sql.append(" ("); + + int numColumns = table.getColumns().size(); + for (int i=0; i < numColumns; i++) { + sql.append(generateSQL(table.getColumns().get(i))); + if (i != numColumns-1) { + sql.append(","); + } + } + + sql.append(generatePrimaryKeySQL(table)); + sql.append(generateForeignKeySQL(table)); + + sql.append(" );"); + + return sql.toString(); + } + + public String generateSQL(SchemaSQLGenerationDataModel dataModel) { + + StringBuilder sql = new StringBuilder(); + List> orderedTables = reorderTablesInHierarchy(dataModel); + + for (List tables : orderedTables) { + for (TableModel table : tables) { + String tableSQL = generateSQL(table); + sql.append(tableSQL + "\n"); + } + } + + return sql.toString(); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java new file mode 100644 index 0000000000..fb59264ffe --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping.schemasqlgeneration; + +import org.springframework.data.relational.core.sql.SqlIdentifier; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Class that models a Table for generating SQL for Schema generation. + * + * @author Kurt Niemi + */ +public class TableModel implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private final String schema; + private final SqlIdentifier name; + private final List columns = new ArrayList(); + private final List keyColumns = new ArrayList(); + private final List foreignKeyColumns = new ArrayList(); + + public TableModel(String schema, SqlIdentifier name) { + this.schema = schema; + this.name = name; + } + public TableModel(SqlIdentifier name) { + this(null, name); + } + + public String getSchema() { + return schema; + } + + public SqlIdentifier getName() { + return name; + } + + public List getColumns() { + return columns; + } + + public List getKeyColumns() { + return keyColumns; + } + + public List getForeignKeyColumns() { + return foreignKeyColumns; + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/schemasqlgeneration/SchemaSQLGenerationDataModelTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/schemasqlgeneration/SchemaSQLGenerationDataModelTests.java new file mode 100644 index 0000000000..517c369d63 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/schemasqlgeneration/SchemaSQLGenerationDataModelTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.schemasqlgeneration; + +import org.junit.jupiter.api.Test; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.Table; +import org.springframework.data.relational.core.mapping.schemasqlgeneration.SchemaSQLGenerationDataModel; +import org.springframework.data.relational.core.mapping.schemasqlgeneration.SchemaSQLGenerator; +import org.springframework.data.relational.core.mapping.schemasqlgeneration.TableModel; +import org.springframework.data.relational.core.sql.IdentifierProcessing; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for the {@link SchemaSQLGenerationDataModel}. + * + * @author Kurt Niemi + */ +public class SchemaSQLGenerationDataModelTests { + + @Test + void testBasicSchemaSQLGeneration() { + + IdentifierProcessing.Quoting quoting = new IdentifierProcessing.Quoting("`"); + IdentifierProcessing identifierProcessing = IdentifierProcessing.create(quoting, IdentifierProcessing.LetterCasing.LOWER_CASE); + SchemaSQLGenerator generator = new SchemaSQLGenerator(identifierProcessing); + + RelationalMappingContext context = new RelationalMappingContext(); + context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Luke.class); + + SchemaSQLGenerationDataModel model = new SchemaSQLGenerationDataModel(context); + String sql = generator.generateSQL(model); + assertThat(sql).isEqualTo("CREATE TABLE `luke` (`force` VARCHAR(255),`be` VARCHAR(255),`with` VARCHAR(255),`you` VARCHAR(255) );\n"); + + context = new RelationalMappingContext(); + context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Vader.class); + + model = new SchemaSQLGenerationDataModel(context); + sql = generator.generateSQL(model); + assertThat(sql).isEqualTo("CREATE TABLE `vader` (`luke_i_am_your_father` VARCHAR(255),`dark_side` TINYINT,`floater` FLOAT,`double_class` DOUBLE,`integer_class` INT );\n"); + } + + + @Table + static class Luke { + @Column + public String force; + @Column + public String be; + @Column + public String with; + @Column + public String you; + } + + @Table + static class Vader { + @Column + public String lukeIAmYourFather; + @Column + public Boolean darkSide; + @Column + public Float floater; + @Column + public Double doubleClass; + @Column + public Integer integerClass; + } + + +} From 201ba554fefd82a98228b24428111dbf3bdf8d1d Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Tue, 25 Apr 2023 19:56:43 -0400 Subject: [PATCH 02/32] Initial changes for SQLGenerationDataModel to perform differences between current and previous state --- .../core/mapping/DerivedSqlIdentifier.java | 3 +- .../schemasqlgeneration/BaseTypeMapper.java | 3 +- .../schemasqlgeneration/SchemaDiff.java | 20 +++ .../SchemaSQLGenerationDataModel.java | 95 +++++++++-- .../schemasqlgeneration/TableDiff.java | 28 ++++ .../schemasqlgeneration/TableModel.java | 14 ++ .../liquibase/ChangeSet.java | 20 +++ .../SchemaSQLGenerationDataModelTests.java | 153 ++++++++++++++++++ .../SchemaSQLGenerationDataModelTests.java | 87 ---------- 9 files changed, 322 insertions(+), 101 deletions(-) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/liquibase/ChangeSet.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaSQLGenerationDataModelTests.java delete mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/schemasqlgeneration/SchemaSQLGenerationDataModelTests.java diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java index d2fda3403c..826b8d5586 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java @@ -15,6 +15,7 @@ */ package org.springframework.data.relational.core.mapping; +import java.io.Serializable; import java.util.Collections; import java.util.Iterator; import java.util.function.UnaryOperator; @@ -32,7 +33,7 @@ * @author Kurt Niemi * @since 2.0 */ -class DerivedSqlIdentifier implements SqlIdentifier { +class DerivedSqlIdentifier implements SqlIdentifier, Serializable { private final String name; private final boolean quoted; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java index 9e8f5335e0..6fb25fec3a 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java @@ -15,9 +15,10 @@ */ package org.springframework.data.relational.core.mapping.schemasqlgeneration; +import java.io.Serializable; import java.util.HashMap; -public class BaseTypeMapper { +public class BaseTypeMapper implements Serializable { final HashMap,String> mapClassToDatabaseType = new HashMap,String>(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java new file mode 100644 index 0000000000..21219b1c8f --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java @@ -0,0 +1,20 @@ +package org.springframework.data.relational.core.mapping.schemasqlgeneration; + +import java.util.ArrayList; +import java.util.List; + +public class SchemaDiff { + private final List tableAdditions = new ArrayList(); + private final List tableDeletions = new ArrayList(); + private final List tableDiff = new ArrayList(); + public List getTableAdditions() { + return tableAdditions; + } + + public List getTableDeletions() { + return tableDeletions; + } + public List getTableDiff() { + return tableDiff; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java index aa31a656f5..af516c34f2 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java @@ -15,17 +15,12 @@ */ package org.springframework.data.relational.core.mapping.schemasqlgeneration; -import org.springframework.data.relational.core.mapping.BasicRelationalPersistentProperty; -import org.springframework.data.relational.core.mapping.Column; -import org.springframework.data.relational.core.mapping.RelationalMappingContext; -import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; - -import java.io.Serial; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; +import org.springframework.data.relational.core.mapping.*; +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.util.Assert; + +import java.io.*; +import java.util.*; /** * Model class that contains Table/Column information that can be used @@ -37,7 +32,7 @@ public class SchemaSQLGenerationDataModel implements Serializable { @Serial private static final long serialVersionUID = 1L; private final List tableData = new ArrayList(); - BaseTypeMapper typeMapper; + public BaseTypeMapper typeMapper; /** * Default constructor so that we can deserialize a model @@ -71,7 +66,83 @@ public SchemaSQLGenerationDataModel(RelationalMappingContext context) { } } + void diffTableAdditionDeletion(SchemaSQLGenerationDataModel source, SchemaDiff diff) { + + Set sourceTableData = new HashSet(source.getTableData()); + Set targetTableData = new HashSet(getTableData()); + + // Identify deleted tables + Set deletedTables = new HashSet(sourceTableData); + deletedTables.removeAll(targetTableData); + diff.getTableDeletions().addAll(deletedTables); + + // Identify added tables + Set addedTables = new HashSet(targetTableData); + addedTables.removeAll(sourceTableData); + diff.getTableAdditions().addAll(addedTables); + } + + void diffTable(SchemaSQLGenerationDataModel source, SchemaDiff diff) { + + HashMap sourceTablesMap = new HashMap(); + for (TableModel table : source.getTableData()) { + sourceTablesMap.put(table.getSchema() + "." + table.getName().getReference(), table); + } + + Set existingTables = new HashSet(getTableData()); + existingTables.removeAll(diff.getTableAdditions()); + + for (TableModel table : existingTables) { + TableDiff tableDiff = new TableDiff(table); + diff.getTableDiff().add(tableDiff); + + System.out.println("Table " + table.getName().getReference() + " modified"); + TableModel sourceTable = sourceTablesMap.get(table.getSchema() + "." + table.getName().getReference()); + + Set sourceTableData = new HashSet(sourceTable.getColumns()); + Set targetTableData = new HashSet(table.getColumns()); + + // Identify deleted columns + Set deletedColumns = new HashSet(sourceTableData); + deletedColumns.removeAll(targetTableData); + + tableDiff.getDeletedColumns().addAll(deletedColumns); + + // Identify added columns + Set addedColumns = new HashSet(targetTableData); + addedColumns.removeAll(sourceTableData); + tableDiff.getAddedColumns().addAll(addedColumns); + } + } + + public SchemaDiff diffModel(SchemaSQLGenerationDataModel source) { + + SchemaDiff diff = new SchemaDiff(); + + diffTableAdditionDeletion(source, diff); + diffTable(source, diff); + + return diff; + } + public List getTableData() { return tableData; } + + public void persist(String fileName) throws IOException { + FileOutputStream file = new FileOutputStream(fileName); + ObjectOutputStream out = new ObjectOutputStream(file); + out.writeObject(this); + + out.close(); + file.close(); + } + + public static SchemaSQLGenerationDataModel load(String fileName) throws IOException, ClassNotFoundException { + FileInputStream file = new FileInputStream(fileName); + ObjectInputStream in = new ObjectInputStream(file); + + SchemaSQLGenerationDataModel model = (SchemaSQLGenerationDataModel) in.readObject(); + return model; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java new file mode 100644 index 0000000000..3e6fe4fe43 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java @@ -0,0 +1,28 @@ +package org.springframework.data.relational.core.mapping.schemasqlgeneration; + +import org.springframework.data.relational.core.sql.SqlIdentifier; + +import java.util.ArrayList; +import java.util.List; + +public class TableDiff { + private final TableModel table; + private final List addedColumns = new ArrayList(); + private final List deletedColumns = new ArrayList(); + + public TableDiff(TableModel table) { + this.table = table; + } + + public TableModel getTable() { + return table; + } + + public List getAddedColumns() { + return addedColumns; + } + + public List getDeletedColumns() { + return deletedColumns; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java index fb59264ffe..cb8c21bbae 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java @@ -21,6 +21,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * Class that models a Table for generating SQL for Schema generation. @@ -63,4 +64,17 @@ public List getKeyColumns() { public List getForeignKeyColumns() { return foreignKeyColumns; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TableModel that = (TableModel) o; + return Objects.equals(schema, that.schema) && name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hash(schema, name); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/liquibase/ChangeSet.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/liquibase/ChangeSet.java new file mode 100644 index 0000000000..bb317384f1 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/liquibase/ChangeSet.java @@ -0,0 +1,20 @@ +package org.springframework.data.relational.core.mapping.schemasqlgeneration.liquibase; + +import java.util.List; + +public class ChangeSet { + private String id; + private String author; + + class ChangeSetChange { + private String type; // createTable, addColumn, dropTable, dropColumn + private String tableName; + private List columns; + } + + class ChangeSetColumns { + private String name; + private String type; + } + +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaSQLGenerationDataModelTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaSQLGenerationDataModelTests.java new file mode 100644 index 0000000000..ae4d37e908 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaSQLGenerationDataModelTests.java @@ -0,0 +1,153 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.sql; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.Table; +import org.springframework.data.relational.core.mapping.schemasqlgeneration.*; + +import static org.assertj.core.api.Assertions.as; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for the {@link SchemaSQLGenerationDataModel}. + * + * @author Kurt Niemi + */ +public class SchemaSQLGenerationDataModelTests { + + @Test + void testBasicSchemaSQLGeneration() { + + IdentifierProcessing.Quoting quoting = new IdentifierProcessing.Quoting("`"); + IdentifierProcessing identifierProcessing = IdentifierProcessing.create(quoting, IdentifierProcessing.LetterCasing.LOWER_CASE); + SchemaSQLGenerator generator = new SchemaSQLGenerator(identifierProcessing); + + RelationalMappingContext context = new RelationalMappingContext(); + context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Luke.class); + + SchemaSQLGenerationDataModel model = new SchemaSQLGenerationDataModel(context); + String sql = generator.generateSQL(model); + assertThat(sql).isEqualTo("CREATE TABLE `luke` (`force` VARCHAR(255),`be` VARCHAR(255),`with` VARCHAR(255),`you` VARCHAR(255) );\n"); + + context = new RelationalMappingContext(); + context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Vader.class); + + model = new SchemaSQLGenerationDataModel(context); + sql = generator.generateSQL(model); + assertThat(sql).isEqualTo("CREATE TABLE `vader` (`luke_i_am_your_father` VARCHAR(255),`dark_side` TINYINT,`floater` FLOAT,`double_class` DOUBLE,`integer_class` INT );\n"); + } + + @Test + void testDiffSchema() { + IdentifierProcessing.Quoting quoting = new IdentifierProcessing.Quoting("`"); + IdentifierProcessing identifierProcessing = IdentifierProcessing.create(quoting, IdentifierProcessing.LetterCasing.LOWER_CASE); + SchemaSQLGenerator generator = new SchemaSQLGenerator(identifierProcessing); + + RelationalMappingContext context = new RelationalMappingContext(); + context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Luke.class); + context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Vader.class); + + SchemaSQLGenerationDataModel model = new SchemaSQLGenerationDataModel(context); + + SchemaSQLGenerationDataModel newModel = new SchemaSQLGenerationDataModel(context); + + // Add column to table + SqlIdentifier newIdentifier = new DefaultSqlIdentifier("newcol", false); + ColumnModel newColumn = new ColumnModel(newIdentifier, "VARCHAR(255)"); + newModel.getTableData().get(0).getColumns().add(newColumn); + + // Remove table + newModel.getTableData().remove(1); + + // Add new table + SqlIdentifier tableIdenfifier = new DefaultSqlIdentifier("newtable", false); + TableModel newTable = new TableModel(null, tableIdenfifier); + newTable.getColumns().add(newColumn); + newModel.getTableData().add(newTable); + + SchemaDiff diff = newModel.diffModel(model); + + // Verify that newtable is an added table in the diff + assertThat(diff.getTableAdditions().size() > 0); + assertThat(diff.getTableAdditions().get(0).getName().getReference().equals("newtable")); + + assertThat(diff.getTableDeletions().size() > 0); + assertThat(diff.getTableDeletions().get(0).getName().getReference().equals("vader")); + + assertThat(diff.getTableDiff().size() > 0); + assertThat(diff.getTableDiff().get(0).getAddedColumns().size() > 0); + assertThat(diff.getTableDiff().get(0).getDeletedColumns().size() > 0); + assertThat(diff.getTableDiff().get(0).getAddedColumns().get(0).getName().getReference().equals("A")); + assertThat(diff.getTableDiff().get(0).getDeletedColumns().get(0).getName().getReference().equals("B")); + } + + @Test + void testSerialization() { + IdentifierProcessing.Quoting quoting = new IdentifierProcessing.Quoting("`"); + IdentifierProcessing identifierProcessing = IdentifierProcessing.create(quoting, IdentifierProcessing.LetterCasing.LOWER_CASE); + SchemaSQLGenerator generator = new SchemaSQLGenerator(identifierProcessing); + + RelationalMappingContext context = new RelationalMappingContext(); + context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Luke.class); + context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Vader.class); + + SchemaSQLGenerationDataModel model = new SchemaSQLGenerationDataModel(context); + + try { + model.persist("model.ser"); + + SchemaSQLGenerationDataModel loadedModel = SchemaSQLGenerationDataModel.load("model.ser"); + + assertThat(loadedModel.getTableData().size() == 2); + } catch (Exception ex) { + ex.printStackTrace(); + assertThat(false); + } + } + + + @Table + static class Luke { + @Column + public String force; + @Column + public String be; + @Column + public String with; + @Column + public String you; + } + + @Table + static class Vader { + @Column + public String lukeIAmYourFather; + @Column + public Boolean darkSide; + @Column + public Float floater; + @Column + public Double doubleClass; + @Column + public Integer integerClass; + } + + +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/schemasqlgeneration/SchemaSQLGenerationDataModelTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/schemasqlgeneration/SchemaSQLGenerationDataModelTests.java deleted file mode 100644 index 517c369d63..0000000000 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/schemasqlgeneration/SchemaSQLGenerationDataModelTests.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.schemasqlgeneration; - -import org.junit.jupiter.api.Test; -import org.springframework.data.relational.core.mapping.Column; -import org.springframework.data.relational.core.mapping.RelationalMappingContext; -import org.springframework.data.relational.core.mapping.Table; -import org.springframework.data.relational.core.mapping.schemasqlgeneration.SchemaSQLGenerationDataModel; -import org.springframework.data.relational.core.mapping.schemasqlgeneration.SchemaSQLGenerator; -import org.springframework.data.relational.core.mapping.schemasqlgeneration.TableModel; -import org.springframework.data.relational.core.sql.IdentifierProcessing; -import org.springframework.util.StringUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for the {@link SchemaSQLGenerationDataModel}. - * - * @author Kurt Niemi - */ -public class SchemaSQLGenerationDataModelTests { - - @Test - void testBasicSchemaSQLGeneration() { - - IdentifierProcessing.Quoting quoting = new IdentifierProcessing.Quoting("`"); - IdentifierProcessing identifierProcessing = IdentifierProcessing.create(quoting, IdentifierProcessing.LetterCasing.LOWER_CASE); - SchemaSQLGenerator generator = new SchemaSQLGenerator(identifierProcessing); - - RelationalMappingContext context = new RelationalMappingContext(); - context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Luke.class); - - SchemaSQLGenerationDataModel model = new SchemaSQLGenerationDataModel(context); - String sql = generator.generateSQL(model); - assertThat(sql).isEqualTo("CREATE TABLE `luke` (`force` VARCHAR(255),`be` VARCHAR(255),`with` VARCHAR(255),`you` VARCHAR(255) );\n"); - - context = new RelationalMappingContext(); - context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Vader.class); - - model = new SchemaSQLGenerationDataModel(context); - sql = generator.generateSQL(model); - assertThat(sql).isEqualTo("CREATE TABLE `vader` (`luke_i_am_your_father` VARCHAR(255),`dark_side` TINYINT,`floater` FLOAT,`double_class` DOUBLE,`integer_class` INT );\n"); - } - - - @Table - static class Luke { - @Column - public String force; - @Column - public String be; - @Column - public String with; - @Column - public String you; - } - - @Table - static class Vader { - @Column - public String lukeIAmYourFather; - @Column - public Boolean darkSide; - @Column - public Float floater; - @Column - public Double doubleClass; - @Column - public Integer integerClass; - } - - -} From e5e993f49937fcf718f8c20b23eefac821e61f3b Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Sun, 30 Apr 2023 15:10:48 -0400 Subject: [PATCH 03/32] Remove Serializable --- .../core/mapping/DerivedSqlIdentifier.java | 3 +-- .../schemasqlgeneration/BaseTypeMapper.java | 3 +-- .../schemasqlgeneration/ColumnModel.java | 6 +---- .../ForeignKeyColumnModel.java | 7 +----- .../SchemaSQLGenerationDataModel.java | 4 +-- .../schemasqlgeneration/TableModel.java | 6 +---- .../SchemaSQLGenerationDataModelTests.java | 25 ------------------- 7 files changed, 6 insertions(+), 48 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java index 826b8d5586..d2fda3403c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java @@ -15,7 +15,6 @@ */ package org.springframework.data.relational.core.mapping; -import java.io.Serializable; import java.util.Collections; import java.util.Iterator; import java.util.function.UnaryOperator; @@ -33,7 +32,7 @@ * @author Kurt Niemi * @since 2.0 */ -class DerivedSqlIdentifier implements SqlIdentifier, Serializable { +class DerivedSqlIdentifier implements SqlIdentifier { private final String name; private final boolean quoted; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java index 6fb25fec3a..9e8f5335e0 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java @@ -15,10 +15,9 @@ */ package org.springframework.data.relational.core.mapping.schemasqlgeneration; -import java.io.Serializable; import java.util.HashMap; -public class BaseTypeMapper implements Serializable { +public class BaseTypeMapper { final HashMap,String> mapClassToDatabaseType = new HashMap,String>(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java index 18cd0d12c1..be26c3c94f 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java @@ -17,17 +17,13 @@ import org.springframework.data.relational.core.sql.SqlIdentifier; -import java.io.Serial; -import java.io.Serializable; /** * Class that models a Column for generating SQL for Schema generation. * * @author Kurt Niemi */ -public class ColumnModel implements Serializable { - @Serial - private static final long serialVersionUID = 1L; +public class ColumnModel { private final SqlIdentifier name; private final String type; private final boolean nullable; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ForeignKeyColumnModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ForeignKeyColumnModel.java index 04bc3d46f0..6f32bbde9b 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ForeignKeyColumnModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ForeignKeyColumnModel.java @@ -15,17 +15,12 @@ */ package org.springframework.data.relational.core.mapping.schemasqlgeneration; -import java.io.Serial; -import java.io.Serializable; - /** * Class that models a Foreign Key relationship for generating SQL for Schema generation. * * @author Kurt Niemi */ -public class ForeignKeyColumnModel implements Serializable { - @Serial - private static final long serialVersionUID = 1L; +public class ForeignKeyColumnModel { private final TableModel foreignTable; private final ColumnModel foreignColumn; private final ColumnModel column; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java index af516c34f2..0b34caf060 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java @@ -28,9 +28,7 @@ * * @author Kurt Niemi */ -public class SchemaSQLGenerationDataModel implements Serializable { - @Serial - private static final long serialVersionUID = 1L; +public class SchemaSQLGenerationDataModel { private final List tableData = new ArrayList(); public BaseTypeMapper typeMapper; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java index cb8c21bbae..20f16a5171 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java @@ -17,8 +17,6 @@ import org.springframework.data.relational.core.sql.SqlIdentifier; -import java.io.Serial; -import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -28,9 +26,7 @@ * * @author Kurt Niemi */ -public class TableModel implements Serializable { - @Serial - private static final long serialVersionUID = 1L; +public class TableModel { private final String schema; private final SqlIdentifier name; private final List columns = new ArrayList(); diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaSQLGenerationDataModelTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaSQLGenerationDataModelTests.java index ae4d37e908..22eb7873d8 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaSQLGenerationDataModelTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaSQLGenerationDataModelTests.java @@ -98,31 +98,6 @@ void testDiffSchema() { assertThat(diff.getTableDiff().get(0).getDeletedColumns().get(0).getName().getReference().equals("B")); } - @Test - void testSerialization() { - IdentifierProcessing.Quoting quoting = new IdentifierProcessing.Quoting("`"); - IdentifierProcessing identifierProcessing = IdentifierProcessing.create(quoting, IdentifierProcessing.LetterCasing.LOWER_CASE); - SchemaSQLGenerator generator = new SchemaSQLGenerator(identifierProcessing); - - RelationalMappingContext context = new RelationalMappingContext(); - context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Luke.class); - context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Vader.class); - - SchemaSQLGenerationDataModel model = new SchemaSQLGenerationDataModel(context); - - try { - model.persist("model.ser"); - - SchemaSQLGenerationDataModel loadedModel = SchemaSQLGenerationDataModel.load("model.ser"); - - assertThat(loadedModel.getTableData().size() == 2); - } catch (Exception ex) { - ex.printStackTrace(); - assertThat(false); - } - } - - @Table static class Luke { @Column From ebde99fb47c7b02ed86ce515454dce4b238bd23f Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Wed, 10 May 2023 21:24:52 -0400 Subject: [PATCH 04/32] Add liquibase as an optional dependency --- pom.xml | 13 +++++++++++++ spring-data-jdbc/pom.xml | 19 +++++++++++++++++++ spring-data-relational/pom.xml | 9 +-------- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 4a5a0c9822..e3a171365a 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,7 @@ spring-data-jdbc 3.2.0-SNAPSHOT + 4.21.1 reuseReports @@ -91,6 +92,18 @@ -6 + + kurtn718 + Kurt Niemi + kniemi(at)vmware.com + VMware. + https://vmware.com + + Project Contributor + + -5 + + diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index ede9f0390f..cd2fb2fc92 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -70,6 +70,17 @@ +1 + + kurtn718 + Kurt Niemi + kniemi(at)vmware.com + VMware + https://vmware.com + + Project Contributor + + -5 + @@ -269,6 +280,14 @@ test + + org.liquibase + liquibase-core + ${liquibase.version} + compile + true + + diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 57b9d707a6..3058eb0afa 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -90,13 +90,6 @@ test - - org.testcontainers - testcontainers - ${testcontainers} - test - - - + From 4d085887de82182ce4fddf3a7c0a894f2548dc28 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Wed, 10 May 2023 21:35:30 -0400 Subject: [PATCH 05/32] Initial changes for Generating Liquibase changeset --- .../SchemaSQLGenerationDataModel.java | 106 ++++++++++++++++++ .../core/sql/DefaultSqlIdentifier.java | 4 +- 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java index 0b34caf060..5df3192686 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java @@ -15,12 +15,40 @@ */ package org.springframework.data.relational.core.mapping.schemasqlgeneration; +import liquibase.CatalogAndSchema; +import liquibase.change.ColumnConfig; +import liquibase.change.core.CreateTableChange; +import liquibase.change.core.DropTableChange; +import liquibase.changelog.ChangeLogChild; +import liquibase.changelog.ChangeSet; +import liquibase.changelog.DatabaseChangeLog; +import liquibase.database.Database; +import liquibase.database.DatabaseConnection; +import liquibase.database.MockDatabaseConnection; +import liquibase.database.core.MySQLDatabase; +import liquibase.database.jvm.JdbcConnection; +import liquibase.exception.DatabaseException; +import liquibase.parser.ChangeLogParser; +import liquibase.parser.core.yaml.YamlChangeLogParser; +import liquibase.serializer.ChangeLogSerializer; +import liquibase.serializer.ChangeLogSerializerFactory; +import liquibase.serializer.SnapshotSerializer; +import liquibase.serializer.SnapshotSerializerFactory; +import liquibase.serializer.core.formattedsql.FormattedSqlChangeLogSerializer; +import liquibase.serializer.core.yaml.YamlChangeLogSerializer; +import liquibase.snapshot.*; +import liquibase.structure.DatabaseObject; +import org.jetbrains.annotations.NotNull; import org.springframework.data.relational.core.mapping.*; +import org.springframework.data.relational.core.sql.DefaultSqlIdentifier; +import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.util.Assert; import java.io.*; +import java.sql.Connection; import java.util.*; +import java.util.function.UnaryOperator; /** * Model class that contains Table/Column information that can be used @@ -143,4 +171,82 @@ public static SchemaSQLGenerationDataModel load(String fileName) throws IOExcept SchemaSQLGenerationDataModel model = (SchemaSQLGenerationDataModel) in.readObject(); return model; } + + public void generateLiquibaseChangeset(Database database, String changeLogFilePath) throws InvalidExampleException, DatabaseException, IOException { + String changeSetId = Long.toString(System.currentTimeMillis()); + generateLiquibaseChangeset(database,changeLogFilePath, changeSetId, "Spring Data JDBC"); + } + + public void generateLiquibaseChangeset(Database database, String changeLogFilePath, String changeSetId, String changeSetAuthor) throws InvalidExampleException, DatabaseException, IOException { + CatalogAndSchema[] schemas = new CatalogAndSchema[] { database.getDefaultSchema() }; + SnapshotControl snapshotControl = new SnapshotControl(database); + + DatabaseSnapshot snapshot = SnapshotGeneratorFactory.getInstance().createSnapshot(schemas, database, snapshotControl); + Set tables = snapshot.get(liquibase.structure.core.Table.class); + + SchemaSQLGenerationDataModel liquibaseModel = new SchemaSQLGenerationDataModel(); + + for (liquibase.structure.core.Table table : tables) { + + SqlIdentifier tableName = new DefaultSqlIdentifier(table.getName(), false); + TableModel tableModel = new TableModel(table.getSchema().getCatalogName(), tableName); + liquibaseModel.getTableData().add(tableModel); + //System.out.println(table.getName()); + List columns = table.getColumns(); + for (liquibase.structure.core.Column column : columns) { + //System.out.println("--- " + column.getName() + "," + column.getType()); + } + } + + SchemaDiff difference = diffModel(liquibaseModel); + + File changeLogFile = new File(changeLogFilePath); + + ChangeLogSerializerFactory factory = ChangeLogSerializerFactory.getInstance(); + ChangeLogSerializer serializer = new YamlChangeLogSerializer(); + DatabaseChangeLog databaseChangeLog = new DatabaseChangeLog(changeLogFilePath); + ChangeSet changeSet = new ChangeSet(changeSetId, changeSetAuthor, false, false, "", "", "" , databaseChangeLog); + + for (TableModel t : difference.getTableAdditions()) { + System.out.println(t.getName().getReference() + " to be added."); + CreateTableChange newTable = createAddTableChange(t); + changeSet.addChange(newTable); + } + + for (TableModel t : difference.getTableDeletions()) { + System.out.println(t.getName().getReference() + " to be removed."); + DropTableChange dropTable = createDropTableChange(t); + changeSet.addChange(dropTable); + } + + List changes = new ArrayList(); + changes.add(changeSet); + FileOutputStream fos = new FileOutputStream(changeLogFile); + serializer.write(changes, fos); + + } + + CreateTableChange createAddTableChange(TableModel table) { + CreateTableChange change = new CreateTableChange(); + change.setSchemaName(table.getSchema()); + change.setTableName(table.getName().getReference()); + + for (ColumnModel column : table.getColumns()) { + ColumnConfig columnConfig = new ColumnConfig(); + columnConfig.setName(column.getName().getReference()); + columnConfig.setType(column.getType()); + change.addColumn(columnConfig); + } + + return change; + } + + DropTableChange createDropTableChange(TableModel table) { + DropTableChange change = new DropTableChange(); + change.setSchemaName(table.getSchema()); + change.setTableName(table.getName().getReference()); + change.setCascadeConstraints(true); + + return change; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSqlIdentifier.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSqlIdentifier.java index 391bcd22da..a31f698a0d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSqlIdentifier.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSqlIdentifier.java @@ -30,12 +30,12 @@ * @author Kurt Niemi * @since 2.0 */ -class DefaultSqlIdentifier implements SqlIdentifier { +public class DefaultSqlIdentifier implements SqlIdentifier { private final String name; private final boolean quoted; - DefaultSqlIdentifier(String name, boolean quoted) { + public DefaultSqlIdentifier(String name, boolean quoted) { Assert.hasText(name, "A database object name must not be null or empty"); From f7a5ead8d291451c5a52094103a65171727b8201 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Thu, 18 May 2023 20:59:03 -0400 Subject: [PATCH 06/32] Remove classes not used for generating Liquibase change set --- .../ForeignKeyColumnModel.java | 45 ------- .../SchemaSQLGenerator.java | 113 ------------------ .../liquibase/ChangeSet.java | 20 ---- 3 files changed, 178 deletions(-) delete mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ForeignKeyColumnModel.java delete mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerator.java delete mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/liquibase/ChangeSet.java diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ForeignKeyColumnModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ForeignKeyColumnModel.java deleted file mode 100644 index 6f32bbde9b..0000000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ForeignKeyColumnModel.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.core.mapping.schemasqlgeneration; - -/** - * Class that models a Foreign Key relationship for generating SQL for Schema generation. - * - * @author Kurt Niemi - */ -public class ForeignKeyColumnModel { - private final TableModel foreignTable; - private final ColumnModel foreignColumn; - private final ColumnModel column; - - public ForeignKeyColumnModel(TableModel foreignTable, ColumnModel foreignColumn, ColumnModel column) { - this.foreignTable = foreignTable; - this.foreignColumn = foreignColumn; - this.column = column; - } - - public TableModel getForeignTable() { - return foreignTable; - } - - public ColumnModel getForeignColumn() { - return foreignColumn; - } - - public ColumnModel getColumn() { - return column; - } -} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerator.java deleted file mode 100644 index 8b9c8a77a0..0000000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerator.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.core.mapping.schemasqlgeneration; - -import org.springframework.data.relational.core.sql.IdentifierProcessing; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -public class SchemaSQLGenerator { - - private final IdentifierProcessing identifierProcssing; - public SchemaSQLGenerator(IdentifierProcessing identifierProcessing) { - this.identifierProcssing = identifierProcessing; - } - - List> reorderTablesInHierarchy(SchemaSQLGenerationDataModel dataModel) { - - // ::TODO:: Take Parent/Child relationships into account (i.e if a child table has - // a Foreign Key to a table, that parent table needs to be created first. - - // For now this method will simple put the tables in the same level - List> orderedTables = new ArrayList>(); - List tables = new ArrayList(); - - for (TableModel table : dataModel.getTableData()) { - tables.add(table); - } - orderedTables.add(tables); - - return orderedTables; - } - - HashMap,String> mapClassToSQLType = null; - - public String generateSQL(ColumnModel column) { - - StringBuilder sql = new StringBuilder(); - sql.append(column.getName().toSql(identifierProcssing)); - sql.append(" "); - - sql.append(column.getType()); - - if (!column.isNullable()) { - sql.append(" NOT NULL"); - } - - return sql.toString(); - } - - public String generatePrimaryKeySQL(TableModel table) { - // ::TODO:: Implement - return ""; - } - - public String generateForeignKeySQL(TableModel table) { - // ::TODO:: Implement - return ""; - } - - public String generateSQL(TableModel table) { - - StringBuilder sql = new StringBuilder(); - - sql.append("CREATE TABLE "); - sql.append(table.getName().toSql(identifierProcssing)); - sql.append(" ("); - - int numColumns = table.getColumns().size(); - for (int i=0; i < numColumns; i++) { - sql.append(generateSQL(table.getColumns().get(i))); - if (i != numColumns-1) { - sql.append(","); - } - } - - sql.append(generatePrimaryKeySQL(table)); - sql.append(generateForeignKeySQL(table)); - - sql.append(" );"); - - return sql.toString(); - } - - public String generateSQL(SchemaSQLGenerationDataModel dataModel) { - - StringBuilder sql = new StringBuilder(); - List> orderedTables = reorderTablesInHierarchy(dataModel); - - for (List tables : orderedTables) { - for (TableModel table : tables) { - String tableSQL = generateSQL(table); - sql.append(tableSQL + "\n"); - } - } - - return sql.toString(); - } -} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/liquibase/ChangeSet.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/liquibase/ChangeSet.java deleted file mode 100644 index bb317384f1..0000000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/liquibase/ChangeSet.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.springframework.data.relational.core.mapping.schemasqlgeneration.liquibase; - -import java.util.List; - -public class ChangeSet { - private String id; - private String author; - - class ChangeSetChange { - private String type; // createTable, addColumn, dropTable, dropColumn - private String tableName; - private List columns; - } - - class ChangeSetColumns { - private String name; - private String type; - } - -} From 989d4b29cfca2fb05834fe7834e787aa78700c1a Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Thu, 18 May 2023 21:04:12 -0400 Subject: [PATCH 07/32] Make DerivedSqlIdentifier public - as we need to create one --- .../data/relational/core/mapping/DerivedSqlIdentifier.java | 4 ++-- .../data/relational/core/sql/DefaultSqlIdentifier.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java index d2fda3403c..2640357207 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java @@ -32,12 +32,12 @@ * @author Kurt Niemi * @since 2.0 */ -class DerivedSqlIdentifier implements SqlIdentifier { +public class DerivedSqlIdentifier implements SqlIdentifier { private final String name; private final boolean quoted; - DerivedSqlIdentifier(String name, boolean quoted) { + public DerivedSqlIdentifier(String name, boolean quoted) { Assert.hasText(name, "A database object must have at least on name part."); this.name = name; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSqlIdentifier.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSqlIdentifier.java index a31f698a0d..391bcd22da 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSqlIdentifier.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSqlIdentifier.java @@ -30,12 +30,12 @@ * @author Kurt Niemi * @since 2.0 */ -public class DefaultSqlIdentifier implements SqlIdentifier { +class DefaultSqlIdentifier implements SqlIdentifier { private final String name; private final boolean quoted; - public DefaultSqlIdentifier(String name, boolean quoted) { + DefaultSqlIdentifier(String name, boolean quoted) { Assert.hasText(name, "A database object name must not be null or empty"); From 7e21cfc3be831875ffce3cb4e4c789c578c8d0bb Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Thu, 18 May 2023 21:17:24 -0400 Subject: [PATCH 08/32] Fix Unit Test --- .../SchemaSQLGenerationDataModelTests.java | 37 +++---------------- 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaSQLGenerationDataModelTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaSQLGenerationDataModelTests.java index 22eb7873d8..5492df54ff 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaSQLGenerationDataModelTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaSQLGenerationDataModelTests.java @@ -22,7 +22,6 @@ import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.mapping.schemasqlgeneration.*; -import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; /** @@ -32,37 +31,12 @@ */ public class SchemaSQLGenerationDataModelTests { - @Test - void testBasicSchemaSQLGeneration() { - - IdentifierProcessing.Quoting quoting = new IdentifierProcessing.Quoting("`"); - IdentifierProcessing identifierProcessing = IdentifierProcessing.create(quoting, IdentifierProcessing.LetterCasing.LOWER_CASE); - SchemaSQLGenerator generator = new SchemaSQLGenerator(identifierProcessing); - - RelationalMappingContext context = new RelationalMappingContext(); - context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Luke.class); - - SchemaSQLGenerationDataModel model = new SchemaSQLGenerationDataModel(context); - String sql = generator.generateSQL(model); - assertThat(sql).isEqualTo("CREATE TABLE `luke` (`force` VARCHAR(255),`be` VARCHAR(255),`with` VARCHAR(255),`you` VARCHAR(255) );\n"); - - context = new RelationalMappingContext(); - context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Vader.class); - - model = new SchemaSQLGenerationDataModel(context); - sql = generator.generateSQL(model); - assertThat(sql).isEqualTo("CREATE TABLE `vader` (`luke_i_am_your_father` VARCHAR(255),`dark_side` TINYINT,`floater` FLOAT,`double_class` DOUBLE,`integer_class` INT );\n"); - } - @Test void testDiffSchema() { - IdentifierProcessing.Quoting quoting = new IdentifierProcessing.Quoting("`"); - IdentifierProcessing identifierProcessing = IdentifierProcessing.create(quoting, IdentifierProcessing.LetterCasing.LOWER_CASE); - SchemaSQLGenerator generator = new SchemaSQLGenerator(identifierProcessing); RelationalMappingContext context = new RelationalMappingContext(); - context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Luke.class); - context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Vader.class); + context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Table1.class); + context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Table2.class); SchemaSQLGenerationDataModel model = new SchemaSQLGenerationDataModel(context); @@ -94,12 +68,11 @@ void testDiffSchema() { assertThat(diff.getTableDiff().size() > 0); assertThat(diff.getTableDiff().get(0).getAddedColumns().size() > 0); assertThat(diff.getTableDiff().get(0).getDeletedColumns().size() > 0); - assertThat(diff.getTableDiff().get(0).getAddedColumns().get(0).getName().getReference().equals("A")); - assertThat(diff.getTableDiff().get(0).getDeletedColumns().get(0).getName().getReference().equals("B")); } + // Test table classes for performing schema diff @Table - static class Luke { + static class Table1 { @Column public String force; @Column @@ -111,7 +84,7 @@ static class Luke { } @Table - static class Vader { + static class Table2 { @Column public String lukeIAmYourFather; @Column From 046d91671734ffe4735569661a6be070cb8249f1 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Thu, 18 May 2023 21:26:04 -0400 Subject: [PATCH 09/32] Generate Liquibase changesets for table additions, modifications, and deletions. --- .../schemasqlgeneration/BaseTypeMapper.java | 5 +- .../schemasqlgeneration/ColumnModel.java | 24 ++- .../schemasqlgeneration/SchemaDiff.java | 3 + .../SchemaSQLGenerationDataModel.java | 153 ++++++++++++------ .../schemasqlgeneration/TableDiff.java | 16 +- .../schemasqlgeneration/TableModel.java | 52 ++++-- 6 files changed, 185 insertions(+), 68 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java index 9e8f5335e0..a48b6b8d07 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java @@ -22,13 +22,16 @@ public class BaseTypeMapper { final HashMap,String> mapClassToDatabaseType = new HashMap,String>(); public BaseTypeMapper() { - mapClassToDatabaseType.put(String.class, "VARCHAR(255)"); + + mapClassToDatabaseType.put(String.class, "VARCHAR(255 BYTE)"); mapClassToDatabaseType.put(Boolean.class, "TINYINT"); mapClassToDatabaseType.put(Double.class, "DOUBLE"); mapClassToDatabaseType.put(Float.class, "FLOAT"); mapClassToDatabaseType.put(Integer.class, "INT"); + mapClassToDatabaseType.put(Long.class, "BIGINT"); } public String databaseTypeFromClass(Class type) { + return mapClassToDatabaseType.get(type); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java index be26c3c94f..596de7f4cf 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java @@ -17,6 +17,8 @@ import org.springframework.data.relational.core.sql.SqlIdentifier; +import java.util.Objects; + /** * Class that models a Column for generating SQL for Schema generation. @@ -27,17 +29,20 @@ public class ColumnModel { private final SqlIdentifier name; private final String type; private final boolean nullable; + private final boolean identityColumn; - public ColumnModel(SqlIdentifier name, String type, boolean nullable) { + public ColumnModel(SqlIdentifier name, String type, boolean nullable, boolean identityColumn) { this.name = name; this.type = type; this.nullable = nullable; + this.identityColumn = identityColumn; } public ColumnModel(SqlIdentifier name, String type) { this.name = name; this.type = type; this.nullable = false; + this.identityColumn = false; } public SqlIdentifier getName() { @@ -51,4 +56,21 @@ public String getType() { public boolean isNullable() { return nullable; } + + public boolean isIdentityColumn() { + return identityColumn; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ColumnModel that = (ColumnModel) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java index 21219b1c8f..100fb1174d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java @@ -8,13 +8,16 @@ public class SchemaDiff { private final List tableDeletions = new ArrayList(); private final List tableDiff = new ArrayList(); public List getTableAdditions() { + return tableAdditions; } public List getTableDeletions() { + return tableDeletions; } public List getTableDiff() { + return tableDiff; } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java index 5df3192686..b626360462 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java @@ -16,39 +16,32 @@ package org.springframework.data.relational.core.mapping.schemasqlgeneration; import liquibase.CatalogAndSchema; +import liquibase.change.AddColumnConfig; import liquibase.change.ColumnConfig; +import liquibase.change.ConstraintsConfig; +import liquibase.change.core.AddColumnChange; import liquibase.change.core.CreateTableChange; +import liquibase.change.core.DropColumnChange; import liquibase.change.core.DropTableChange; import liquibase.changelog.ChangeLogChild; +import liquibase.changelog.ChangeLogParameters; import liquibase.changelog.ChangeSet; import liquibase.changelog.DatabaseChangeLog; import liquibase.database.Database; -import liquibase.database.DatabaseConnection; -import liquibase.database.MockDatabaseConnection; -import liquibase.database.core.MySQLDatabase; -import liquibase.database.jvm.JdbcConnection; +import liquibase.exception.ChangeLogParseException; import liquibase.exception.DatabaseException; -import liquibase.parser.ChangeLogParser; import liquibase.parser.core.yaml.YamlChangeLogParser; +import liquibase.resource.DirectoryResourceAccessor; import liquibase.serializer.ChangeLogSerializer; -import liquibase.serializer.ChangeLogSerializerFactory; -import liquibase.serializer.SnapshotSerializer; -import liquibase.serializer.SnapshotSerializerFactory; -import liquibase.serializer.core.formattedsql.FormattedSqlChangeLogSerializer; import liquibase.serializer.core.yaml.YamlChangeLogSerializer; import liquibase.snapshot.*; -import liquibase.structure.DatabaseObject; -import org.jetbrains.annotations.NotNull; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.*; -import org.springframework.data.relational.core.sql.DefaultSqlIdentifier; -import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.SqlIdentifier; -import org.springframework.util.Assert; import java.io.*; -import java.sql.Connection; import java.util.*; -import java.util.function.UnaryOperator; /** * Model class that contains Table/Column information that can be used @@ -56,14 +49,16 @@ * * @author Kurt Niemi */ -public class SchemaSQLGenerationDataModel { +public class SchemaSQLGenerationDataModel +{ private final List tableData = new ArrayList(); public BaseTypeMapper typeMapper; /** - * Default constructor so that we can deserialize a model + * Create empty model */ public SchemaSQLGenerationDataModel() { + } /** @@ -79,15 +74,24 @@ public SchemaSQLGenerationDataModel(RelationalMappingContext context) { TableModel tableModel = new TableModel(entity.getTableName()); Iterator iter = + entity.getPersistentProperties(Id.class).iterator(); + Set setIdentifierColumns = new HashSet(); + while (iter.hasNext()) { + BasicRelationalPersistentProperty p = iter.next(); + setIdentifierColumns.add(p); + } + + iter = entity.getPersistentProperties(Column.class).iterator(); while (iter.hasNext()) { BasicRelationalPersistentProperty p = iter.next(); ColumnModel columnModel = new ColumnModel(p.getColumnName(), typeMapper.databaseTypeFromClass(p.getActualType()), - true); + true, setIdentifierColumns.contains(p)); tableModel.getColumns().add(columnModel); } + tableData.add(tableModel); } } @@ -122,7 +126,6 @@ void diffTable(SchemaSQLGenerationDataModel source, SchemaDiff diff) { TableDiff tableDiff = new TableDiff(table); diff.getTableDiff().add(tableDiff); - System.out.println("Table " + table.getName().getReference() + " modified"); TableModel sourceTable = sourceTablesMap.get(table.getSchema() + "." + table.getName().getReference()); Set sourceTableData = new HashSet(sourceTable.getColumns()); @@ -155,29 +158,14 @@ public List getTableData() { return tableData; } - public void persist(String fileName) throws IOException { - FileOutputStream file = new FileOutputStream(fileName); - ObjectOutputStream out = new ObjectOutputStream(file); - out.writeObject(this); - - out.close(); - file.close(); - } + public void generateLiquibaseChangeset(Database database, String changeLogFilePath) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { - public static SchemaSQLGenerationDataModel load(String fileName) throws IOException, ClassNotFoundException { - FileInputStream file = new FileInputStream(fileName); - ObjectInputStream in = new ObjectInputStream(file); - - SchemaSQLGenerationDataModel model = (SchemaSQLGenerationDataModel) in.readObject(); - return model; - } - - public void generateLiquibaseChangeset(Database database, String changeLogFilePath) throws InvalidExampleException, DatabaseException, IOException { String changeSetId = Long.toString(System.currentTimeMillis()); generateLiquibaseChangeset(database,changeLogFilePath, changeSetId, "Spring Data JDBC"); } - public void generateLiquibaseChangeset(Database database, String changeLogFilePath, String changeSetId, String changeSetAuthor) throws InvalidExampleException, DatabaseException, IOException { + public void generateLiquibaseChangeset(Database database, String changeLogFilePath, String changeSetId, String changeSetAuthor) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { + CatalogAndSchema[] schemas = new CatalogAndSchema[] { database.getDefaultSchema() }; SnapshotControl snapshotControl = new SnapshotControl(database); @@ -186,15 +174,30 @@ public void generateLiquibaseChangeset(Database database, String changeLogFilePa SchemaSQLGenerationDataModel liquibaseModel = new SchemaSQLGenerationDataModel(); + for (TableModel t : tableData) { + if (t.getSchema() == null || t.getSchema().isEmpty()) { + t.setSchema(database.getDefaultSchema().getCatalogName()); + } + } + for (liquibase.structure.core.Table table : tables) { - SqlIdentifier tableName = new DefaultSqlIdentifier(table.getName(), false); + // Exclude internal Liquibase tables from comparison + if (table.getName().startsWith("DATABASECHANGELOG")) { + continue; + } + + SqlIdentifier tableName = new DerivedSqlIdentifier(table.getName(), true); TableModel tableModel = new TableModel(table.getSchema().getCatalogName(), tableName); liquibaseModel.getTableData().add(tableModel); - //System.out.println(table.getName()); + List columns = table.getColumns(); for (liquibase.structure.core.Column column : columns) { - //System.out.println("--- " + column.getName() + "," + column.getType()); + SqlIdentifier columnName = new DerivedSqlIdentifier(column.getName(), true); + String type = column.getType().toString(); + boolean nullable = column.isNullable(); + ColumnModel columnModel = new ColumnModel(columnName, type, nullable, false); + tableModel.getColumns().add(columnModel); } } @@ -202,31 +205,84 @@ public void generateLiquibaseChangeset(Database database, String changeLogFilePa File changeLogFile = new File(changeLogFilePath); - ChangeLogSerializerFactory factory = ChangeLogSerializerFactory.getInstance(); + DatabaseChangeLog databaseChangeLog; + + try { + YamlChangeLogParser parser = new YamlChangeLogParser(); + DirectoryResourceAccessor resourceAccessor = new DirectoryResourceAccessor(changeLogFile.getParentFile()); + ChangeLogParameters parameters = new ChangeLogParameters(); + databaseChangeLog = parser.parse(changeLogFilePath, parameters, resourceAccessor); + } catch (Exception ex) { + databaseChangeLog = new DatabaseChangeLog(changeLogFilePath); + } + ChangeLogSerializer serializer = new YamlChangeLogSerializer(); - DatabaseChangeLog databaseChangeLog = new DatabaseChangeLog(changeLogFilePath); ChangeSet changeSet = new ChangeSet(changeSetId, changeSetAuthor, false, false, "", "", "" , databaseChangeLog); for (TableModel t : difference.getTableAdditions()) { - System.out.println(t.getName().getReference() + " to be added."); CreateTableChange newTable = createAddTableChange(t); changeSet.addChange(newTable); } for (TableModel t : difference.getTableDeletions()) { - System.out.println(t.getName().getReference() + " to be removed."); DropTableChange dropTable = createDropTableChange(t); changeSet.addChange(dropTable); } + for (TableDiff t : difference.getTableDiff()) { + + if (t.getAddedColumns().size() > 0) { + AddColumnChange addColumnChange = new AddColumnChange(); + addColumnChange.setSchemaName(t.getTableModel().getSchema()); + addColumnChange.setTableName(t.getTableModel().getName().getReference()); + + for (ColumnModel column : t.getAddedColumns()) { + AddColumnConfig addColumn = createAddColumnChange(column); + addColumnChange.addColumn(addColumn); + } + + changeSet.addChange(addColumnChange); + } + + if (t.getDeletedColumns().size() > 0) { + DropColumnChange dropColumnChange = new DropColumnChange(); + dropColumnChange.setSchemaName(t.getTableModel().getSchema()); + dropColumnChange.setTableName(t.getTableModel().getName().getReference()); + + List dropColumns = new ArrayList(); + for (ColumnModel column : t.getDeletedColumns()) { + ColumnConfig config = new ColumnConfig(); + config.setName(column.getName().getReference()); + dropColumns.add(config); + } + dropColumnChange.setColumns(dropColumns); + changeSet.addChange(dropColumnChange); + } + } + List changes = new ArrayList(); + for (ChangeSet change : databaseChangeLog.getChangeSets()) { + changes.add(change); + } changes.add(changeSet); FileOutputStream fos = new FileOutputStream(changeLogFile); serializer.write(changes, fos); + } + + private AddColumnConfig createAddColumnChange(ColumnModel column) { + + AddColumnConfig config = new AddColumnConfig(); + config.setName(column.getName().getReference()); + config.setType(column.getType()); + if (column.isIdentityColumn()) { + config.setAutoIncrement(true); + } + return config; } CreateTableChange createAddTableChange(TableModel table) { + CreateTableChange change = new CreateTableChange(); change.setSchemaName(table.getSchema()); change.setTableName(table.getName().getReference()); @@ -235,6 +291,13 @@ CreateTableChange createAddTableChange(TableModel table) { ColumnConfig columnConfig = new ColumnConfig(); columnConfig.setName(column.getName().getReference()); columnConfig.setType(column.getType()); + + if (column.isIdentityColumn()) { + columnConfig.setAutoIncrement(true); + ConstraintsConfig constraints = new ConstraintsConfig(); + constraints.setPrimaryKey(true); + columnConfig.setConstraints(constraints); + } change.addColumn(columnConfig); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java index 3e6fe4fe43..593fd5eb2c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java @@ -1,28 +1,30 @@ package org.springframework.data.relational.core.mapping.schemasqlgeneration; -import org.springframework.data.relational.core.sql.SqlIdentifier; - import java.util.ArrayList; import java.util.List; public class TableDiff { - private final TableModel table; + private final TableModel tableModel; private final List addedColumns = new ArrayList(); private final List deletedColumns = new ArrayList(); - public TableDiff(TableModel table) { - this.table = table; + public TableDiff(TableModel tableModel) { + + this.tableModel = tableModel; } - public TableModel getTable() { - return table; + public TableModel getTableModel() { + + return tableModel; } public List getAddedColumns() { + return addedColumns; } public List getDeletedColumns() { + return deletedColumns; } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java index 20f16a5171..98a11f45d8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java @@ -27,50 +27,74 @@ * @author Kurt Niemi */ public class TableModel { - private final String schema; - private final SqlIdentifier name; + private String schema; + private SqlIdentifier name; private final List columns = new ArrayList(); private final List keyColumns = new ArrayList(); - private final List foreignKeyColumns = new ArrayList(); public TableModel(String schema, SqlIdentifier name) { + this.schema = schema; this.name = name; } public TableModel(SqlIdentifier name) { + this(null, name); } public String getSchema() { + return schema; } + public void setSchema(String schema) { + + this.schema = schema; + } + public SqlIdentifier getName() { + return name; } - public List getColumns() { - return columns; - } + public void setName(SqlIdentifier name) { - public List getKeyColumns() { - return keyColumns; + this.name = name; } - public List getForeignKeyColumns() { - return foreignKeyColumns; + public List getColumns() { + + return columns; } @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + TableModel that = (TableModel) o; - return Objects.equals(schema, that.schema) && name.equals(that.name); + + // If we are missing the schema for either TableModel we will not treat that as being different + if (schema != null && that.schema != null && !schema.isEmpty() && !that.schema.isEmpty()) { + if (!Objects.equals(schema, that.schema)) { + return false; + } + } + if (!name.getReference().toUpperCase().equals(that.name.getReference().toUpperCase())) { + return false; + } + return true; } @Override public int hashCode() { - return Objects.hash(schema, name); + + return Objects.hash(name.getReference().toUpperCase()); } } From 33e3308ae15611573ce19329d78456b8b0749110 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Fri, 19 May 2023 21:49:47 -0400 Subject: [PATCH 10/32] Re-run new-issue-branch script after rebase --- pom.xml | 2 +- spring-data-jdbc-distribution/pom.xml | 2 +- spring-data-jdbc/pom.xml | 4 ++-- spring-data-r2dbc/pom.xml | 4 ++-- spring-data-relational/pom.xml | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index e3a171365a..fb5b63ccde 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-1480-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index d834798834..d20edea1f3 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-1480-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index cd2fb2fc92..651f66faf3 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 3.2.0-SNAPSHOT + 3.2.0-1480-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-1480-SNAPSHOT diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index 98019e0295..92f55a39dd 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-r2dbc - 3.2.0-SNAPSHOT + 3.2.0-1480-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-1480-SNAPSHOT diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 3058eb0afa..8760457655 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 3.2.0-SNAPSHOT + 3.2.0-1480-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-1480-SNAPSHOT From 2335a4b9de821dea6f6162be563613e2f9b301eb Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Fri, 19 May 2023 22:08:50 -0400 Subject: [PATCH 11/32] Re-add things lost in rebase --- pom.xml | 2 +- spring-data-relational/pom.xml | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fb5b63ccde..06e5b65ff4 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ spring-data-jdbc 3.2.0-SNAPSHOT - 4.21.1 + 4.21.1 reuseReports diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 8760457655..d23880e4a0 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -50,6 +50,14 @@ spring-core + + org.liquibase + liquibase-core + ${liquibase.version} + compile + true + + com.google.code.findbugs jsr305 @@ -90,6 +98,13 @@ test + + org.testcontainers + testcontainers + ${testcontainers} + test + + From a22eccc7641d48b575719153a2c4f973a7cdfa63 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Fri, 19 May 2023 22:16:41 -0400 Subject: [PATCH 12/32] cleanup - remove unnecessary changes --- .../data/relational/core/mapping/RelationalMappingContext.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java index 4a20ba7080..779a70ddb5 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java @@ -22,8 +22,6 @@ import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; -import java.util.Iterator; - /** * {@link MappingContext} implementation. * @@ -103,4 +101,5 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert public NamingStrategy getNamingStrategy() { return this.namingStrategy; } + } From d2d7845009fcd8d19a384ad0f80ca4b49bb49620 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Sun, 21 May 2023 16:30:57 -0400 Subject: [PATCH 13/32] Refactoring - move logic for generating Liquibase Change Sets into its own class --- .../LiquibaseChangeSetGenerator.java | 219 ++++++++++++++++++ .../SchemaSQLGenerationDataModel.java | 155 ------------- 2 files changed, 219 insertions(+), 155 deletions(-) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java new file mode 100644 index 0000000000..bec3a517d3 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java @@ -0,0 +1,219 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.relational.core.mapping.schemasqlgeneration; + +import liquibase.CatalogAndSchema; +import liquibase.change.AddColumnConfig; +import liquibase.change.ColumnConfig; +import liquibase.change.ConstraintsConfig; +import liquibase.change.core.AddColumnChange; +import liquibase.change.core.CreateTableChange; +import liquibase.change.core.DropColumnChange; +import liquibase.change.core.DropTableChange; +import liquibase.changelog.ChangeLogChild; +import liquibase.changelog.ChangeLogParameters; +import liquibase.changelog.ChangeSet; +import liquibase.changelog.DatabaseChangeLog; +import liquibase.database.Database; +import liquibase.exception.ChangeLogParseException; +import liquibase.exception.DatabaseException; +import liquibase.parser.core.yaml.YamlChangeLogParser; +import liquibase.resource.DirectoryResourceAccessor; +import liquibase.serializer.ChangeLogSerializer; +import liquibase.serializer.core.yaml.YamlChangeLogSerializer; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.InvalidExampleException; +import liquibase.snapshot.SnapshotControl; +import liquibase.snapshot.SnapshotGeneratorFactory; +import liquibase.structure.core.Column; +import liquibase.structure.core.Table; +import org.springframework.data.relational.core.mapping.DerivedSqlIdentifier; +import org.springframework.data.relational.core.sql.SqlIdentifier; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class LiquibaseChangeSetGenerator { + + private final SchemaSQLGenerationDataModel sourceModel; + private final Database targetDatabase; + + public LiquibaseChangeSetGenerator(SchemaSQLGenerationDataModel sourceModel, Database targetDatabase) { + + this.sourceModel = sourceModel; + this.targetDatabase = targetDatabase; + } + + public void generateLiquibaseChangeset(String changeLogFilePath) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { + + String changeSetId = Long.toString(System.currentTimeMillis()); + generateLiquibaseChangeset(changeLogFilePath, changeSetId, "Spring Data JDBC"); + } + + public void generateLiquibaseChangeset(String changeLogFilePath, String changeSetId, String changeSetAuthor) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { + + CatalogAndSchema[] schemas = new CatalogAndSchema[] { targetDatabase.getDefaultSchema() }; + SnapshotControl snapshotControl = new SnapshotControl(targetDatabase); + + DatabaseSnapshot snapshot = SnapshotGeneratorFactory.getInstance().createSnapshot(schemas, targetDatabase, snapshotControl); + Set tables = snapshot.get(liquibase.structure.core.Table.class); + + SchemaSQLGenerationDataModel liquibaseModel = new SchemaSQLGenerationDataModel(); + + for (TableModel t : sourceModel.getTableData()) { + if (t.getSchema() == null || t.getSchema().isEmpty()) { + t.setSchema(targetDatabase.getDefaultSchema().getCatalogName()); + } + } + + for (liquibase.structure.core.Table table : tables) { + + // Exclude internal Liquibase tables from comparison + if (table.getName().startsWith("DATABASECHANGELOG")) { + continue; + } + + SqlIdentifier tableName = new DerivedSqlIdentifier(table.getName(), true); + TableModel tableModel = new TableModel(table.getSchema().getCatalogName(), tableName); + liquibaseModel.getTableData().add(tableModel); + + List columns = table.getColumns(); + for (liquibase.structure.core.Column column : columns) { + SqlIdentifier columnName = new DerivedSqlIdentifier(column.getName(), true); + String type = column.getType().toString(); + boolean nullable = column.isNullable(); + ColumnModel columnModel = new ColumnModel(columnName, type, nullable, false); + tableModel.getColumns().add(columnModel); + } + } + + SchemaDiff difference = sourceModel.diffModel(liquibaseModel); + + File changeLogFile = new File(changeLogFilePath); + + DatabaseChangeLog databaseChangeLog; + + try { + YamlChangeLogParser parser = new YamlChangeLogParser(); + DirectoryResourceAccessor resourceAccessor = new DirectoryResourceAccessor(changeLogFile.getParentFile()); + ChangeLogParameters parameters = new ChangeLogParameters(); + databaseChangeLog = parser.parse(changeLogFilePath, parameters, resourceAccessor); + } catch (Exception ex) { + databaseChangeLog = new DatabaseChangeLog(changeLogFilePath); + } + + ChangeLogSerializer serializer = new YamlChangeLogSerializer(); + ChangeSet changeSet = new ChangeSet(changeSetId, changeSetAuthor, false, false, "", "", "" , databaseChangeLog); + + for (TableModel t : difference.getTableAdditions()) { + CreateTableChange newTable = createAddTableChange(t); + changeSet.addChange(newTable); + } + + for (TableModel t : difference.getTableDeletions()) { + DropTableChange dropTable = createDropTableChange(t); + changeSet.addChange(dropTable); + } + + for (TableDiff t : difference.getTableDiff()) { + + if (t.getAddedColumns().size() > 0) { + AddColumnChange addColumnChange = new AddColumnChange(); + addColumnChange.setSchemaName(t.getTableModel().getSchema()); + addColumnChange.setTableName(t.getTableModel().getName().getReference()); + + for (ColumnModel column : t.getAddedColumns()) { + AddColumnConfig addColumn = createAddColumnChange(column); + addColumnChange.addColumn(addColumn); + } + + changeSet.addChange(addColumnChange); + } + + if (t.getDeletedColumns().size() > 0) { + DropColumnChange dropColumnChange = new DropColumnChange(); + dropColumnChange.setSchemaName(t.getTableModel().getSchema()); + dropColumnChange.setTableName(t.getTableModel().getName().getReference()); + + List dropColumns = new ArrayList(); + for (ColumnModel column : t.getDeletedColumns()) { + ColumnConfig config = new ColumnConfig(); + config.setName(column.getName().getReference()); + dropColumns.add(config); + } + dropColumnChange.setColumns(dropColumns); + changeSet.addChange(dropColumnChange); + } + } + + List changes = new ArrayList(); + for (ChangeSet change : databaseChangeLog.getChangeSets()) { + changes.add(change); + } + changes.add(changeSet); + FileOutputStream fos = new FileOutputStream(changeLogFile); + serializer.write(changes, fos); + } + + private AddColumnConfig createAddColumnChange(ColumnModel column) { + + AddColumnConfig config = new AddColumnConfig(); + config.setName(column.getName().getReference()); + config.setType(column.getType()); + + if (column.isIdentityColumn()) { + config.setAutoIncrement(true); + } + return config; + } + + CreateTableChange createAddTableChange(TableModel table) { + + CreateTableChange change = new CreateTableChange(); + change.setSchemaName(table.getSchema()); + change.setTableName(table.getName().getReference()); + + for (ColumnModel column : table.getColumns()) { + ColumnConfig columnConfig = new ColumnConfig(); + columnConfig.setName(column.getName().getReference()); + columnConfig.setType(column.getType()); + + if (column.isIdentityColumn()) { + columnConfig.setAutoIncrement(true); + ConstraintsConfig constraints = new ConstraintsConfig(); + constraints.setPrimaryKey(true); + columnConfig.setConstraints(constraints); + } + change.addColumn(columnConfig); + } + + return change; + } + + DropTableChange createDropTableChange(TableModel table) { + DropTableChange change = new DropTableChange(); + change.setSchemaName(table.getSchema()); + change.setTableName(table.getName().getReference()); + change.setCascadeConstraints(true); + + return change; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java index b626360462..4ea3cb16c2 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java @@ -157,159 +157,4 @@ public SchemaDiff diffModel(SchemaSQLGenerationDataModel source) { public List getTableData() { return tableData; } - - public void generateLiquibaseChangeset(Database database, String changeLogFilePath) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { - - String changeSetId = Long.toString(System.currentTimeMillis()); - generateLiquibaseChangeset(database,changeLogFilePath, changeSetId, "Spring Data JDBC"); - } - - public void generateLiquibaseChangeset(Database database, String changeLogFilePath, String changeSetId, String changeSetAuthor) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { - - CatalogAndSchema[] schemas = new CatalogAndSchema[] { database.getDefaultSchema() }; - SnapshotControl snapshotControl = new SnapshotControl(database); - - DatabaseSnapshot snapshot = SnapshotGeneratorFactory.getInstance().createSnapshot(schemas, database, snapshotControl); - Set tables = snapshot.get(liquibase.structure.core.Table.class); - - SchemaSQLGenerationDataModel liquibaseModel = new SchemaSQLGenerationDataModel(); - - for (TableModel t : tableData) { - if (t.getSchema() == null || t.getSchema().isEmpty()) { - t.setSchema(database.getDefaultSchema().getCatalogName()); - } - } - - for (liquibase.structure.core.Table table : tables) { - - // Exclude internal Liquibase tables from comparison - if (table.getName().startsWith("DATABASECHANGELOG")) { - continue; - } - - SqlIdentifier tableName = new DerivedSqlIdentifier(table.getName(), true); - TableModel tableModel = new TableModel(table.getSchema().getCatalogName(), tableName); - liquibaseModel.getTableData().add(tableModel); - - List columns = table.getColumns(); - for (liquibase.structure.core.Column column : columns) { - SqlIdentifier columnName = new DerivedSqlIdentifier(column.getName(), true); - String type = column.getType().toString(); - boolean nullable = column.isNullable(); - ColumnModel columnModel = new ColumnModel(columnName, type, nullable, false); - tableModel.getColumns().add(columnModel); - } - } - - SchemaDiff difference = diffModel(liquibaseModel); - - File changeLogFile = new File(changeLogFilePath); - - DatabaseChangeLog databaseChangeLog; - - try { - YamlChangeLogParser parser = new YamlChangeLogParser(); - DirectoryResourceAccessor resourceAccessor = new DirectoryResourceAccessor(changeLogFile.getParentFile()); - ChangeLogParameters parameters = new ChangeLogParameters(); - databaseChangeLog = parser.parse(changeLogFilePath, parameters, resourceAccessor); - } catch (Exception ex) { - databaseChangeLog = new DatabaseChangeLog(changeLogFilePath); - } - - ChangeLogSerializer serializer = new YamlChangeLogSerializer(); - ChangeSet changeSet = new ChangeSet(changeSetId, changeSetAuthor, false, false, "", "", "" , databaseChangeLog); - - for (TableModel t : difference.getTableAdditions()) { - CreateTableChange newTable = createAddTableChange(t); - changeSet.addChange(newTable); - } - - for (TableModel t : difference.getTableDeletions()) { - DropTableChange dropTable = createDropTableChange(t); - changeSet.addChange(dropTable); - } - - for (TableDiff t : difference.getTableDiff()) { - - if (t.getAddedColumns().size() > 0) { - AddColumnChange addColumnChange = new AddColumnChange(); - addColumnChange.setSchemaName(t.getTableModel().getSchema()); - addColumnChange.setTableName(t.getTableModel().getName().getReference()); - - for (ColumnModel column : t.getAddedColumns()) { - AddColumnConfig addColumn = createAddColumnChange(column); - addColumnChange.addColumn(addColumn); - } - - changeSet.addChange(addColumnChange); - } - - if (t.getDeletedColumns().size() > 0) { - DropColumnChange dropColumnChange = new DropColumnChange(); - dropColumnChange.setSchemaName(t.getTableModel().getSchema()); - dropColumnChange.setTableName(t.getTableModel().getName().getReference()); - - List dropColumns = new ArrayList(); - for (ColumnModel column : t.getDeletedColumns()) { - ColumnConfig config = new ColumnConfig(); - config.setName(column.getName().getReference()); - dropColumns.add(config); - } - dropColumnChange.setColumns(dropColumns); - changeSet.addChange(dropColumnChange); - } - } - - List changes = new ArrayList(); - for (ChangeSet change : databaseChangeLog.getChangeSets()) { - changes.add(change); - } - changes.add(changeSet); - FileOutputStream fos = new FileOutputStream(changeLogFile); - serializer.write(changes, fos); - } - - private AddColumnConfig createAddColumnChange(ColumnModel column) { - - AddColumnConfig config = new AddColumnConfig(); - config.setName(column.getName().getReference()); - config.setType(column.getType()); - - if (column.isIdentityColumn()) { - config.setAutoIncrement(true); - } - return config; - } - - CreateTableChange createAddTableChange(TableModel table) { - - CreateTableChange change = new CreateTableChange(); - change.setSchemaName(table.getSchema()); - change.setTableName(table.getName().getReference()); - - for (ColumnModel column : table.getColumns()) { - ColumnConfig columnConfig = new ColumnConfig(); - columnConfig.setName(column.getName().getReference()); - columnConfig.setType(column.getType()); - - if (column.isIdentityColumn()) { - columnConfig.setAutoIncrement(true); - ConstraintsConfig constraints = new ConstraintsConfig(); - constraints.setPrimaryKey(true); - columnConfig.setConstraints(constraints); - } - change.addColumn(columnConfig); - } - - return change; - } - - DropTableChange createDropTableChange(TableModel table) { - DropTableChange change = new DropTableChange(); - change.setSchemaName(table.getSchema()); - change.setTableName(table.getName().getReference()); - change.setCascadeConstraints(true); - - return change; - } } From 023b34f1afd69abd483b5036584d047710876685 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Sun, 21 May 2023 17:26:54 -0400 Subject: [PATCH 14/32] Additional refactoring on LiquibaseChangeSetGenerator --- .../LiquibaseChangeSetGenerator.java | 142 +++++++++++------- 1 file changed, 84 insertions(+), 58 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java index bec3a517d3..91e375a394 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java @@ -45,6 +45,7 @@ import org.springframework.data.relational.core.sql.SqlIdentifier; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; @@ -70,77 +71,44 @@ public void generateLiquibaseChangeset(String changeLogFilePath) throws InvalidE public void generateLiquibaseChangeset(String changeLogFilePath, String changeSetId, String changeSetAuthor) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { - CatalogAndSchema[] schemas = new CatalogAndSchema[] { targetDatabase.getDefaultSchema() }; - SnapshotControl snapshotControl = new SnapshotControl(targetDatabase); - - DatabaseSnapshot snapshot = SnapshotGeneratorFactory.getInstance().createSnapshot(schemas, targetDatabase, snapshotControl); - Set
tables = snapshot.get(liquibase.structure.core.Table.class); - - SchemaSQLGenerationDataModel liquibaseModel = new SchemaSQLGenerationDataModel(); + SchemaSQLGenerationDataModel liquibaseModel = getLiquibaseModel(); + SchemaDiff difference = sourceModel.diffModel(liquibaseModel); - for (TableModel t : sourceModel.getTableData()) { - if (t.getSchema() == null || t.getSchema().isEmpty()) { - t.setSchema(targetDatabase.getDefaultSchema().getCatalogName()); - } - } + DatabaseChangeLog databaseChangeLog = getDatabaseChangeLog(changeLogFilePath); - for (liquibase.structure.core.Table table : tables) { + ChangeSet changeSet = new ChangeSet(changeSetId, changeSetAuthor, false, false, "", "", "" , databaseChangeLog); - // Exclude internal Liquibase tables from comparison - if (table.getName().startsWith("DATABASECHANGELOG")) { - continue; - } + generateTableAdditionsDeletions(changeSet, difference); + generateTableModifications(changeSet, difference); - SqlIdentifier tableName = new DerivedSqlIdentifier(table.getName(), true); - TableModel tableModel = new TableModel(table.getSchema().getCatalogName(), tableName); - liquibaseModel.getTableData().add(tableModel); - - List columns = table.getColumns(); - for (liquibase.structure.core.Column column : columns) { - SqlIdentifier columnName = new DerivedSqlIdentifier(column.getName(), true); - String type = column.getType().toString(); - boolean nullable = column.isNullable(); - ColumnModel columnModel = new ColumnModel(columnName, type, nullable, false); - tableModel.getColumns().add(columnModel); - } - } - - SchemaDiff difference = sourceModel.diffModel(liquibaseModel); File changeLogFile = new File(changeLogFilePath); + writeChangeSet(databaseChangeLog, changeSet, changeLogFile); + } - DatabaseChangeLog databaseChangeLog; - - try { - YamlChangeLogParser parser = new YamlChangeLogParser(); - DirectoryResourceAccessor resourceAccessor = new DirectoryResourceAccessor(changeLogFile.getParentFile()); - ChangeLogParameters parameters = new ChangeLogParameters(); - databaseChangeLog = parser.parse(changeLogFilePath, parameters, resourceAccessor); - } catch (Exception ex) { - databaseChangeLog = new DatabaseChangeLog(changeLogFilePath); - } - - ChangeLogSerializer serializer = new YamlChangeLogSerializer(); - ChangeSet changeSet = new ChangeSet(changeSetId, changeSetAuthor, false, false, "", "", "" , databaseChangeLog); + private void generateTableAdditionsDeletions(ChangeSet changeSet, SchemaDiff difference) { - for (TableModel t : difference.getTableAdditions()) { - CreateTableChange newTable = createAddTableChange(t); + for (TableModel table : difference.getTableAdditions()) { + CreateTableChange newTable = createAddTableChange(table); changeSet.addChange(newTable); } - for (TableModel t : difference.getTableDeletions()) { - DropTableChange dropTable = createDropTableChange(t); + for (TableModel table : difference.getTableDeletions()) { + DropTableChange dropTable = createDropTableChange(table); changeSet.addChange(dropTable); } + } - for (TableDiff t : difference.getTableDiff()) { + private void generateTableModifications(ChangeSet changeSet, SchemaDiff difference) { - if (t.getAddedColumns().size() > 0) { + for (TableDiff table : difference.getTableDiff()) { + + if (table.getAddedColumns().size() > 0) { AddColumnChange addColumnChange = new AddColumnChange(); - addColumnChange.setSchemaName(t.getTableModel().getSchema()); - addColumnChange.setTableName(t.getTableModel().getName().getReference()); + addColumnChange.setSchemaName(table.getTableModel().getSchema()); + addColumnChange.setTableName(table.getTableModel().getName().getReference()); - for (ColumnModel column : t.getAddedColumns()) { + for (ColumnModel column : table.getAddedColumns()) { AddColumnConfig addColumn = createAddColumnChange(column); addColumnChange.addColumn(addColumn); } @@ -148,13 +116,13 @@ public void generateLiquibaseChangeset(String changeLogFilePath, String changeSe changeSet.addChange(addColumnChange); } - if (t.getDeletedColumns().size() > 0) { + if (table.getDeletedColumns().size() > 0) { DropColumnChange dropColumnChange = new DropColumnChange(); - dropColumnChange.setSchemaName(t.getTableModel().getSchema()); - dropColumnChange.setTableName(t.getTableModel().getName().getReference()); + dropColumnChange.setSchemaName(table.getTableModel().getSchema()); + dropColumnChange.setTableName(table.getTableModel().getName().getReference()); List dropColumns = new ArrayList(); - for (ColumnModel column : t.getDeletedColumns()) { + for (ColumnModel column : table.getDeletedColumns()) { ColumnConfig config = new ColumnConfig(); config.setName(column.getName().getReference()); dropColumns.add(config); @@ -163,7 +131,26 @@ public void generateLiquibaseChangeset(String changeLogFilePath, String changeSe changeSet.addChange(dropColumnChange); } } + } + + private DatabaseChangeLog getDatabaseChangeLog(String changeLogFilePath) { + File changeLogFile = new File(changeLogFilePath); + DatabaseChangeLog databaseChangeLog = null; + + try { + YamlChangeLogParser parser = new YamlChangeLogParser(); + DirectoryResourceAccessor resourceAccessor = new DirectoryResourceAccessor(changeLogFile.getParentFile()); + ChangeLogParameters parameters = new ChangeLogParameters(); + databaseChangeLog = parser.parse(changeLogFilePath, parameters, resourceAccessor); + } catch (Exception ex) { + databaseChangeLog = new DatabaseChangeLog(changeLogFilePath); + } + return databaseChangeLog; + } + private void writeChangeSet(DatabaseChangeLog databaseChangeLog, ChangeSet changeSet, File changeLogFile) throws FileNotFoundException, IOException { + + ChangeLogSerializer serializer = new YamlChangeLogSerializer(); List changes = new ArrayList(); for (ChangeSet change : databaseChangeLog.getChangeSets()) { changes.add(change); @@ -173,6 +160,45 @@ public void generateLiquibaseChangeset(String changeLogFilePath, String changeSe serializer.write(changes, fos); } + private SchemaSQLGenerationDataModel getLiquibaseModel() throws DatabaseException, InvalidExampleException { + SchemaSQLGenerationDataModel liquibaseModel = new SchemaSQLGenerationDataModel(); + + CatalogAndSchema[] schemas = new CatalogAndSchema[] { targetDatabase.getDefaultSchema() }; + SnapshotControl snapshotControl = new SnapshotControl(targetDatabase); + + DatabaseSnapshot snapshot = SnapshotGeneratorFactory.getInstance().createSnapshot(schemas, targetDatabase, snapshotControl); + Set
tables = snapshot.get(liquibase.structure.core.Table.class); + + for (TableModel t : sourceModel.getTableData()) { + if (t.getSchema() == null || t.getSchema().isEmpty()) { + t.setSchema(targetDatabase.getDefaultSchema().getCatalogName()); + } + } + + for (liquibase.structure.core.Table table : tables) { + + // Exclude internal Liquibase tables from comparison + if (table.getName().startsWith("DATABASECHANGELOG")) { + continue; + } + + SqlIdentifier tableName = new DerivedSqlIdentifier(table.getName(), true); + TableModel tableModel = new TableModel(table.getSchema().getCatalogName(), tableName); + liquibaseModel.getTableData().add(tableModel); + + List columns = table.getColumns(); + for (liquibase.structure.core.Column column : columns) { + SqlIdentifier columnName = new DerivedSqlIdentifier(column.getName(), true); + String type = column.getType().toString(); + boolean nullable = column.isNullable(); + ColumnModel columnModel = new ColumnModel(columnName, type, nullable, false); + tableModel.getColumns().add(columnModel); + } + } + + return liquibaseModel; + } + private AddColumnConfig createAddColumnChange(ColumnModel column) { AddColumnConfig config = new AddColumnConfig(); From 03597b92dbf3926e4542c54c34b2794aa25e8518 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Mon, 22 May 2023 09:24:12 -0400 Subject: [PATCH 15/32] Refactor rename SchemaSQLGenerationDataModel to SchemaModel --- .../LiquibaseChangeSetGenerator.java | 10 +++--- ...erationDataModel.java => SchemaModel.java} | 35 ++++--------------- ...aModelTests.java => SchemaModelTests.java} | 13 ++++--- 3 files changed, 17 insertions(+), 41 deletions(-) rename spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/{SchemaSQLGenerationDataModel.java => SchemaModel.java} (76%) rename spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/{SchemaSQLGenerationDataModelTests.java => SchemaModelTests.java} (85%) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java index 91e375a394..5aa869b31b 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java @@ -54,10 +54,10 @@ public class LiquibaseChangeSetGenerator { - private final SchemaSQLGenerationDataModel sourceModel; + private final SchemaModel sourceModel; private final Database targetDatabase; - public LiquibaseChangeSetGenerator(SchemaSQLGenerationDataModel sourceModel, Database targetDatabase) { + public LiquibaseChangeSetGenerator(SchemaModel sourceModel, Database targetDatabase) { this.sourceModel = sourceModel; this.targetDatabase = targetDatabase; @@ -71,7 +71,7 @@ public void generateLiquibaseChangeset(String changeLogFilePath) throws InvalidE public void generateLiquibaseChangeset(String changeLogFilePath, String changeSetId, String changeSetAuthor) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { - SchemaSQLGenerationDataModel liquibaseModel = getLiquibaseModel(); + SchemaModel liquibaseModel = getLiquibaseModel(); SchemaDiff difference = sourceModel.diffModel(liquibaseModel); DatabaseChangeLog databaseChangeLog = getDatabaseChangeLog(changeLogFilePath); @@ -160,8 +160,8 @@ private void writeChangeSet(DatabaseChangeLog databaseChangeLog, ChangeSet chang serializer.write(changes, fos); } - private SchemaSQLGenerationDataModel getLiquibaseModel() throws DatabaseException, InvalidExampleException { - SchemaSQLGenerationDataModel liquibaseModel = new SchemaSQLGenerationDataModel(); + private SchemaModel getLiquibaseModel() throws DatabaseException, InvalidExampleException { + SchemaModel liquibaseModel = new SchemaModel(); CatalogAndSchema[] schemas = new CatalogAndSchema[] { targetDatabase.getDefaultSchema() }; SnapshotControl snapshotControl = new SnapshotControl(targetDatabase); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java similarity index 76% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java rename to spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java index 4ea3cb16c2..8a4dfeba65 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaSQLGenerationDataModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java @@ -15,32 +15,9 @@ */ package org.springframework.data.relational.core.mapping.schemasqlgeneration; -import liquibase.CatalogAndSchema; -import liquibase.change.AddColumnConfig; -import liquibase.change.ColumnConfig; -import liquibase.change.ConstraintsConfig; -import liquibase.change.core.AddColumnChange; -import liquibase.change.core.CreateTableChange; -import liquibase.change.core.DropColumnChange; -import liquibase.change.core.DropTableChange; -import liquibase.changelog.ChangeLogChild; -import liquibase.changelog.ChangeLogParameters; -import liquibase.changelog.ChangeSet; -import liquibase.changelog.DatabaseChangeLog; -import liquibase.database.Database; -import liquibase.exception.ChangeLogParseException; -import liquibase.exception.DatabaseException; -import liquibase.parser.core.yaml.YamlChangeLogParser; -import liquibase.resource.DirectoryResourceAccessor; -import liquibase.serializer.ChangeLogSerializer; -import liquibase.serializer.core.yaml.YamlChangeLogSerializer; -import liquibase.snapshot.*; import org.springframework.data.annotation.Id; -import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.*; -import org.springframework.data.relational.core.sql.SqlIdentifier; -import java.io.*; import java.util.*; /** @@ -49,7 +26,7 @@ * * @author Kurt Niemi */ -public class SchemaSQLGenerationDataModel +public class SchemaModel { private final List tableData = new ArrayList(); public BaseTypeMapper typeMapper; @@ -57,14 +34,14 @@ public class SchemaSQLGenerationDataModel /** * Create empty model */ - public SchemaSQLGenerationDataModel() { + public SchemaModel() { } /** * Create model from a RelationalMappingContext */ - public SchemaSQLGenerationDataModel(RelationalMappingContext context) { + public SchemaModel(RelationalMappingContext context) { if (typeMapper == null) { typeMapper = new BaseTypeMapper(); @@ -96,7 +73,7 @@ public SchemaSQLGenerationDataModel(RelationalMappingContext context) { } } - void diffTableAdditionDeletion(SchemaSQLGenerationDataModel source, SchemaDiff diff) { + void diffTableAdditionDeletion(SchemaModel source, SchemaDiff diff) { Set sourceTableData = new HashSet(source.getTableData()); Set targetTableData = new HashSet(getTableData()); @@ -112,7 +89,7 @@ void diffTableAdditionDeletion(SchemaSQLGenerationDataModel source, SchemaDiff d diff.getTableAdditions().addAll(addedTables); } - void diffTable(SchemaSQLGenerationDataModel source, SchemaDiff diff) { + void diffTable(SchemaModel source, SchemaDiff diff) { HashMap sourceTablesMap = new HashMap(); for (TableModel table : source.getTableData()) { @@ -144,7 +121,7 @@ void diffTable(SchemaSQLGenerationDataModel source, SchemaDiff diff) { } } - public SchemaDiff diffModel(SchemaSQLGenerationDataModel source) { + public SchemaDiff diffModel(SchemaModel source) { SchemaDiff diff = new SchemaDiff(); diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaSQLGenerationDataModelTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java similarity index 85% rename from spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaSQLGenerationDataModelTests.java rename to spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java index 5492df54ff..e703432ef7 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaSQLGenerationDataModelTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java @@ -15,7 +15,6 @@ */ package org.springframework.data.relational.core.sql; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.RelationalMappingContext; @@ -25,22 +24,22 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Unit tests for the {@link SchemaSQLGenerationDataModel}. + * Unit tests for the {@link SchemaModel}. * * @author Kurt Niemi */ -public class SchemaSQLGenerationDataModelTests { +public class SchemaModelTests { @Test void testDiffSchema() { RelationalMappingContext context = new RelationalMappingContext(); - context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Table1.class); - context.getRequiredPersistentEntity(SchemaSQLGenerationDataModelTests.Table2.class); + context.getRequiredPersistentEntity(SchemaModelTests.Table1.class); + context.getRequiredPersistentEntity(SchemaModelTests.Table2.class); - SchemaSQLGenerationDataModel model = new SchemaSQLGenerationDataModel(context); + SchemaModel model = new SchemaModel(context); - SchemaSQLGenerationDataModel newModel = new SchemaSQLGenerationDataModel(context); + SchemaModel newModel = new SchemaModel(context); // Add column to table SqlIdentifier newIdentifier = new DefaultSqlIdentifier("newcol", false); From 86c52f13609d35061416f36913d816dac07c2041 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Tue, 23 May 2023 21:07:22 -0400 Subject: [PATCH 16/32] Refactor Database type mapping and provide Default implementation to SchemaModel --- .../DatabaseTypeMapping.java | 29 +++++++++++++++++++ ...r.java => DefaultDatabaseTypeMapping.java} | 14 +++++++-- .../schemasqlgeneration/SchemaModel.java | 9 +++--- 3 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DatabaseTypeMapping.java rename spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/{BaseTypeMapper.java => DefaultDatabaseTypeMapping.java} (76%) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DatabaseTypeMapping.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DatabaseTypeMapping.java new file mode 100644 index 0000000000..bd99e4ce27 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DatabaseTypeMapping.java @@ -0,0 +1,29 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping.schemasqlgeneration; + +/** + * Interface for mapping a Java type to a Database type. + * + * To customize the mapping an instance of a class implementing {@link DatabaseTypeMapping} interface + * can be set on the {@link SchemaModel} class. + * + * @author Kurt Niemi + * @since 3.2 + */ +public interface DatabaseTypeMapping { + public String databaseTypeFromClass(Class type); +} \ No newline at end of file diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DefaultDatabaseTypeMapping.java similarity index 76% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java rename to spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DefaultDatabaseTypeMapping.java index a48b6b8d07..c4ebe81b51 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/BaseTypeMapper.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DefaultDatabaseTypeMapping.java @@ -17,11 +17,21 @@ import java.util.HashMap; -public class BaseTypeMapper { + +/** + * Class that provides a default implementation of mapping Java type to a Database type. + * + * To customize the mapping an instance of a class implementing {@link DatabaseTypeMapping} interface + * can be set on the {@link SchemaModel} class + * + * @author Kurt Niemi + * @since 3.2 + */ +public class DefaultDatabaseTypeMapping implements DatabaseTypeMapping { final HashMap,String> mapClassToDatabaseType = new HashMap,String>(); - public BaseTypeMapper() { + public DefaultDatabaseTypeMapping() { mapClassToDatabaseType.put(String.class, "VARCHAR(255 BYTE)"); mapClassToDatabaseType.put(Boolean.class, "TINYINT"); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java index 8a4dfeba65..66bfe30604 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java @@ -25,11 +25,12 @@ * to generate SQL for Schema generation. * * @author Kurt Niemi + * @since 3.2 */ public class SchemaModel { private final List tableData = new ArrayList(); - public BaseTypeMapper typeMapper; + public DatabaseTypeMapping databaseTypeMapping; /** * Create empty model @@ -43,8 +44,8 @@ public SchemaModel() { */ public SchemaModel(RelationalMappingContext context) { - if (typeMapper == null) { - typeMapper = new BaseTypeMapper(); + if (databaseTypeMapping == null) { + databaseTypeMapping = new DefaultDatabaseTypeMapping(); } for (RelationalPersistentEntity entity : context.getPersistentEntities()) { @@ -64,7 +65,7 @@ public SchemaModel(RelationalMappingContext context) { while (iter.hasNext()) { BasicRelationalPersistentProperty p = iter.next(); ColumnModel columnModel = new ColumnModel(p.getColumnName(), - typeMapper.databaseTypeFromClass(p.getActualType()), + databaseTypeMapping.databaseTypeFromClass(p.getActualType()), true, setIdentifierColumns.contains(p)); tableModel.getColumns().add(columnModel); } From c68584b288861037735c0294004a2dba9b1fe5c7 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Tue, 23 May 2023 21:09:44 -0400 Subject: [PATCH 17/32] Add additional Javadocs --- .../schemasqlgeneration/ColumnModel.java | 1 + .../LiquibaseChangeSetGenerator.java | 80 ++++++++++++++++++- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java index 596de7f4cf..fbd0d10981 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java @@ -24,6 +24,7 @@ * Class that models a Column for generating SQL for Schema generation. * * @author Kurt Niemi + * @since 3.2 */ public class ColumnModel { private final SqlIdentifier name; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java index 5aa869b31b..c26c84d739 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java @@ -52,27 +52,98 @@ import java.util.List; import java.util.Set; +/** + * Use this class to generate Liquibase change sets. + * + * First create a {@link SchemaModel} instance passing in a RelationalContext to have + * a model that represents the Table(s)/Column(s) that the code expects to exist. + * + * And then optionally create a Liquibase database object that points to an existing database + * if one desires to create a changeset that could be applied to that database. + * + * If a Liquibase database object is not used, then the change set created would be + * something that could be applied to an empty database to make it match the state of the code. + * + * Prior to applying the changeset one should review and make adjustments appropriately. + * + * @author Kurt Niemi + * @since 3.2 + */ public class LiquibaseChangeSetGenerator { private final SchemaModel sourceModel; private final Database targetDatabase; + /** + * Use this to generate a ChangeSet that can be used on an empty database + * + * @author Kurt Niemi + * @since 3.2 + * + * @param sourceModel - Model representing table(s)/column(s) as existing in code + */ + public LiquibaseChangeSetGenerator(SchemaModel sourceModel) { + + this.sourceModel = sourceModel; + } + + /** + * Use this to generate a ChangeSet against an existing database + * + * @author Kurt Niemi + * @since 3.2 + * + * @param sourceModel - Model representing table(s)/column(s) as existing in code + * @param targetDatabase - Existing Liquibase database + */ public LiquibaseChangeSetGenerator(SchemaModel sourceModel, Database targetDatabase) { this.sourceModel = sourceModel; this.targetDatabase = targetDatabase; } + /** + * Generates a Liquibase Changeset + * + * @author Kurt Niemi + * @since 3.2 + * + * @param changeLogFilePath - File that changeset will be written to (or append to an existing ChangeSet file) + * @throws InvalidExampleException + * @throws DatabaseException + * @throws IOException + * @throws ChangeLogParseException + */ public void generateLiquibaseChangeset(String changeLogFilePath) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { String changeSetId = Long.toString(System.currentTimeMillis()); generateLiquibaseChangeset(changeLogFilePath, changeSetId, "Spring Data JDBC"); } + /** + * Generates a Liquibase Changeset + * + * @author Kurt Niemi + * @since 3.2 + * + * @param changeLogFilePath - File that changeset will be written to (or append to an existing ChangeSet file) + * @param changeSetId - A unique value to identify the changeset + * @param changeSetAuthor - Author information to be written to changeset file. + * @throws InvalidExampleException + * @throws DatabaseException + * @throws IOException + * @throws ChangeLogParseException + */ public void generateLiquibaseChangeset(String changeLogFilePath, String changeSetId, String changeSetAuthor) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { - SchemaModel liquibaseModel = getLiquibaseModel(); - SchemaDiff difference = sourceModel.diffModel(liquibaseModel); + SchemaDiff difference; + + if (targetDatabase != null) { + SchemaModel liquibaseModel = getLiquibaseModel(); + difference = sourceModel.diffModel(liquibaseModel); + } else { + difference = sourceModel.diffModel(new SchemaModel()); + } DatabaseChangeLog databaseChangeLog = getDatabaseChangeLog(changeLogFilePath); @@ -148,6 +219,7 @@ private DatabaseChangeLog getDatabaseChangeLog(String changeLogFilePath) { } return databaseChangeLog; } + private void writeChangeSet(DatabaseChangeLog databaseChangeLog, ChangeSet changeSet, File changeLogFile) throws FileNotFoundException, IOException { ChangeLogSerializer serializer = new YamlChangeLogSerializer(); @@ -211,7 +283,7 @@ private AddColumnConfig createAddColumnChange(ColumnModel column) { return config; } - CreateTableChange createAddTableChange(TableModel table) { + private CreateTableChange createAddTableChange(TableModel table) { CreateTableChange change = new CreateTableChange(); change.setSchemaName(table.getSchema()); @@ -234,7 +306,7 @@ CreateTableChange createAddTableChange(TableModel table) { return change; } - DropTableChange createDropTableChange(TableModel table) { + private DropTableChange createDropTableChange(TableModel table) { DropTableChange change = new DropTableChange(); change.setSchemaName(table.getSchema()); change.setTableName(table.getName().getReference()); From c9efa82b2674a1e734c5d34a425bddbb34ad61ee Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Wed, 24 May 2023 13:41:39 -0400 Subject: [PATCH 18/32] Additional Javadoc's --- .../core/mapping/schemasqlgeneration/SchemaDiff.java | 9 +++++++++ .../core/mapping/schemasqlgeneration/TableDiff.java | 7 +++++++ .../core/mapping/schemasqlgeneration/TableModel.java | 1 + 3 files changed, 17 insertions(+) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java index 100fb1174d..9707efd41a 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java @@ -3,6 +3,15 @@ import java.util.ArrayList; import java.util.List; +/** + * This class is created to return the difference between a source and target {@link SchemaModel} + * + * The difference consists of Table Additions, Deletions, and Modified Tables (i.e. table + * exists in both source and target - but has columns to add or delete) + * + * @author Kurt Niemi + * @since 3.2 + */ public class SchemaDiff { private final List tableAdditions = new ArrayList(); private final List tableDeletions = new ArrayList(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java index 593fd5eb2c..9783b21264 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java @@ -3,6 +3,13 @@ import java.util.ArrayList; import java.util.List; +/** + * This class is used to keep track of columns that have been added or deleted, + * when performing a difference between a source and target {@link SchemaModel} + * + * @author Kurt Niemi + * @since 3.2 + */ public class TableDiff { private final TableModel tableModel; private final List addedColumns = new ArrayList(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java index 98a11f45d8..4ea465e739 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java @@ -25,6 +25,7 @@ * Class that models a Table for generating SQL for Schema generation. * * @author Kurt Niemi + * @since 3.2 */ public class TableModel { private String schema; From 9757b718f58b6b579fa6e26c5bc973619e7e349b Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Wed, 24 May 2023 14:14:09 -0400 Subject: [PATCH 19/32] Refactoring - make SchemaModel class purely a model class (and not perform any difference/comparison) --- .../LiquibaseChangeSetGenerator.java | 5 +- .../schemasqlgeneration/SchemaDiff.java | 75 ++++++++++++++++++- .../schemasqlgeneration/SchemaModel.java | 58 -------------- .../relational/core/sql/SchemaModelTests.java | 2 +- 4 files changed, 75 insertions(+), 65 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java index c26c84d739..843710da0e 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java @@ -85,6 +85,7 @@ public class LiquibaseChangeSetGenerator { public LiquibaseChangeSetGenerator(SchemaModel sourceModel) { this.sourceModel = sourceModel; + this.targetDatabase = null; } /** @@ -140,9 +141,9 @@ public void generateLiquibaseChangeset(String changeLogFilePath, String changeSe if (targetDatabase != null) { SchemaModel liquibaseModel = getLiquibaseModel(); - difference = sourceModel.diffModel(liquibaseModel); + difference = new SchemaDiff(sourceModel,liquibaseModel); } else { - difference = sourceModel.diffModel(new SchemaModel()); + difference = new SchemaDiff(sourceModel, new SchemaModel()); } DatabaseChangeLog databaseChangeLog = getDatabaseChangeLog(changeLogFilePath); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java index 9707efd41a..e50371fd62 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java @@ -1,7 +1,6 @@ package org.springframework.data.relational.core.mapping.schemasqlgeneration; -import java.util.ArrayList; -import java.util.List; +import java.util.*; /** * This class is created to return the difference between a source and target {@link SchemaModel} @@ -15,7 +14,27 @@ public class SchemaDiff { private final List tableAdditions = new ArrayList(); private final List tableDeletions = new ArrayList(); - private final List tableDiff = new ArrayList(); + private final List tableDiffs = new ArrayList(); + + private SchemaModel source; + private SchemaModel target; + + /** + * + * Compare two {@link SchemaModel} to identify differences. + * + * @param target - Model reflecting current database state + * @param source - Model reflecting desired database state + */ + public SchemaDiff(SchemaModel target, SchemaModel source) { + + this.source = source; + this.target = target; + + diffTableAdditionDeletion(); + diffTable(); + } + public List getTableAdditions() { return tableAdditions; @@ -27,6 +46,54 @@ public List getTableDeletions() { } public List getTableDiff() { - return tableDiff; + return tableDiffs; + } + + private void diffTableAdditionDeletion() { + + Set sourceTableData = new HashSet(source.getTableData()); + Set targetTableData = new HashSet(target.getTableData()); + + // Identify deleted tables + Set deletedTables = new HashSet(sourceTableData); + deletedTables.removeAll(targetTableData); + tableDeletions.addAll(deletedTables); + + // Identify added tables + Set addedTables = new HashSet(targetTableData); + addedTables.removeAll(sourceTableData); + tableAdditions.addAll(addedTables); + } + + private void diffTable() { + + HashMap sourceTablesMap = new HashMap(); + for (TableModel table : source.getTableData()) { + sourceTablesMap.put(table.getSchema() + "." + table.getName().getReference(), table); + } + + Set existingTables = new HashSet(target.getTableData()); + existingTables.removeAll(getTableAdditions()); + + for (TableModel table : existingTables) { + TableDiff tableDiff = new TableDiff(table); + tableDiffs.add(tableDiff); + + TableModel sourceTable = sourceTablesMap.get(table.getSchema() + "." + table.getName().getReference()); + + Set sourceTableData = new HashSet(sourceTable.getColumns()); + Set targetTableData = new HashSet(table.getColumns()); + + // Identify deleted columns + Set deletedColumns = new HashSet(sourceTableData); + deletedColumns.removeAll(targetTableData); + + tableDiff.getDeletedColumns().addAll(deletedColumns); + + // Identify added columns + Set addedColumns = new HashSet(targetTableData); + addedColumns.removeAll(sourceTableData); + tableDiff.getAddedColumns().addAll(addedColumns); + } } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java index 66bfe30604..7e48872b43 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java @@ -74,64 +74,6 @@ public SchemaModel(RelationalMappingContext context) { } } - void diffTableAdditionDeletion(SchemaModel source, SchemaDiff diff) { - - Set sourceTableData = new HashSet(source.getTableData()); - Set targetTableData = new HashSet(getTableData()); - - // Identify deleted tables - Set deletedTables = new HashSet(sourceTableData); - deletedTables.removeAll(targetTableData); - diff.getTableDeletions().addAll(deletedTables); - - // Identify added tables - Set addedTables = new HashSet(targetTableData); - addedTables.removeAll(sourceTableData); - diff.getTableAdditions().addAll(addedTables); - } - - void diffTable(SchemaModel source, SchemaDiff diff) { - - HashMap sourceTablesMap = new HashMap(); - for (TableModel table : source.getTableData()) { - sourceTablesMap.put(table.getSchema() + "." + table.getName().getReference(), table); - } - - Set existingTables = new HashSet(getTableData()); - existingTables.removeAll(diff.getTableAdditions()); - - for (TableModel table : existingTables) { - TableDiff tableDiff = new TableDiff(table); - diff.getTableDiff().add(tableDiff); - - TableModel sourceTable = sourceTablesMap.get(table.getSchema() + "." + table.getName().getReference()); - - Set sourceTableData = new HashSet(sourceTable.getColumns()); - Set targetTableData = new HashSet(table.getColumns()); - - // Identify deleted columns - Set deletedColumns = new HashSet(sourceTableData); - deletedColumns.removeAll(targetTableData); - - tableDiff.getDeletedColumns().addAll(deletedColumns); - - // Identify added columns - Set addedColumns = new HashSet(targetTableData); - addedColumns.removeAll(sourceTableData); - tableDiff.getAddedColumns().addAll(addedColumns); - } - } - - public SchemaDiff diffModel(SchemaModel source) { - - SchemaDiff diff = new SchemaDiff(); - - diffTableAdditionDeletion(source, diff); - diffTable(source, diff); - - return diff; - } - public List getTableData() { return tableData; } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java index e703432ef7..27a2f707c6 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java @@ -55,7 +55,7 @@ void testDiffSchema() { newTable.getColumns().add(newColumn); newModel.getTableData().add(newTable); - SchemaDiff diff = newModel.diffModel(model); + SchemaDiff diff = new SchemaDiff(model, newModel); //, model); // Verify that newtable is an added table in the diff assertThat(diff.getTableAdditions().size() > 0); From 0c01a9cebc4d438ea1dda6205ad1a19767a00c97 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Thu, 25 May 2023 13:52:55 -0400 Subject: [PATCH 20/32] Change to use Records for pure data types --- .../schemasqlgeneration/ColumnModel.java | 38 ++------------ .../LiquibaseChangeSetGenerator.java | 51 ++++++++++--------- .../schemasqlgeneration/SchemaDiff.java | 12 ++--- .../schemasqlgeneration/SchemaModel.java | 2 +- .../schemasqlgeneration/TableDiff.java | 26 ++-------- .../schemasqlgeneration/TableModel.java | 40 ++------------- .../relational/core/sql/SchemaModelTests.java | 12 ++--- 7 files changed, 53 insertions(+), 128 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java index fbd0d10981..f904a9aad4 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java @@ -21,45 +21,15 @@ /** - * Class that models a Column for generating SQL for Schema generation. + * Models a Column for generating SQL for Schema generation. * * @author Kurt Niemi * @since 3.2 */ -public class ColumnModel { - private final SqlIdentifier name; - private final String type; - private final boolean nullable; - private final boolean identityColumn; +public record ColumnModel(SqlIdentifier name, String type, boolean nullable, boolean identityColumn) { - public ColumnModel(SqlIdentifier name, String type, boolean nullable, boolean identityColumn) { - this.name = name; - this.type = type; - this.nullable = nullable; - this.identityColumn = identityColumn; - } - - public ColumnModel(SqlIdentifier name, String type) { - this.name = name; - this.type = type; - this.nullable = false; - this.identityColumn = false; - } - - public SqlIdentifier getName() { - return name; - } - - public String getType() { - return type; - } - - public boolean isNullable() { - return nullable; - } - - public boolean isIdentityColumn() { - return identityColumn; + public ColumnModel(SqlIdentifier name, String type) { + this(name, type, false, false); } @Override diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java index 843710da0e..c6c95bf03a 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java @@ -175,12 +175,12 @@ private void generateTableModifications(ChangeSet changeSet, SchemaDiff differen for (TableDiff table : difference.getTableDiff()) { - if (table.getAddedColumns().size() > 0) { + if (table.addedColumns().size() > 0) { AddColumnChange addColumnChange = new AddColumnChange(); - addColumnChange.setSchemaName(table.getTableModel().getSchema()); - addColumnChange.setTableName(table.getTableModel().getName().getReference()); + addColumnChange.setSchemaName(table.tableModel().schema()); + addColumnChange.setTableName(table.tableModel().name().getReference()); - for (ColumnModel column : table.getAddedColumns()) { + for (ColumnModel column : table.addedColumns()) { AddColumnConfig addColumn = createAddColumnChange(column); addColumnChange.addColumn(addColumn); } @@ -188,15 +188,15 @@ private void generateTableModifications(ChangeSet changeSet, SchemaDiff differen changeSet.addChange(addColumnChange); } - if (table.getDeletedColumns().size() > 0) { + if (table.deletedColumns().size() > 0) { DropColumnChange dropColumnChange = new DropColumnChange(); - dropColumnChange.setSchemaName(table.getTableModel().getSchema()); - dropColumnChange.setTableName(table.getTableModel().getName().getReference()); + dropColumnChange.setSchemaName(table.tableModel().schema()); + dropColumnChange.setTableName(table.tableModel().name().getReference()); List dropColumns = new ArrayList(); - for (ColumnModel column : table.getDeletedColumns()) { + for (ColumnModel column : table.deletedColumns()) { ColumnConfig config = new ColumnConfig(); - config.setName(column.getName().getReference()); + config.setName(column.name().getReference()); dropColumns.add(config); } dropColumnChange.setColumns(dropColumns); @@ -242,9 +242,12 @@ private SchemaModel getLiquibaseModel() throws DatabaseException, InvalidExample DatabaseSnapshot snapshot = SnapshotGeneratorFactory.getInstance().createSnapshot(schemas, targetDatabase, snapshotControl); Set
tables = snapshot.get(liquibase.structure.core.Table.class); - for (TableModel t : sourceModel.getTableData()) { - if (t.getSchema() == null || t.getSchema().isEmpty()) { - t.setSchema(targetDatabase.getDefaultSchema().getCatalogName()); + for (int i=0; i < sourceModel.getTableData().size(); i++) { + TableModel currentModel = sourceModel.getTableData().get(i); + if (currentModel.schema() == null || currentModel.schema().isEmpty()) { + TableModel newModel = new TableModel(targetDatabase.getDefaultSchema().getCatalogName(), + currentModel.name(), currentModel.columns(), currentModel.keyColumns()); + sourceModel.getTableData().set(i, newModel); } } @@ -265,7 +268,7 @@ private SchemaModel getLiquibaseModel() throws DatabaseException, InvalidExample String type = column.getType().toString(); boolean nullable = column.isNullable(); ColumnModel columnModel = new ColumnModel(columnName, type, nullable, false); - tableModel.getColumns().add(columnModel); + tableModel.columns().add(columnModel); } } @@ -275,10 +278,10 @@ private SchemaModel getLiquibaseModel() throws DatabaseException, InvalidExample private AddColumnConfig createAddColumnChange(ColumnModel column) { AddColumnConfig config = new AddColumnConfig(); - config.setName(column.getName().getReference()); - config.setType(column.getType()); + config.setName(column.name().getReference()); + config.setType(column.type()); - if (column.isIdentityColumn()) { + if (column.identityColumn()) { config.setAutoIncrement(true); } return config; @@ -287,15 +290,15 @@ private AddColumnConfig createAddColumnChange(ColumnModel column) { private CreateTableChange createAddTableChange(TableModel table) { CreateTableChange change = new CreateTableChange(); - change.setSchemaName(table.getSchema()); - change.setTableName(table.getName().getReference()); + change.setSchemaName(table.schema()); + change.setTableName(table.name().getReference()); - for (ColumnModel column : table.getColumns()) { + for (ColumnModel column : table.columns()) { ColumnConfig columnConfig = new ColumnConfig(); - columnConfig.setName(column.getName().getReference()); - columnConfig.setType(column.getType()); + columnConfig.setName(column.name().getReference()); + columnConfig.setType(column.type()); - if (column.isIdentityColumn()) { + if (column.identityColumn()) { columnConfig.setAutoIncrement(true); ConstraintsConfig constraints = new ConstraintsConfig(); constraints.setPrimaryKey(true); @@ -309,8 +312,8 @@ private CreateTableChange createAddTableChange(TableModel table) { private DropTableChange createDropTableChange(TableModel table) { DropTableChange change = new DropTableChange(); - change.setSchemaName(table.getSchema()); - change.setTableName(table.getName().getReference()); + change.setSchemaName(table.schema()); + change.setTableName(table.name().getReference()); change.setCascadeConstraints(true); return change; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java index e50371fd62..ed6c41c431 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java @@ -69,7 +69,7 @@ private void diffTable() { HashMap sourceTablesMap = new HashMap(); for (TableModel table : source.getTableData()) { - sourceTablesMap.put(table.getSchema() + "." + table.getName().getReference(), table); + sourceTablesMap.put(table.schema() + "." + table.name().getReference(), table); } Set existingTables = new HashSet(target.getTableData()); @@ -79,21 +79,21 @@ private void diffTable() { TableDiff tableDiff = new TableDiff(table); tableDiffs.add(tableDiff); - TableModel sourceTable = sourceTablesMap.get(table.getSchema() + "." + table.getName().getReference()); + TableModel sourceTable = sourceTablesMap.get(table.schema() + "." + table.name().getReference()); - Set sourceTableData = new HashSet(sourceTable.getColumns()); - Set targetTableData = new HashSet(table.getColumns()); + Set sourceTableData = new HashSet(sourceTable.columns()); + Set targetTableData = new HashSet(table.columns()); // Identify deleted columns Set deletedColumns = new HashSet(sourceTableData); deletedColumns.removeAll(targetTableData); - tableDiff.getDeletedColumns().addAll(deletedColumns); + tableDiff.deletedColumns().addAll(deletedColumns); // Identify added columns Set addedColumns = new HashSet(targetTableData); addedColumns.removeAll(sourceTableData); - tableDiff.getAddedColumns().addAll(addedColumns); + tableDiff.addedColumns().addAll(addedColumns); } } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java index 7e48872b43..02447faee4 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java @@ -67,7 +67,7 @@ public SchemaModel(RelationalMappingContext context) { ColumnModel columnModel = new ColumnModel(p.getColumnName(), databaseTypeMapping.databaseTypeFromClass(p.getActualType()), true, setIdentifierColumns.contains(p)); - tableModel.getColumns().add(columnModel); + tableModel.columns().add(columnModel); } tableData.add(tableModel); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java index 9783b21264..137a5bdf94 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java @@ -4,34 +4,18 @@ import java.util.List; /** - * This class is used to keep track of columns that have been added or deleted, + * Used to keep track of columns that have been added or deleted, * when performing a difference between a source and target {@link SchemaModel} * * @author Kurt Niemi * @since 3.2 */ -public class TableDiff { - private final TableModel tableModel; - private final List addedColumns = new ArrayList(); - private final List deletedColumns = new ArrayList(); +public record TableDiff(TableModel tableModel, + ArrayList addedColumns, + ArrayList deletedColumns) { public TableDiff(TableModel tableModel) { - - this.tableModel = tableModel; + this(tableModel, new ArrayList<>(), new ArrayList<>()); } - public TableModel getTableModel() { - - return tableModel; - } - - public List getAddedColumns() { - - return addedColumns; - } - - public List getDeletedColumns() { - - return deletedColumns; - } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java index 4ea465e739..fdcfa01c21 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java @@ -22,52 +22,20 @@ import java.util.Objects; /** - * Class that models a Table for generating SQL for Schema generation. + * Models a Table for generating SQL for Schema generation. * * @author Kurt Niemi * @since 3.2 */ -public class TableModel { - private String schema; - private SqlIdentifier name; - private final List columns = new ArrayList(); - private final List keyColumns = new ArrayList(); - +public record TableModel(String schema, SqlIdentifier name, List columns, List keyColumns) { public TableModel(String schema, SqlIdentifier name) { - - this.schema = schema; - this.name = name; + this(schema, name, new ArrayList<>(), new ArrayList<>()); } - public TableModel(SqlIdentifier name) { + public TableModel(SqlIdentifier name) { this(null, name); } - public String getSchema() { - - return schema; - } - - public void setSchema(String schema) { - - this.schema = schema; - } - - public SqlIdentifier getName() { - - return name; - } - - public void setName(SqlIdentifier name) { - - this.name = name; - } - - public List getColumns() { - - return columns; - } - @Override public boolean equals(Object o) { diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java index 27a2f707c6..f1141502e7 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java @@ -44,7 +44,7 @@ void testDiffSchema() { // Add column to table SqlIdentifier newIdentifier = new DefaultSqlIdentifier("newcol", false); ColumnModel newColumn = new ColumnModel(newIdentifier, "VARCHAR(255)"); - newModel.getTableData().get(0).getColumns().add(newColumn); + newModel.getTableData().get(0).columns().add(newColumn); // Remove table newModel.getTableData().remove(1); @@ -52,21 +52,21 @@ void testDiffSchema() { // Add new table SqlIdentifier tableIdenfifier = new DefaultSqlIdentifier("newtable", false); TableModel newTable = new TableModel(null, tableIdenfifier); - newTable.getColumns().add(newColumn); + newTable.columns().add(newColumn); newModel.getTableData().add(newTable); SchemaDiff diff = new SchemaDiff(model, newModel); //, model); // Verify that newtable is an added table in the diff assertThat(diff.getTableAdditions().size() > 0); - assertThat(diff.getTableAdditions().get(0).getName().getReference().equals("newtable")); + assertThat(diff.getTableAdditions().get(0).name().getReference().equals("newtable")); assertThat(diff.getTableDeletions().size() > 0); - assertThat(diff.getTableDeletions().get(0).getName().getReference().equals("vader")); + assertThat(diff.getTableDeletions().get(0).name().getReference().equals("vader")); assertThat(diff.getTableDiff().size() > 0); - assertThat(diff.getTableDiff().get(0).getAddedColumns().size() > 0); - assertThat(diff.getTableDiff().get(0).getDeletedColumns().size() > 0); + assertThat(diff.getTableDiff().get(0).addedColumns().size() > 0); + assertThat(diff.getTableDiff().get(0).deletedColumns().size() > 0); } // Test table classes for performing schema diff From e3e123d5eee23dc9fd95cfafef7f5708074582fa Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Thu, 25 May 2023 14:10:48 -0400 Subject: [PATCH 21/32] Use predicate to define all Liquibase specific tables --- .../schemasqlgeneration/LiquibaseChangeSetGenerator.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java index c6c95bf03a..4beb4cdc89 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java @@ -51,6 +51,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.function.Predicate; /** * Use this class to generate Liquibase change sets. @@ -74,6 +75,12 @@ public class LiquibaseChangeSetGenerator { private final SchemaModel sourceModel; private final Database targetDatabase; + /** + * If there should ever be future Liquibase tables that should not be deleted (removed), this + * predicate should be modified + */ + private final Predicate liquibaseTables = table -> ( table.startsWith("DATABASECHANGELOG") ); + /** * Use this to generate a ChangeSet that can be used on an empty database * @@ -254,7 +261,7 @@ private SchemaModel getLiquibaseModel() throws DatabaseException, InvalidExample for (liquibase.structure.core.Table table : tables) { // Exclude internal Liquibase tables from comparison - if (table.getName().startsWith("DATABASECHANGELOG")) { + if (liquibaseTables.test(table.getName())) { continue; } From cf21d299a517bcb4bdc520e5d57ac49b1eecd5e6 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Thu, 25 May 2023 14:37:28 -0400 Subject: [PATCH 22/32] Add predicate with default implementation to not drop any external tables / columns (i.e. only perform additions - unless the user implements a predicate telling us otherwise) --- .../LiquibaseChangeSetGenerator.java | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java index 4beb4cdc89..7ed3f69965 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java @@ -81,6 +81,17 @@ public class LiquibaseChangeSetGenerator { */ private final Predicate liquibaseTables = table -> ( table.startsWith("DATABASECHANGELOG") ); + /** + * By default existing tables in the target database are never deleted + */ + public Predicate userApplicationTables = table -> ( true ); + + /** + * By default existing columns in the target database are never deleted. + * Columns will be passed into the predicate in the format TableName.ColumnName + */ + public Predicate userApplicationTableColumns = table -> ( true ); + /** * Use this to generate a ChangeSet that can be used on an empty database * @@ -173,8 +184,11 @@ private void generateTableAdditionsDeletions(ChangeSet changeSet, SchemaDiff dif } for (TableModel table : difference.getTableDeletions()) { - DropTableChange dropTable = createDropTableChange(table); - changeSet.addChange(dropTable); + // Do not delete/drop table if it is an external application table + if (!userApplicationTables.test(table.name().getReference())) { + DropTableChange dropTable = createDropTableChange(table); + changeSet.addChange(dropTable); + } } } @@ -195,7 +209,16 @@ private void generateTableModifications(ChangeSet changeSet, SchemaDiff differen changeSet.addChange(addColumnChange); } - if (table.deletedColumns().size() > 0) { + ArrayList deletedColumns = new ArrayList<>(); + for (ColumnModel columnModel : table.deletedColumns()) { + String fullName = table.tableModel().name().getReference() + "." + columnModel.name().getReference(); + + if (!userApplicationTableColumns.test(fullName)) { + deletedColumns.add(columnModel); + } + } + + if (deletedColumns.size() > 0) { DropColumnChange dropColumnChange = new DropColumnChange(); dropColumnChange.setSchemaName(table.tableModel().schema()); dropColumnChange.setTableName(table.tableModel().name().getReference()); From c9841c5a32fde4a13c4cd857b0123c1870e3cfdc Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Fri, 26 May 2023 08:51:09 -0400 Subject: [PATCH 23/32] Remove usage of DerivedSqlIdentifier as what we get from Liquibase when querying DB state is the Reference Name. --- .../core/mapping/DerivedSqlIdentifier.java | 4 ++-- .../schemasqlgeneration/ColumnModel.java | 4 ++-- .../LiquibaseChangeSetGenerator.java | 24 +++++++++---------- .../schemasqlgeneration/SchemaDiff.java | 4 ++-- .../schemasqlgeneration/TableModel.java | 10 ++++---- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java index 2640357207..d2fda3403c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java @@ -32,12 +32,12 @@ * @author Kurt Niemi * @since 2.0 */ -public class DerivedSqlIdentifier implements SqlIdentifier { +class DerivedSqlIdentifier implements SqlIdentifier { private final String name; private final boolean quoted; - public DerivedSqlIdentifier(String name, boolean quoted) { + DerivedSqlIdentifier(String name, boolean quoted) { Assert.hasText(name, "A database object must have at least on name part."); this.name = name; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java index f904a9aad4..6a6c1c128f 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java @@ -26,9 +26,9 @@ * @author Kurt Niemi * @since 3.2 */ -public record ColumnModel(SqlIdentifier name, String type, boolean nullable, boolean identityColumn) { +public record ColumnModel(String name, String type, boolean nullable, boolean identityColumn) { - public ColumnModel(SqlIdentifier name, String type) { + public ColumnModel(String name, String type) { this(name, type, false, false); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java index 7ed3f69965..7867799cd9 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java @@ -185,7 +185,7 @@ private void generateTableAdditionsDeletions(ChangeSet changeSet, SchemaDiff dif for (TableModel table : difference.getTableDeletions()) { // Do not delete/drop table if it is an external application table - if (!userApplicationTables.test(table.name().getReference())) { + if (!userApplicationTables.test(table.name())) { DropTableChange dropTable = createDropTableChange(table); changeSet.addChange(dropTable); } @@ -199,7 +199,7 @@ private void generateTableModifications(ChangeSet changeSet, SchemaDiff differen if (table.addedColumns().size() > 0) { AddColumnChange addColumnChange = new AddColumnChange(); addColumnChange.setSchemaName(table.tableModel().schema()); - addColumnChange.setTableName(table.tableModel().name().getReference()); + addColumnChange.setTableName(table.tableModel().name()); for (ColumnModel column : table.addedColumns()) { AddColumnConfig addColumn = createAddColumnChange(column); @@ -211,7 +211,7 @@ private void generateTableModifications(ChangeSet changeSet, SchemaDiff differen ArrayList deletedColumns = new ArrayList<>(); for (ColumnModel columnModel : table.deletedColumns()) { - String fullName = table.tableModel().name().getReference() + "." + columnModel.name().getReference(); + String fullName = table.tableModel().name() + "." + columnModel.name(); if (!userApplicationTableColumns.test(fullName)) { deletedColumns.add(columnModel); @@ -221,12 +221,12 @@ private void generateTableModifications(ChangeSet changeSet, SchemaDiff differen if (deletedColumns.size() > 0) { DropColumnChange dropColumnChange = new DropColumnChange(); dropColumnChange.setSchemaName(table.tableModel().schema()); - dropColumnChange.setTableName(table.tableModel().name().getReference()); + dropColumnChange.setTableName(table.tableModel().name()); List dropColumns = new ArrayList(); for (ColumnModel column : table.deletedColumns()) { ColumnConfig config = new ColumnConfig(); - config.setName(column.name().getReference()); + config.setName(column.name()); dropColumns.add(config); } dropColumnChange.setColumns(dropColumns); @@ -288,16 +288,14 @@ private SchemaModel getLiquibaseModel() throws DatabaseException, InvalidExample continue; } - SqlIdentifier tableName = new DerivedSqlIdentifier(table.getName(), true); - TableModel tableModel = new TableModel(table.getSchema().getCatalogName(), tableName); + TableModel tableModel = new TableModel(table.getSchema().getCatalogName(), table.getName()); liquibaseModel.getTableData().add(tableModel); List columns = table.getColumns(); for (liquibase.structure.core.Column column : columns) { - SqlIdentifier columnName = new DerivedSqlIdentifier(column.getName(), true); String type = column.getType().toString(); boolean nullable = column.isNullable(); - ColumnModel columnModel = new ColumnModel(columnName, type, nullable, false); + ColumnModel columnModel = new ColumnModel(column.getName(), type, nullable, false); tableModel.columns().add(columnModel); } } @@ -308,7 +306,7 @@ private SchemaModel getLiquibaseModel() throws DatabaseException, InvalidExample private AddColumnConfig createAddColumnChange(ColumnModel column) { AddColumnConfig config = new AddColumnConfig(); - config.setName(column.name().getReference()); + config.setName(column.name()); config.setType(column.type()); if (column.identityColumn()) { @@ -321,11 +319,11 @@ private CreateTableChange createAddTableChange(TableModel table) { CreateTableChange change = new CreateTableChange(); change.setSchemaName(table.schema()); - change.setTableName(table.name().getReference()); + change.setTableName(table.name()); for (ColumnModel column : table.columns()) { ColumnConfig columnConfig = new ColumnConfig(); - columnConfig.setName(column.name().getReference()); + columnConfig.setName(column.name()); columnConfig.setType(column.type()); if (column.identityColumn()) { @@ -343,7 +341,7 @@ private CreateTableChange createAddTableChange(TableModel table) { private DropTableChange createDropTableChange(TableModel table) { DropTableChange change = new DropTableChange(); change.setSchemaName(table.schema()); - change.setTableName(table.name().getReference()); + change.setTableName(table.name()); change.setCascadeConstraints(true); return change; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java index ed6c41c431..3a4668335b 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java @@ -69,7 +69,7 @@ private void diffTable() { HashMap sourceTablesMap = new HashMap(); for (TableModel table : source.getTableData()) { - sourceTablesMap.put(table.schema() + "." + table.name().getReference(), table); + sourceTablesMap.put(table.schema() + "." + table.name(), table); } Set existingTables = new HashSet(target.getTableData()); @@ -79,7 +79,7 @@ private void diffTable() { TableDiff tableDiff = new TableDiff(table); tableDiffs.add(tableDiff); - TableModel sourceTable = sourceTablesMap.get(table.schema() + "." + table.name().getReference()); + TableModel sourceTable = sourceTablesMap.get(table.schema() + "." + table.name()); Set sourceTableData = new HashSet(sourceTable.columns()); Set targetTableData = new HashSet(table.columns()); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java index fdcfa01c21..795fbb8b23 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java @@ -27,12 +27,12 @@ * @author Kurt Niemi * @since 3.2 */ -public record TableModel(String schema, SqlIdentifier name, List columns, List keyColumns) { - public TableModel(String schema, SqlIdentifier name) { +public record TableModel(String schema, String name, List columns, List keyColumns) { + public TableModel(String schema, String name) { this(schema, name, new ArrayList<>(), new ArrayList<>()); } - public TableModel(SqlIdentifier name) { + public TableModel(String name) { this(null, name); } @@ -55,7 +55,7 @@ public boolean equals(Object o) { return false; } } - if (!name.getReference().toUpperCase().equals(that.name.getReference().toUpperCase())) { + if (!name.toUpperCase().equals(that.name.toUpperCase())) { return false; } return true; @@ -64,6 +64,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(name.getReference().toUpperCase()); + return Objects.hash(name.toUpperCase()); } } From 1693d24c79c45edb2801763f4b334d9fa07b5183 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Fri, 26 May 2023 08:51:44 -0400 Subject: [PATCH 24/32] Fix unit tests --- .../data/relational/core/sql/SchemaModelTests.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java index f1141502e7..fed396222d 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java @@ -42,27 +42,25 @@ void testDiffSchema() { SchemaModel newModel = new SchemaModel(context); // Add column to table - SqlIdentifier newIdentifier = new DefaultSqlIdentifier("newcol", false); - ColumnModel newColumn = new ColumnModel(newIdentifier, "VARCHAR(255)"); + ColumnModel newColumn = new ColumnModel("newcol", "VARCHAR(255)"); newModel.getTableData().get(0).columns().add(newColumn); // Remove table newModel.getTableData().remove(1); // Add new table - SqlIdentifier tableIdenfifier = new DefaultSqlIdentifier("newtable", false); - TableModel newTable = new TableModel(null, tableIdenfifier); + TableModel newTable = new TableModel(null, "newtable"); newTable.columns().add(newColumn); newModel.getTableData().add(newTable); - SchemaDiff diff = new SchemaDiff(model, newModel); //, model); + SchemaDiff diff = new SchemaDiff(model, newModel); // Verify that newtable is an added table in the diff assertThat(diff.getTableAdditions().size() > 0); - assertThat(diff.getTableAdditions().get(0).name().getReference().equals("newtable")); + assertThat(diff.getTableAdditions().get(0).name().equals("table1")); assertThat(diff.getTableDeletions().size() > 0); - assertThat(diff.getTableDeletions().get(0).name().getReference().equals("vader")); + assertThat(diff.getTableDeletions().get(0).name().equals("vader")); assertThat(diff.getTableDiff().size() > 0); assertThat(diff.getTableDiff().get(0).addedColumns().size() > 0); From 212d07a2d018dd4aae7d2106cf9675b0e3749411 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Fri, 26 May 2023 10:57:52 -0400 Subject: [PATCH 25/32] Changes for not requiring @Table annotation --- .../schemasqlgeneration/SchemaModel.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java index 02447faee4..209fc2d763 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java @@ -16,6 +16,7 @@ package org.springframework.data.relational.core.mapping.schemasqlgeneration; import org.springframework.data.annotation.Id; +import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.relational.core.mapping.*; import java.util.*; @@ -49,7 +50,7 @@ public SchemaModel(RelationalMappingContext context) { } for (RelationalPersistentEntity entity : context.getPersistentEntities()) { - TableModel tableModel = new TableModel(entity.getTableName()); + TableModel tableModel = new TableModel(entity.getTableName().getReference()); Iterator iter = entity.getPersistentProperties(Id.class).iterator(); @@ -59,16 +60,18 @@ public SchemaModel(RelationalMappingContext context) { setIdentifierColumns.add(p); } - iter = - entity.getPersistentProperties(Column.class).iterator(); + entity.doWithProperties((PropertyHandler) handler -> { + BasicRelationalPersistentProperty property = (BasicRelationalPersistentProperty)handler; - while (iter.hasNext()) { - BasicRelationalPersistentProperty p = iter.next(); - ColumnModel columnModel = new ColumnModel(p.getColumnName(), - databaseTypeMapping.databaseTypeFromClass(p.getActualType()), - true, setIdentifierColumns.contains(p)); + if (property.isEntity() && !property.isEmbedded()) { + return; + } + + ColumnModel columnModel = new ColumnModel(property.getColumnName().getReference(), + databaseTypeMapping.databaseTypeFromClass(property.getActualType()), + true, setIdentifierColumns.contains(property)); tableModel.columns().add(columnModel); - } + }); tableData.add(tableModel); } From 9749dcf4f3e9fbb14b5ed9d4e78ee18e2e8f5f02 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Fri, 26 May 2023 12:32:05 -0400 Subject: [PATCH 26/32] For Liquibase changeset generation - Use Spring Resource instead of absolute file name --- .../LiquibaseChangeSetGenerator.java | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java index 7867799cd9..783f8057ad 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java @@ -41,6 +41,7 @@ import liquibase.snapshot.SnapshotGeneratorFactory; import liquibase.structure.core.Column; import liquibase.structure.core.Table; +import org.springframework.core.io.Resource; import org.springframework.data.relational.core.mapping.DerivedSqlIdentifier; import org.springframework.data.relational.core.sql.SqlIdentifier; @@ -127,16 +128,16 @@ public LiquibaseChangeSetGenerator(SchemaModel sourceModel, Database targetDatab * @author Kurt Niemi * @since 3.2 * - * @param changeLogFilePath - File that changeset will be written to (or append to an existing ChangeSet file) + * @param changeLogResource - Resource that changeset will be written to (or append to an existing ChangeSet file) * @throws InvalidExampleException * @throws DatabaseException * @throws IOException * @throws ChangeLogParseException */ - public void generateLiquibaseChangeset(String changeLogFilePath) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { + public void generateLiquibaseChangeset(Resource changeLogResource) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { String changeSetId = Long.toString(System.currentTimeMillis()); - generateLiquibaseChangeset(changeLogFilePath, changeSetId, "Spring Data JDBC"); + generateLiquibaseChangeset(changeLogResource, changeSetId, "Spring Data JDBC"); } /** @@ -145,7 +146,7 @@ public void generateLiquibaseChangeset(String changeLogFilePath) throws InvalidE * @author Kurt Niemi * @since 3.2 * - * @param changeLogFilePath - File that changeset will be written to (or append to an existing ChangeSet file) + * @param changeLogResource - Resource that changeset will be written to (or append to an existing ChangeSet file) * @param changeSetId - A unique value to identify the changeset * @param changeSetAuthor - Author information to be written to changeset file. * @throws InvalidExampleException @@ -153,7 +154,7 @@ public void generateLiquibaseChangeset(String changeLogFilePath) throws InvalidE * @throws IOException * @throws ChangeLogParseException */ - public void generateLiquibaseChangeset(String changeLogFilePath, String changeSetId, String changeSetAuthor) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { + public void generateLiquibaseChangeset(Resource changeLogResource, String changeSetId, String changeSetAuthor) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { SchemaDiff difference; @@ -164,7 +165,7 @@ public void generateLiquibaseChangeset(String changeLogFilePath, String changeSe difference = new SchemaDiff(sourceModel, new SchemaModel()); } - DatabaseChangeLog databaseChangeLog = getDatabaseChangeLog(changeLogFilePath); + DatabaseChangeLog databaseChangeLog = getDatabaseChangeLog(changeLogResource.getFile()); ChangeSet changeSet = new ChangeSet(changeSetId, changeSetAuthor, false, false, "", "", "" , databaseChangeLog); @@ -172,8 +173,8 @@ public void generateLiquibaseChangeset(String changeLogFilePath, String changeSe generateTableModifications(changeSet, difference); - File changeLogFile = new File(changeLogFilePath); - writeChangeSet(databaseChangeLog, changeSet, changeLogFile); +// File changeLogFile = new File(changeLogFilePath); + writeChangeSet(databaseChangeLog, changeSet, changeLogResource.getFile()); } private void generateTableAdditionsDeletions(ChangeSet changeSet, SchemaDiff difference) { @@ -235,18 +236,21 @@ private void generateTableModifications(ChangeSet changeSet, SchemaDiff differen } } - private DatabaseChangeLog getDatabaseChangeLog(String changeLogFilePath) { + private DatabaseChangeLog getDatabaseChangeLog(File changeLogFile) { - File changeLogFile = new File(changeLogFilePath); DatabaseChangeLog databaseChangeLog = null; try { YamlChangeLogParser parser = new YamlChangeLogParser(); - DirectoryResourceAccessor resourceAccessor = new DirectoryResourceAccessor(changeLogFile.getParentFile()); + File parentDirectory = changeLogFile.getParentFile(); + if (parentDirectory == null) { + parentDirectory = new File("./"); + } + DirectoryResourceAccessor resourceAccessor = new DirectoryResourceAccessor(parentDirectory); ChangeLogParameters parameters = new ChangeLogParameters(); - databaseChangeLog = parser.parse(changeLogFilePath, parameters, resourceAccessor); + databaseChangeLog = parser.parse(changeLogFile.getName(), parameters, resourceAccessor); } catch (Exception ex) { - databaseChangeLog = new DatabaseChangeLog(changeLogFilePath); + databaseChangeLog = new DatabaseChangeLog(changeLogFile.getAbsolutePath()); } return databaseChangeLog; } From 0ea5833d1c76d5e210886175060ea09af06b671b Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Fri, 26 May 2023 12:37:38 -0400 Subject: [PATCH 27/32] Ensure all author(s) are listed in parent pom - and remove from other ones --- pom.xml | 11 ++++++++ spring-data-jdbc/pom.xml | 58 --------------------------------------- spring-data-r2dbc/pom.xml | 25 ----------------- 3 files changed, 11 insertions(+), 83 deletions(-) diff --git a/pom.xml b/pom.xml index 06e5b65ff4..139e76d265 100644 --- a/pom.xml +++ b/pom.xml @@ -92,6 +92,17 @@ -6 + + ogierke + Oliver Gierke + ogierke(at)pivotal.io + Pivotal Software, Inc. + https://pivotal.io + + Project Contributor + + +1 + kurtn718 Kurt Niemi diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index 651f66faf3..dc889ffe52 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -25,64 +25,6 @@ 2017 - - - schauder - Jens Schauder - jschauder(at)pivotal.io - Pivotal Software, Inc. - https://pivotal.io - - Project Lead - - +1 - - - gregturn - Greg L. Turnquist - gturnquist(at)pivotal.io - Pivotal Software, Inc. - https://pivotal.io - - Project Contributor - - -6 - - - ogierke - Oliver Gierke - ogierke(at)pivotal.io - Pivotal Software, Inc. - https://pivotal.io - - Project Contributor - - +1 - - - mpaluch - Mark Paluch - mpaluch(at)pivotal.io - Pivotal Software, Inc. - https://pivotal.io - - Project Contributor - - +1 - - - kurtn718 - Kurt Niemi - kniemi(at)vmware.com - VMware - https://vmware.com - - Project Contributor - - -5 - - - diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index 92f55a39dd..9e4104f54f 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -39,31 +39,6 @@ 2018 - - - mpaluch - Mark Paluch - mpaluch(at)pivotal.io - Pivotal Software, Inc. - https://pivotal.io - - Project Lead - - +1 - - - ogierke - Oliver Gierke - ogierke(at)pivotal.io - Pivotal Software, Inc. - https://pivotal.io - - Project Lead - - +1 - - - From 187e55d45b9eb15c7ac19a688ce25c269f712c78 Mon Sep 17 00:00:00 2001 From: Kurt Niemi Date: Wed, 31 May 2023 11:26:48 -0400 Subject: [PATCH 28/32] Cleanup - remove un-needed imports (fix build errors) --- .../schemasqlgeneration/LiquibaseChangeSetGenerator.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java index 783f8057ad..e496985492 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java @@ -42,8 +42,6 @@ import liquibase.structure.core.Column; import liquibase.structure.core.Table; import org.springframework.core.io.Resource; -import org.springframework.data.relational.core.mapping.DerivedSqlIdentifier; -import org.springframework.data.relational.core.sql.SqlIdentifier; import java.io.File; import java.io.FileNotFoundException; From d8b105dd1b80b1b240d5446cca918430d8598dac Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 1 Jun 2023 11:45:13 +0200 Subject: [PATCH 29/32] Polishing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reformat code, switch to tabs. Accept property in DatabaseTypeMapping to provide more context to the type mapping component. Rename LiquibaseChangeSetGenerator to …Writer as we're writing a changeset and computing the contents is a consequence of writing a changeset. Refine naming to express what we're actually doing. Introduce setters for enhanced configuration of predicates. Reduce visibility of types to avoid unwanted public API where public access is not needed. Remove usused code, move methods around for improved grouping of code. --- spring-data-jdbc/pom.xml | 1 - spring-data-relational/pom.xml | 1 - .../mapping/RelationalMappingContext.java | 2 +- .../schemasqlgeneration/ColumnModel.java | 11 +- .../DefaultDatabaseTypeMapping.java | 47 -- .../DefaultSqlTypeMapping.java | 47 ++ .../LiquibaseChangeSetGenerator.java | 351 --------------- .../LiquibaseChangeSetWriter.java | 423 ++++++++++++++++++ .../schemasqlgeneration/MappedTables.java | 83 ++++ .../schemasqlgeneration/SchemaDiff.java | 140 +++--- .../schemasqlgeneration/SchemaModel.java | 83 ---- ...seTypeMapping.java => SqlTypeMapping.java} | 20 +- .../schemasqlgeneration/TableDiff.java | 14 +- .../schemasqlgeneration/TableModel.java | 64 ++- .../MappedTablesUnitTests.java | 85 ++++ .../relational/core/sql/SchemaModelTests.java | 98 ---- 16 files changed, 764 insertions(+), 706 deletions(-) delete mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DefaultDatabaseTypeMapping.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DefaultSqlTypeMapping.java delete mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetWriter.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTables.java delete mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java rename spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/{DatabaseTypeMapping.java => SqlTypeMapping.java} (60%) create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTablesUnitTests.java delete mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index dc889ffe52..e62a76fb90 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -226,7 +226,6 @@ org.liquibase liquibase-core ${liquibase.version} - compile true diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index d23880e4a0..6c90278ca8 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -54,7 +54,6 @@ org.liquibase liquibase-core ${liquibase.version} - compile true diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java index 779a70ddb5..c6712a4c9a 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java @@ -101,5 +101,5 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert public NamingStrategy getNamingStrategy() { return this.namingStrategy; } - + } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java index 6a6c1c128f..5eda4d9d9f 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java @@ -15,22 +15,19 @@ */ package org.springframework.data.relational.core.mapping.schemasqlgeneration; -import org.springframework.data.relational.core.sql.SqlIdentifier; - import java.util.Objects; - /** * Models a Column for generating SQL for Schema generation. * * @author Kurt Niemi * @since 3.2 */ -public record ColumnModel(String name, String type, boolean nullable, boolean identityColumn) { +record ColumnModel(String name, String type, boolean nullable, boolean identityColumn) { - public ColumnModel(String name, String type) { - this(name, type, false, false); - } + public ColumnModel(String name, String type) { + this(name, type, false, false); + } @Override public boolean equals(Object o) { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DefaultDatabaseTypeMapping.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DefaultDatabaseTypeMapping.java deleted file mode 100644 index c4ebe81b51..0000000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DefaultDatabaseTypeMapping.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.core.mapping.schemasqlgeneration; - -import java.util.HashMap; - - -/** - * Class that provides a default implementation of mapping Java type to a Database type. - * - * To customize the mapping an instance of a class implementing {@link DatabaseTypeMapping} interface - * can be set on the {@link SchemaModel} class - * - * @author Kurt Niemi - * @since 3.2 - */ -public class DefaultDatabaseTypeMapping implements DatabaseTypeMapping { - - final HashMap,String> mapClassToDatabaseType = new HashMap,String>(); - - public DefaultDatabaseTypeMapping() { - - mapClassToDatabaseType.put(String.class, "VARCHAR(255 BYTE)"); - mapClassToDatabaseType.put(Boolean.class, "TINYINT"); - mapClassToDatabaseType.put(Double.class, "DOUBLE"); - mapClassToDatabaseType.put(Float.class, "FLOAT"); - mapClassToDatabaseType.put(Integer.class, "INT"); - mapClassToDatabaseType.put(Long.class, "BIGINT"); - } - public String databaseTypeFromClass(Class type) { - - return mapClassToDatabaseType.get(type); - } -} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DefaultSqlTypeMapping.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DefaultSqlTypeMapping.java new file mode 100644 index 0000000000..9f32598f5a --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DefaultSqlTypeMapping.java @@ -0,0 +1,47 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping.schemasqlgeneration; + +import java.util.HashMap; + +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; + +/** + * Class that provides a default implementation of mapping Java type to a Database type. To customize the mapping an + * instance of a class implementing {@link SqlTypeMapping} interface can be set on the {@link MappedTables} class + * + * @author Kurt Niemi + * @since 3.2 + */ +public class DefaultSqlTypeMapping implements SqlTypeMapping { + + private final HashMap, String> mapClassToDatabaseType = new HashMap<>(); + + public DefaultSqlTypeMapping() { + + mapClassToDatabaseType.put(String.class, "VARCHAR(255 BYTE)"); + mapClassToDatabaseType.put(Boolean.class, "TINYINT"); + mapClassToDatabaseType.put(Double.class, "DOUBLE"); + mapClassToDatabaseType.put(Float.class, "FLOAT"); + mapClassToDatabaseType.put(Integer.class, "INT"); + mapClassToDatabaseType.put(Long.class, "BIGINT"); + } + + @Override + public String getColumnType(RelationalPersistentProperty property) { + return mapClassToDatabaseType.get(property.getActualType()); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java deleted file mode 100644 index e496985492..0000000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetGenerator.java +++ /dev/null @@ -1,351 +0,0 @@ -/* - * Copyright 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.data.relational.core.mapping.schemasqlgeneration; - -import liquibase.CatalogAndSchema; -import liquibase.change.AddColumnConfig; -import liquibase.change.ColumnConfig; -import liquibase.change.ConstraintsConfig; -import liquibase.change.core.AddColumnChange; -import liquibase.change.core.CreateTableChange; -import liquibase.change.core.DropColumnChange; -import liquibase.change.core.DropTableChange; -import liquibase.changelog.ChangeLogChild; -import liquibase.changelog.ChangeLogParameters; -import liquibase.changelog.ChangeSet; -import liquibase.changelog.DatabaseChangeLog; -import liquibase.database.Database; -import liquibase.exception.ChangeLogParseException; -import liquibase.exception.DatabaseException; -import liquibase.parser.core.yaml.YamlChangeLogParser; -import liquibase.resource.DirectoryResourceAccessor; -import liquibase.serializer.ChangeLogSerializer; -import liquibase.serializer.core.yaml.YamlChangeLogSerializer; -import liquibase.snapshot.DatabaseSnapshot; -import liquibase.snapshot.InvalidExampleException; -import liquibase.snapshot.SnapshotControl; -import liquibase.snapshot.SnapshotGeneratorFactory; -import liquibase.structure.core.Column; -import liquibase.structure.core.Table; -import org.springframework.core.io.Resource; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.function.Predicate; - -/** - * Use this class to generate Liquibase change sets. - * - * First create a {@link SchemaModel} instance passing in a RelationalContext to have - * a model that represents the Table(s)/Column(s) that the code expects to exist. - * - * And then optionally create a Liquibase database object that points to an existing database - * if one desires to create a changeset that could be applied to that database. - * - * If a Liquibase database object is not used, then the change set created would be - * something that could be applied to an empty database to make it match the state of the code. - * - * Prior to applying the changeset one should review and make adjustments appropriately. - * - * @author Kurt Niemi - * @since 3.2 - */ -public class LiquibaseChangeSetGenerator { - - private final SchemaModel sourceModel; - private final Database targetDatabase; - - /** - * If there should ever be future Liquibase tables that should not be deleted (removed), this - * predicate should be modified - */ - private final Predicate liquibaseTables = table -> ( table.startsWith("DATABASECHANGELOG") ); - - /** - * By default existing tables in the target database are never deleted - */ - public Predicate userApplicationTables = table -> ( true ); - - /** - * By default existing columns in the target database are never deleted. - * Columns will be passed into the predicate in the format TableName.ColumnName - */ - public Predicate userApplicationTableColumns = table -> ( true ); - - /** - * Use this to generate a ChangeSet that can be used on an empty database - * - * @author Kurt Niemi - * @since 3.2 - * - * @param sourceModel - Model representing table(s)/column(s) as existing in code - */ - public LiquibaseChangeSetGenerator(SchemaModel sourceModel) { - - this.sourceModel = sourceModel; - this.targetDatabase = null; - } - - /** - * Use this to generate a ChangeSet against an existing database - * - * @author Kurt Niemi - * @since 3.2 - * - * @param sourceModel - Model representing table(s)/column(s) as existing in code - * @param targetDatabase - Existing Liquibase database - */ - public LiquibaseChangeSetGenerator(SchemaModel sourceModel, Database targetDatabase) { - - this.sourceModel = sourceModel; - this.targetDatabase = targetDatabase; - } - - /** - * Generates a Liquibase Changeset - * - * @author Kurt Niemi - * @since 3.2 - * - * @param changeLogResource - Resource that changeset will be written to (or append to an existing ChangeSet file) - * @throws InvalidExampleException - * @throws DatabaseException - * @throws IOException - * @throws ChangeLogParseException - */ - public void generateLiquibaseChangeset(Resource changeLogResource) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { - - String changeSetId = Long.toString(System.currentTimeMillis()); - generateLiquibaseChangeset(changeLogResource, changeSetId, "Spring Data JDBC"); - } - - /** - * Generates a Liquibase Changeset - * - * @author Kurt Niemi - * @since 3.2 - * - * @param changeLogResource - Resource that changeset will be written to (or append to an existing ChangeSet file) - * @param changeSetId - A unique value to identify the changeset - * @param changeSetAuthor - Author information to be written to changeset file. - * @throws InvalidExampleException - * @throws DatabaseException - * @throws IOException - * @throws ChangeLogParseException - */ - public void generateLiquibaseChangeset(Resource changeLogResource, String changeSetId, String changeSetAuthor) throws InvalidExampleException, DatabaseException, IOException, ChangeLogParseException { - - SchemaDiff difference; - - if (targetDatabase != null) { - SchemaModel liquibaseModel = getLiquibaseModel(); - difference = new SchemaDiff(sourceModel,liquibaseModel); - } else { - difference = new SchemaDiff(sourceModel, new SchemaModel()); - } - - DatabaseChangeLog databaseChangeLog = getDatabaseChangeLog(changeLogResource.getFile()); - - ChangeSet changeSet = new ChangeSet(changeSetId, changeSetAuthor, false, false, "", "", "" , databaseChangeLog); - - generateTableAdditionsDeletions(changeSet, difference); - generateTableModifications(changeSet, difference); - - -// File changeLogFile = new File(changeLogFilePath); - writeChangeSet(databaseChangeLog, changeSet, changeLogResource.getFile()); - } - - private void generateTableAdditionsDeletions(ChangeSet changeSet, SchemaDiff difference) { - - for (TableModel table : difference.getTableAdditions()) { - CreateTableChange newTable = createAddTableChange(table); - changeSet.addChange(newTable); - } - - for (TableModel table : difference.getTableDeletions()) { - // Do not delete/drop table if it is an external application table - if (!userApplicationTables.test(table.name())) { - DropTableChange dropTable = createDropTableChange(table); - changeSet.addChange(dropTable); - } - } - } - - private void generateTableModifications(ChangeSet changeSet, SchemaDiff difference) { - - for (TableDiff table : difference.getTableDiff()) { - - if (table.addedColumns().size() > 0) { - AddColumnChange addColumnChange = new AddColumnChange(); - addColumnChange.setSchemaName(table.tableModel().schema()); - addColumnChange.setTableName(table.tableModel().name()); - - for (ColumnModel column : table.addedColumns()) { - AddColumnConfig addColumn = createAddColumnChange(column); - addColumnChange.addColumn(addColumn); - } - - changeSet.addChange(addColumnChange); - } - - ArrayList deletedColumns = new ArrayList<>(); - for (ColumnModel columnModel : table.deletedColumns()) { - String fullName = table.tableModel().name() + "." + columnModel.name(); - - if (!userApplicationTableColumns.test(fullName)) { - deletedColumns.add(columnModel); - } - } - - if (deletedColumns.size() > 0) { - DropColumnChange dropColumnChange = new DropColumnChange(); - dropColumnChange.setSchemaName(table.tableModel().schema()); - dropColumnChange.setTableName(table.tableModel().name()); - - List dropColumns = new ArrayList(); - for (ColumnModel column : table.deletedColumns()) { - ColumnConfig config = new ColumnConfig(); - config.setName(column.name()); - dropColumns.add(config); - } - dropColumnChange.setColumns(dropColumns); - changeSet.addChange(dropColumnChange); - } - } - } - - private DatabaseChangeLog getDatabaseChangeLog(File changeLogFile) { - - DatabaseChangeLog databaseChangeLog = null; - - try { - YamlChangeLogParser parser = new YamlChangeLogParser(); - File parentDirectory = changeLogFile.getParentFile(); - if (parentDirectory == null) { - parentDirectory = new File("./"); - } - DirectoryResourceAccessor resourceAccessor = new DirectoryResourceAccessor(parentDirectory); - ChangeLogParameters parameters = new ChangeLogParameters(); - databaseChangeLog = parser.parse(changeLogFile.getName(), parameters, resourceAccessor); - } catch (Exception ex) { - databaseChangeLog = new DatabaseChangeLog(changeLogFile.getAbsolutePath()); - } - return databaseChangeLog; - } - - private void writeChangeSet(DatabaseChangeLog databaseChangeLog, ChangeSet changeSet, File changeLogFile) throws FileNotFoundException, IOException { - - ChangeLogSerializer serializer = new YamlChangeLogSerializer(); - List changes = new ArrayList(); - for (ChangeSet change : databaseChangeLog.getChangeSets()) { - changes.add(change); - } - changes.add(changeSet); - FileOutputStream fos = new FileOutputStream(changeLogFile); - serializer.write(changes, fos); - } - - private SchemaModel getLiquibaseModel() throws DatabaseException, InvalidExampleException { - SchemaModel liquibaseModel = new SchemaModel(); - - CatalogAndSchema[] schemas = new CatalogAndSchema[] { targetDatabase.getDefaultSchema() }; - SnapshotControl snapshotControl = new SnapshotControl(targetDatabase); - - DatabaseSnapshot snapshot = SnapshotGeneratorFactory.getInstance().createSnapshot(schemas, targetDatabase, snapshotControl); - Set
tables = snapshot.get(liquibase.structure.core.Table.class); - - for (int i=0; i < sourceModel.getTableData().size(); i++) { - TableModel currentModel = sourceModel.getTableData().get(i); - if (currentModel.schema() == null || currentModel.schema().isEmpty()) { - TableModel newModel = new TableModel(targetDatabase.getDefaultSchema().getCatalogName(), - currentModel.name(), currentModel.columns(), currentModel.keyColumns()); - sourceModel.getTableData().set(i, newModel); - } - } - - for (liquibase.structure.core.Table table : tables) { - - // Exclude internal Liquibase tables from comparison - if (liquibaseTables.test(table.getName())) { - continue; - } - - TableModel tableModel = new TableModel(table.getSchema().getCatalogName(), table.getName()); - liquibaseModel.getTableData().add(tableModel); - - List columns = table.getColumns(); - for (liquibase.structure.core.Column column : columns) { - String type = column.getType().toString(); - boolean nullable = column.isNullable(); - ColumnModel columnModel = new ColumnModel(column.getName(), type, nullable, false); - tableModel.columns().add(columnModel); - } - } - - return liquibaseModel; - } - - private AddColumnConfig createAddColumnChange(ColumnModel column) { - - AddColumnConfig config = new AddColumnConfig(); - config.setName(column.name()); - config.setType(column.type()); - - if (column.identityColumn()) { - config.setAutoIncrement(true); - } - return config; - } - - private CreateTableChange createAddTableChange(TableModel table) { - - CreateTableChange change = new CreateTableChange(); - change.setSchemaName(table.schema()); - change.setTableName(table.name()); - - for (ColumnModel column : table.columns()) { - ColumnConfig columnConfig = new ColumnConfig(); - columnConfig.setName(column.name()); - columnConfig.setType(column.type()); - - if (column.identityColumn()) { - columnConfig.setAutoIncrement(true); - ConstraintsConfig constraints = new ConstraintsConfig(); - constraints.setPrimaryKey(true); - columnConfig.setConstraints(constraints); - } - change.addColumn(columnConfig); - } - - return change; - } - - private DropTableChange createDropTableChange(TableModel table) { - DropTableChange change = new DropTableChange(); - change.setSchemaName(table.schema()); - change.setTableName(table.name()); - change.setCascadeConstraints(true); - - return change; - } -} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetWriter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetWriter.java new file mode 100644 index 0000000000..8afd43835c --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetWriter.java @@ -0,0 +1,423 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.relational.core.mapping.schemasqlgeneration; + +import liquibase.CatalogAndSchema; +import liquibase.change.AddColumnConfig; +import liquibase.change.ColumnConfig; +import liquibase.change.ConstraintsConfig; +import liquibase.change.core.AddColumnChange; +import liquibase.change.core.CreateTableChange; +import liquibase.change.core.DropColumnChange; +import liquibase.change.core.DropTableChange; +import liquibase.changelog.ChangeLogChild; +import liquibase.changelog.ChangeLogParameters; +import liquibase.changelog.ChangeSet; +import liquibase.changelog.DatabaseChangeLog; +import liquibase.database.Database; +import liquibase.exception.LiquibaseException; +import liquibase.parser.ChangeLogParser; +import liquibase.parser.core.yaml.YamlChangeLogParser; +import liquibase.resource.DirectoryResourceAccessor; +import liquibase.serializer.ChangeLogSerializer; +import liquibase.serializer.core.yaml.YamlChangeLogSerializer; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.SnapshotControl; +import liquibase.snapshot.SnapshotGeneratorFactory; +import liquibase.structure.core.Column; +import liquibase.structure.core.Table; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * Use this class to write Liquibase change sets. + *

+ * First create a {@link MappedTables} instance passing in a RelationalContext to have a model that represents the + * Table(s)/Column(s) that the code expects to exist. And then optionally create a Liquibase database object that points + * to an existing database if one desires to create a changeset that could be applied to that database. If a database + * object is not used, then the change set created would be something that could be applied to an empty database to make + * it match the state of the code. Prior to applying the changeset one should review and make adjustments appropriately. + * + * @author Kurt Niemi + * @since 3.2 + */ +public class LiquibaseChangeSetWriter { + + private final MappedTables sourceModel; + private final Database targetDatabase; + + private ChangeLogSerializer changeLogSerializer = new YamlChangeLogSerializer(); + + private ChangeLogParser changeLogParser = new YamlChangeLogParser(); + + /** + * Predicate to identify Liquibase system tables. + */ + private final Predicate liquibaseTables = table -> table.toUpperCase(Locale.ROOT) + .startsWith("DATABASECHANGELOG"); + + /** + * Filter predicate used to determine whether an existing table should be removed. Defaults to {@code false} to keep + * existing tables. + */ + public Predicate dropTableFilter = table -> true; + + /** + * Filter predicate used to determine whether an existing column should be removed. Defaults to {@code false} to keep + * existing columns. + */ + public BiPredicate dropColumnFilter = (table, column) -> false; + + /** + * Use this to generate a ChangeSet that can be used on an empty database + * + * @param sourceModel - Model representing table(s)/column(s) as existing in code + */ + public LiquibaseChangeSetWriter(MappedTables sourceModel) { + + this.sourceModel = sourceModel; + this.targetDatabase = null; + } + + /** + * Use this to generate a ChangeSet against an existing database + * + * @param sourceModel model representing table(s)/column(s) as existing in code. + * @param targetDatabase existing Liquibase database. + */ + public LiquibaseChangeSetWriter(MappedTables sourceModel, Database targetDatabase) { + + this.sourceModel = sourceModel; + this.targetDatabase = targetDatabase; + } + + /** + * Set the {@link ChangeLogSerializer}. + * + * @param changeLogSerializer + */ + public void setChangeLogSerializer(ChangeLogSerializer changeLogSerializer) { + + Assert.notNull(changeLogSerializer, "ChangeLogSerializer must not be null"); + + this.changeLogSerializer = changeLogSerializer; + } + + /** + * Set the {@link ChangeLogParser}. + * + * @param changeLogParser + */ + public void setChangeLogParser(ChangeLogParser changeLogParser) { + + Assert.notNull(changeLogParser, "ChangeLogParser must not be null"); + + this.changeLogParser = changeLogParser; + } + + /** + * Set the filter predicate to identify tables to drop. The predicate accepts the table name. Returning {@code true} + * will delete the table; {@code false} retains the table. + * + * @param dropTableFilter must not be {@literal null}. + */ + public void setDropTableFilter(Predicate dropTableFilter) { + + Assert.notNull(dropTableFilter, "Drop Column filter must not be null"); + + this.dropTableFilter = dropTableFilter; + } + + /** + * Set the filter predicate to identify columns within a table to drop. The predicate accepts the table- and column + * name. Returning {@code true} will delete the column; {@code false} retains the column. + * + * @param dropColumnFilter must not be {@literal null}. + */ + public void setDropColumnFilter(BiPredicate dropColumnFilter) { + + Assert.notNull(dropColumnFilter, "Drop Column filter must not be null"); + + this.dropColumnFilter = dropColumnFilter; + } + + /** + * Write a Liquibase changeset. + * + * @param changeLogResource resource that changeset will be written to (or append to an existing ChangeSet file). The + * resource must resolve to a valid {@link Resource#getFile()}. + * @throws LiquibaseException + * @throws IOException + */ + public void writeChangeSet(Resource changeLogResource) throws LiquibaseException, IOException { + + String changeSetId = Long.toString(System.currentTimeMillis()); + writeChangeSet(changeLogResource, changeSetId, "Spring Data Relational"); + } + + /** + * Write a Liquibase changeset. + * + * @param changeLogResource resource that changeset will be written to (or append to an existing ChangeSet file). + * @param changeSetId unique value to identify the changeset. + * @param changeSetAuthor author information to be written to changeset file. + * @throws LiquibaseException + * @throws IOException + */ + public void writeChangeSet(Resource changeLogResource, String changeSetId, String changeSetAuthor) + throws LiquibaseException, IOException { + + SchemaDiff difference; + + if (targetDatabase != null) { + MappedTables liquibaseModel = getLiquibaseModel(targetDatabase); + difference = new SchemaDiff(sourceModel, liquibaseModel); + } else { + difference = new SchemaDiff(sourceModel, new MappedTables()); + } + + DatabaseChangeLog databaseChangeLog = getDatabaseChangeLog(changeLogResource.getFile()); + ChangeSet changeSet = new ChangeSet(changeSetId, changeSetAuthor, false, false, "", "", "", databaseChangeLog); + + generateTableAdditionsDeletions(changeSet, difference); + generateTableModifications(changeSet, difference); + + // File changeLogFile = new File(changeLogFilePath); + writeChangeSet(databaseChangeLog, changeSet, changeLogResource.getFile()); + } + + private DatabaseChangeLog getDatabaseChangeLog(File changeLogFile) { + + DatabaseChangeLog databaseChangeLog; + + try { + File parentDirectory = changeLogFile.getParentFile(); + if (parentDirectory == null) { + parentDirectory = new File("./"); + } + DirectoryResourceAccessor resourceAccessor = new DirectoryResourceAccessor(parentDirectory); + ChangeLogParameters parameters = new ChangeLogParameters(); + databaseChangeLog = changeLogParser.parse(changeLogFile.getName(), parameters, resourceAccessor); + } catch (Exception ex) { + databaseChangeLog = new DatabaseChangeLog(changeLogFile.getAbsolutePath()); + } + + return databaseChangeLog; + } + + private void generateTableAdditionsDeletions(ChangeSet changeSet, SchemaDiff difference) { + + for (TableModel table : difference.getTableAdditions()) { + CreateTableChange newTable = changeTable(table); + changeSet.addChange(newTable); + } + + for (TableModel table : difference.getTableDeletions()) { + // Do not delete/drop table if it is an external application table + if (dropTableFilter.test(table.name())) { + changeSet.addChange(dropTable(table)); + } + } + } + + private void generateTableModifications(ChangeSet changeSet, SchemaDiff difference) { + + for (TableDiff table : difference.getTableDiff()) { + + if (!table.columnsToAdd().isEmpty()) { + changeSet.addChange(addColumns(table)); + } + + List deletedColumns = getColumnsToDrop(table); + + if (deletedColumns.size() > 0) { + changeSet.addChange(dropColumns(table, deletedColumns)); + } + } + } + + private List getColumnsToDrop(TableDiff table) { + + List deletedColumns = new ArrayList<>(); + for (ColumnModel columnModel : table.columnsToDrop()) { + + if (dropColumnFilter.test(table.table().name(), columnModel.name())) { + deletedColumns.add(columnModel); + } + } + return deletedColumns; + } + + private void writeChangeSet(DatabaseChangeLog databaseChangeLog, ChangeSet changeSet, File changeLogFile) + throws IOException { + + List changes = new ArrayList<>(databaseChangeLog.getChangeSets()); + changes.add(changeSet); + + try (FileOutputStream fos = new FileOutputStream(changeLogFile)) { + changeLogSerializer.write(changes, fos); + } + } + + private MappedTables getLiquibaseModel(Database targetDatabase) throws LiquibaseException { + + MappedTables liquibaseModel = new MappedTables(); + + CatalogAndSchema[] schemas = new CatalogAndSchema[] { targetDatabase.getDefaultSchema() }; + SnapshotControl snapshotControl = new SnapshotControl(targetDatabase); + + DatabaseSnapshot snapshot = SnapshotGeneratorFactory.getInstance().createSnapshot(schemas, targetDatabase, + snapshotControl); + Set

tables = snapshot.get(liquibase.structure.core.Table.class); + List processed = associateTablesWithSchema(sourceModel.getTableData(), targetDatabase); + + sourceModel.getTableData().clear(); + sourceModel.getTableData().addAll(processed); + + for (liquibase.structure.core.Table table : tables) { + + // Exclude internal Liquibase tables from comparison + if (liquibaseTables.test(table.getName())) { + continue; + } + + TableModel tableModel = new TableModel(table.getSchema().getCatalogName(), table.getName()); + + List columns = table.getColumns(); + + for (liquibase.structure.core.Column column : columns) { + + String type = column.getType().toString(); + boolean nullable = column.isNullable(); + ColumnModel columnModel = new ColumnModel(column.getName(), type, nullable, false); + + tableModel.columns().add(columnModel); + } + + liquibaseModel.getTableData().add(tableModel); + } + + return liquibaseModel; + } + + private List associateTablesWithSchema(List tables, Database targetDatabase) { + + List processed = new ArrayList<>(tables.size()); + + for (TableModel currentModel : tables) { + + if (currentModel.schema() == null || currentModel.schema().isEmpty()) { + TableModel newModel = new TableModel(targetDatabase.getDefaultSchema().getCatalogName(), currentModel.name(), + currentModel.columns(), currentModel.keyColumns()); + processed.add(newModel); + } else { + processed.add(currentModel); + } + } + + return processed; + } + + private static AddColumnChange addColumns(TableDiff table) { + + AddColumnChange addColumnChange = new AddColumnChange(); + addColumnChange.setSchemaName(table.table().schema()); + addColumnChange.setTableName(table.table().name()); + + for (ColumnModel column : table.columnsToAdd()) { + AddColumnConfig addColumn = createAddColumnChange(column); + addColumnChange.addColumn(addColumn); + } + return addColumnChange; + } + + private static AddColumnConfig createAddColumnChange(ColumnModel column) { + + AddColumnConfig config = new AddColumnConfig(); + config.setName(column.name()); + config.setType(column.type()); + + if (column.identityColumn()) { + config.setAutoIncrement(true); + } + + return config; + } + + private static DropColumnChange dropColumns(TableDiff table, Collection deletedColumns) { + + DropColumnChange dropColumnChange = new DropColumnChange(); + dropColumnChange.setSchemaName(table.table().schema()); + dropColumnChange.setTableName(table.table().name()); + + List dropColumns = new ArrayList<>(); + + for (ColumnModel column : deletedColumns) { + ColumnConfig config = new ColumnConfig(); + config.setName(column.name()); + dropColumns.add(config); + } + + dropColumnChange.setColumns(dropColumns); + return dropColumnChange; + } + + private static CreateTableChange changeTable(TableModel table) { + + CreateTableChange change = new CreateTableChange(); + change.setSchemaName(table.schema()); + change.setTableName(table.name()); + + for (ColumnModel column : table.columns()) { + ColumnConfig columnConfig = new ColumnConfig(); + columnConfig.setName(column.name()); + columnConfig.setType(column.type()); + + if (column.identityColumn()) { + columnConfig.setAutoIncrement(true); + ConstraintsConfig constraints = new ConstraintsConfig(); + constraints.setPrimaryKey(true); + columnConfig.setConstraints(constraints); + } + change.addColumn(columnConfig); + } + + return change; + } + + private static DropTableChange dropTable(TableModel table) { + + DropTableChange change = new DropTableChange(); + change.setSchemaName(table.schema()); + change.setTableName(table.name()); + change.setCascadeConstraints(true); + + return change; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTables.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTables.java new file mode 100644 index 0000000000..fd1ab6a100 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTables.java @@ -0,0 +1,83 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping.schemasqlgeneration; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; + +/** + * Model class that contains Table/Column information that can be used to generate SQL for Schema generation. + * + * @author Kurt Niemi + * @since 3.2 + */ +public class MappedTables { + + private final List tableData = new ArrayList<>(); + public final SqlTypeMapping sqlTypeMapping; + + public MappedTables() { + this.sqlTypeMapping = new DefaultSqlTypeMapping(); + } + + /** + * Create model from a RelationalMappingContext + */ + public MappedTables(RelationalMappingContext context) { + this.sqlTypeMapping = createTypeMapping(context); + } + + // TODO: Add support (i.e. create tickets) to support mapped collections, entities, embedded properties, and aggregate + // references. + private SqlTypeMapping createTypeMapping(RelationalMappingContext context) { + + SqlTypeMapping sqlTypeMapping = new DefaultSqlTypeMapping(); + + for (RelationalPersistentEntity entity : context.getPersistentEntities()) { + + TableModel tableModel = new TableModel(entity.getTableName().getReference()); + + Set identifierColumns = new LinkedHashSet<>(); + entity.getPersistentProperties(Id.class).forEach(identifierColumns::add); + + for (RelationalPersistentProperty property : entity) { + + if (property.isEntity() && !property.isEmbedded()) { + continue; + } + + ColumnModel columnModel = new ColumnModel(property.getColumnName().getReference(), + sqlTypeMapping.getColumnType(property), true, identifierColumns.contains(property)); + tableModel.columns().add(columnModel); + } + + tableData.add(tableModel); + } + + return sqlTypeMapping; + } + + List getTableData() { + return tableData; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java index 3a4668335b..d125b87d12 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java @@ -1,99 +1,101 @@ package org.springframework.data.relational.core.mapping.schemasqlgeneration; -import java.util.*; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; /** - * This class is created to return the difference between a source and target {@link SchemaModel} - * - * The difference consists of Table Additions, Deletions, and Modified Tables (i.e. table - * exists in both source and target - but has columns to add or delete) + * This class is created to return the difference between a source and target {@link MappedTables} The difference + * consists of Table Additions, Deletions, and Modified Tables (i.e. table exists in both source and target - but has + * columns to add or delete) * * @author Kurt Niemi * @since 3.2 */ -public class SchemaDiff { - private final List tableAdditions = new ArrayList(); - private final List tableDeletions = new ArrayList(); - private final List tableDiffs = new ArrayList(); - - private SchemaModel source; - private SchemaModel target; +class SchemaDiff { - /** - * - * Compare two {@link SchemaModel} to identify differences. - * - * @param target - Model reflecting current database state - * @param source - Model reflecting desired database state - */ - public SchemaDiff(SchemaModel target, SchemaModel source) { + private final MappedTables source; + private final MappedTables target; - this.source = source; - this.target = target; + private final List tableAdditions = new ArrayList<>(); + private final List tableDeletions = new ArrayList<>(); + private final List tableDiffs = new ArrayList<>(); - diffTableAdditionDeletion(); - diffTable(); - } + /** + * Compare two {@link MappedTables} to identify differences. + * + * @param target model reflecting current database state. + * @param source model reflecting desired database state. + */ + public SchemaDiff(MappedTables target, MappedTables source) { - public List getTableAdditions() { + this.source = source; + this.target = target; - return tableAdditions; - } + diffTableAdditionDeletion(); + diffTable(); + } - public List getTableDeletions() { + public List getTableAdditions() { + return tableAdditions; + } - return tableDeletions; - } - public List getTableDiff() { + public List getTableDeletions() { + return tableDeletions; + } - return tableDiffs; - } + public List getTableDiff() { + return tableDiffs; + } - private void diffTableAdditionDeletion() { + private void diffTableAdditionDeletion() { - Set sourceTableData = new HashSet(source.getTableData()); - Set targetTableData = new HashSet(target.getTableData()); + List sourceTableData = new ArrayList<>(source.getTableData()); + List targetTableData = new ArrayList<>(target.getTableData()); - // Identify deleted tables - Set deletedTables = new HashSet(sourceTableData); - deletedTables.removeAll(targetTableData); - tableDeletions.addAll(deletedTables); + // Identify deleted tables + List deletedTables = new ArrayList<>(sourceTableData); + deletedTables.removeAll(targetTableData); + tableDeletions.addAll(deletedTables); - // Identify added tables - Set addedTables = new HashSet(targetTableData); - addedTables.removeAll(sourceTableData); - tableAdditions.addAll(addedTables); - } + // Identify added tables + List addedTables = new ArrayList<>(targetTableData); + addedTables.removeAll(sourceTableData); + tableAdditions.addAll(addedTables); + } - private void diffTable() { + private void diffTable() { - HashMap sourceTablesMap = new HashMap(); - for (TableModel table : source.getTableData()) { - sourceTablesMap.put(table.schema() + "." + table.name(), table); - } + Map sourceTablesMap = new LinkedHashMap<>(); + for (TableModel table : source.getTableData()) { + sourceTablesMap.put(table.schema() + "." + table.name(), table); + } - Set existingTables = new HashSet(target.getTableData()); - existingTables.removeAll(getTableAdditions()); + Set existingTables = new LinkedHashSet<>(target.getTableData()); + getTableAdditions().forEach(existingTables::remove); - for (TableModel table : existingTables) { - TableDiff tableDiff = new TableDiff(table); - tableDiffs.add(tableDiff); + for (TableModel table : existingTables) { + TableDiff tableDiff = new TableDiff(table); + tableDiffs.add(tableDiff); - TableModel sourceTable = sourceTablesMap.get(table.schema() + "." + table.name()); + TableModel sourceTable = sourceTablesMap.get(table.schema() + "." + table.name()); - Set sourceTableData = new HashSet(sourceTable.columns()); - Set targetTableData = new HashSet(table.columns()); + Set sourceTableData = new LinkedHashSet<>(sourceTable.columns()); + Set targetTableData = new LinkedHashSet<>(table.columns()); - // Identify deleted columns - Set deletedColumns = new HashSet(sourceTableData); - deletedColumns.removeAll(targetTableData); + // Identify deleted columns + Set deletedColumns = new LinkedHashSet<>(sourceTableData); + deletedColumns.removeAll(targetTableData); - tableDiff.deletedColumns().addAll(deletedColumns); + tableDiff.columnsToDrop().addAll(deletedColumns); - // Identify added columns - Set addedColumns = new HashSet(targetTableData); - addedColumns.removeAll(sourceTableData); - tableDiff.addedColumns().addAll(addedColumns); - } - } + // Identify added columns + Set addedColumns = new LinkedHashSet<>(targetTableData); + addedColumns.removeAll(sourceTableData); + tableDiff.columnsToAdd().addAll(addedColumns); + } + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java deleted file mode 100644 index 209fc2d763..0000000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaModel.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.core.mapping.schemasqlgeneration; - -import org.springframework.data.annotation.Id; -import org.springframework.data.mapping.PropertyHandler; -import org.springframework.data.relational.core.mapping.*; - -import java.util.*; - -/** - * Model class that contains Table/Column information that can be used - * to generate SQL for Schema generation. - * - * @author Kurt Niemi - * @since 3.2 - */ -public class SchemaModel -{ - private final List tableData = new ArrayList(); - public DatabaseTypeMapping databaseTypeMapping; - - /** - * Create empty model - */ - public SchemaModel() { - - } - - /** - * Create model from a RelationalMappingContext - */ - public SchemaModel(RelationalMappingContext context) { - - if (databaseTypeMapping == null) { - databaseTypeMapping = new DefaultDatabaseTypeMapping(); - } - - for (RelationalPersistentEntity entity : context.getPersistentEntities()) { - TableModel tableModel = new TableModel(entity.getTableName().getReference()); - - Iterator iter = - entity.getPersistentProperties(Id.class).iterator(); - Set setIdentifierColumns = new HashSet(); - while (iter.hasNext()) { - BasicRelationalPersistentProperty p = iter.next(); - setIdentifierColumns.add(p); - } - - entity.doWithProperties((PropertyHandler) handler -> { - BasicRelationalPersistentProperty property = (BasicRelationalPersistentProperty)handler; - - if (property.isEntity() && !property.isEmbedded()) { - return; - } - - ColumnModel columnModel = new ColumnModel(property.getColumnName().getReference(), - databaseTypeMapping.databaseTypeFromClass(property.getActualType()), - true, setIdentifierColumns.contains(property)); - tableModel.columns().add(columnModel); - }); - - tableData.add(tableModel); - } - } - - public List getTableData() { - return tableData; - } -} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DatabaseTypeMapping.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SqlTypeMapping.java similarity index 60% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DatabaseTypeMapping.java rename to spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SqlTypeMapping.java index bd99e4ce27..1f13ee4c78 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DatabaseTypeMapping.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SqlTypeMapping.java @@ -15,15 +15,21 @@ */ package org.springframework.data.relational.core.mapping.schemasqlgeneration; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; + /** - * Interface for mapping a Java type to a Database type. - * - * To customize the mapping an instance of a class implementing {@link DatabaseTypeMapping} interface - * can be set on the {@link SchemaModel} class. + * Interface for mapping a {@link RelationalPersistentProperty} to a Database type. * * @author Kurt Niemi * @since 3.2 */ -public interface DatabaseTypeMapping { - public String databaseTypeFromClass(Class type); -} \ No newline at end of file +public interface SqlTypeMapping { + + /** + * Determine a column type for a persistent property. + * + * @param property the property for which the type should be determined. + * @return the SQL type to use, such as {@code VARCHAR} or {@code NUMERIC}. + */ + String getColumnType(RelationalPersistentProperty property); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java index 137a5bdf94..648f813ad8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java @@ -4,18 +4,16 @@ import java.util.List; /** - * Used to keep track of columns that have been added or deleted, - * when performing a difference between a source and target {@link SchemaModel} + * Used to keep track of columns that should be added or deleted, when performing a difference between a source and + * target {@link MappedTables}. * * @author Kurt Niemi * @since 3.2 */ -public record TableDiff(TableModel tableModel, - ArrayList addedColumns, - ArrayList deletedColumns) { +record TableDiff(TableModel table, List columnsToAdd, List columnsToDrop) { - public TableDiff(TableModel tableModel) { - this(tableModel, new ArrayList<>(), new ArrayList<>()); - } + public TableDiff(TableModel tableModel) { + this(tableModel, new ArrayList<>(), new ArrayList<>()); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java index 795fbb8b23..8266e0172f 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java @@ -15,8 +15,6 @@ */ package org.springframework.data.relational.core.mapping.schemasqlgeneration; -import org.springframework.data.relational.core.sql.SqlIdentifier; - import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -27,43 +25,43 @@ * @author Kurt Niemi * @since 3.2 */ -public record TableModel(String schema, String name, List columns, List keyColumns) { - public TableModel(String schema, String name) { - this(schema, name, new ArrayList<>(), new ArrayList<>()); - } +record TableModel(String schema, String name, List columns, List keyColumns) { - public TableModel(String name) { - this(null, name); - } + public TableModel(String schema, String name) { + this(schema, name, new ArrayList<>(), new ArrayList<>()); + } - @Override - public boolean equals(Object o) { + public TableModel(String name) { + this(null, name); + } - if (this == o) { - return true; - } + @Override + public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) { - return false; - } + if (this == o) { + return true; + } - TableModel that = (TableModel) o; + if (o == null || getClass() != o.getClass()) { + return false; + } - // If we are missing the schema for either TableModel we will not treat that as being different - if (schema != null && that.schema != null && !schema.isEmpty() && !that.schema.isEmpty()) { - if (!Objects.equals(schema, that.schema)) { - return false; - } - } - if (!name.toUpperCase().equals(that.name.toUpperCase())) { - return false; - } - return true; - } + TableModel that = (TableModel) o; - @Override - public int hashCode() { + // If we are missing the schema for either TableModel we will not treat that as being different + if (schema != null && that.schema != null && !schema.isEmpty() && !that.schema.isEmpty()) { + if (!Objects.equals(schema, that.schema)) { + return false; + } + } + if (!name.equalsIgnoreCase(that.name)) { + return false; + } + return true; + } - return Objects.hash(name.toUpperCase()); - } + @Override + public int hashCode() { + return Objects.hash(schema, name.toUpperCase()); + } } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTablesUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTablesUnitTests.java new file mode 100644 index 0000000000..78348825de --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTablesUnitTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping.schemasqlgeneration; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.Table; + +/** + * Unit tests for the {@link MappedTables}. + * + * @author Kurt Niemi + */ +class MappedTablesUnitTests { + + @Test + void testDiffSchema() { + + RelationalMappingContext context = new RelationalMappingContext(); + context.getRequiredPersistentEntity(MappedTablesUnitTests.Table1.class); + context.getRequiredPersistentEntity(MappedTablesUnitTests.Table2.class); + + MappedTables target = new MappedTables(context); + MappedTables source = new MappedTables(context); + + // Add column to table + ColumnModel newColumn = new ColumnModel("newcol", "VARCHAR(255)"); + source.getTableData().get(0).columns().add(newColumn); + + // Remove table + source.getTableData().remove(1); + + // Add new table + TableModel newTable = new TableModel(null, "newtable"); + newTable.columns().add(newColumn); + source.getTableData().add(newTable); + + SchemaDiff diff = new SchemaDiff(target, source); + + // Verify that newtable is an added table in the diff + assertThat(diff.getTableAdditions()).isNotEmpty(); + assertThat(diff.getTableAdditions().get(0).name()).isEqualTo("table1"); + + assertThat(diff.getTableDeletions()).isNotEmpty(); + assertThat(diff.getTableDeletions().get(0).name()).isEqualTo("newtable"); + + assertThat(diff.getTableDiff()).isNotEmpty(); + assertThat(diff.getTableDiff().get(0).columnsToAdd()).isEmpty(); + assertThat(diff.getTableDiff().get(0).columnsToDrop()).isNotEmpty(); + } + + // Test table classes for performing schema diff + @Table + static class Table1 { + String force; + String be; + String with; + String you; + } + + @Table + static class Table2 { + String lukeIAmYourFather; + Boolean darkSide; + Float floater; + Double doubleClass; + Integer integerClass; + } + +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java deleted file mode 100644 index fed396222d..0000000000 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/SchemaModelTests.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.core.sql; - -import org.junit.jupiter.api.Test; -import org.springframework.data.relational.core.mapping.Column; -import org.springframework.data.relational.core.mapping.RelationalMappingContext; -import org.springframework.data.relational.core.mapping.Table; -import org.springframework.data.relational.core.mapping.schemasqlgeneration.*; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for the {@link SchemaModel}. - * - * @author Kurt Niemi - */ -public class SchemaModelTests { - - @Test - void testDiffSchema() { - - RelationalMappingContext context = new RelationalMappingContext(); - context.getRequiredPersistentEntity(SchemaModelTests.Table1.class); - context.getRequiredPersistentEntity(SchemaModelTests.Table2.class); - - SchemaModel model = new SchemaModel(context); - - SchemaModel newModel = new SchemaModel(context); - - // Add column to table - ColumnModel newColumn = new ColumnModel("newcol", "VARCHAR(255)"); - newModel.getTableData().get(0).columns().add(newColumn); - - // Remove table - newModel.getTableData().remove(1); - - // Add new table - TableModel newTable = new TableModel(null, "newtable"); - newTable.columns().add(newColumn); - newModel.getTableData().add(newTable); - - SchemaDiff diff = new SchemaDiff(model, newModel); - - // Verify that newtable is an added table in the diff - assertThat(diff.getTableAdditions().size() > 0); - assertThat(diff.getTableAdditions().get(0).name().equals("table1")); - - assertThat(diff.getTableDeletions().size() > 0); - assertThat(diff.getTableDeletions().get(0).name().equals("vader")); - - assertThat(diff.getTableDiff().size() > 0); - assertThat(diff.getTableDiff().get(0).addedColumns().size() > 0); - assertThat(diff.getTableDiff().get(0).deletedColumns().size() > 0); - } - - // Test table classes for performing schema diff - @Table - static class Table1 { - @Column - public String force; - @Column - public String be; - @Column - public String with; - @Column - public String you; - } - - @Table - static class Table2 { - @Column - public String lukeIAmYourFather; - @Column - public Boolean darkSide; - @Column - public Float floater; - @Column - public Double doubleClass; - @Column - public Integer integerClass; - } - - -} From e4f1a0a8765b8576633940aff1f5a214c2fd5d4f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 2 Jun 2023 18:06:51 +0200 Subject: [PATCH 30/32] Polishing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename package to schema as the schema is being created and updated and not generated. Rename …Model classes to just their name as types are package-private and not visible externally. Refactor SchemaDiff to Java record. Use different overloads to write schema changes to avoid LiquibaseException leaking into cases where no diff is being used. Introduce SchemaFilter to filter unwanted mapped entities. Add tests for changeset-creation. --- spring-data-relational/pom.xml | 2 +- .../ColumnModel.java => schema/Column.java} | 8 +- .../DefaultSqlTypeMapping.java | 7 +- .../LiquibaseChangeSetWriter.java | 230 +++++++++++------- .../core/mapping/schema/SchemaDiff.java | 99 ++++++++ .../SqlTypeMapping.java | 2 +- .../TableModel.java => schema/Table.java} | 33 ++- .../core/mapping/schema/TableDiff.java | 19 ++ .../core/mapping/schema/Tables.java | 76 ++++++ .../core/mapping/schema/package-info.java | 7 + .../schemasqlgeneration/MappedTables.java | 83 ------- .../schemasqlgeneration/SchemaDiff.java | 101 -------- .../schemasqlgeneration/TableDiff.java | 19 -- .../LiquibaseChangeSetWriterUnitTests.java | 65 +++++ .../mapping/schema/SchemaDiffUnitTests.java | 87 +++++++ .../MappedTablesUnitTests.java | 85 ------- 16 files changed, 523 insertions(+), 400 deletions(-) rename spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/{schemasqlgeneration/ColumnModel.java => schema/Column.java} (80%) rename spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/{schemasqlgeneration => schema}/DefaultSqlTypeMapping.java (85%) rename spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/{schemasqlgeneration => schema}/LiquibaseChangeSetWriter.java (58%) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/SchemaDiff.java rename spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/{schemasqlgeneration => schema}/SqlTypeMapping.java (93%) rename spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/{schemasqlgeneration/TableModel.java => schema/Table.java} (59%) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/TableDiff.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Tables.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/package-info.java delete mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTables.java delete mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java delete mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schema/LiquibaseChangeSetWriterUnitTests.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schema/SchemaDiffUnitTests.java delete mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTablesUnitTests.java diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 6c90278ca8..892a9d3503 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -104,6 +104,6 @@ test - + diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Column.java similarity index 80% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java rename to spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Column.java index 5eda4d9d9f..8d728e62de 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/ColumnModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Column.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.relational.core.mapping.schemasqlgeneration; +package org.springframework.data.relational.core.mapping.schema; import java.util.Objects; @@ -23,9 +23,9 @@ * @author Kurt Niemi * @since 3.2 */ -record ColumnModel(String name, String type, boolean nullable, boolean identityColumn) { +record Column(String name, String type, boolean nullable, boolean identity) { - public ColumnModel(String name, String type) { + public Column(String name, String type) { this(name, type, false, false); } @@ -33,7 +33,7 @@ public ColumnModel(String name, String type) { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - ColumnModel that = (ColumnModel) o; + Column that = (Column) o; return Objects.equals(name, that.name); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DefaultSqlTypeMapping.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/DefaultSqlTypeMapping.java similarity index 85% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DefaultSqlTypeMapping.java rename to spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/DefaultSqlTypeMapping.java index 9f32598f5a..b3eb3f5aac 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/DefaultSqlTypeMapping.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/DefaultSqlTypeMapping.java @@ -13,15 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.relational.core.mapping.schemasqlgeneration; +package org.springframework.data.relational.core.mapping.schema; import java.util.HashMap; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.util.ClassUtils; /** * Class that provides a default implementation of mapping Java type to a Database type. To customize the mapping an - * instance of a class implementing {@link SqlTypeMapping} interface can be set on the {@link MappedTables} class + * instance of a class implementing {@link SqlTypeMapping} interface can be set on the {@link Tables} class * * @author Kurt Niemi * @since 3.2 @@ -42,6 +43,6 @@ public DefaultSqlTypeMapping() { @Override public String getColumnType(RelationalPersistentProperty property) { - return mapClassToDatabaseType.get(property.getActualType()); + return mapClassToDatabaseType.get(ClassUtils.resolvePrimitiveIfNecessary(property.getActualType())); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetWriter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/LiquibaseChangeSetWriter.java similarity index 58% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetWriter.java rename to spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/LiquibaseChangeSetWriter.java index 8afd43835c..ea38683f5e 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/LiquibaseChangeSetWriter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/LiquibaseChangeSetWriter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.data.relational.core.mapping.schemasqlgeneration; +package org.springframework.data.relational.core.mapping.schema; import liquibase.CatalogAndSchema; import liquibase.change.AddColumnConfig; @@ -38,8 +38,6 @@ import liquibase.snapshot.DatabaseSnapshot; import liquibase.snapshot.SnapshotControl; import liquibase.snapshot.SnapshotGeneratorFactory; -import liquibase.structure.core.Column; -import liquibase.structure.core.Table; import java.io.File; import java.io.FileOutputStream; @@ -53,12 +51,16 @@ import java.util.function.Predicate; import org.springframework.core.io.Resource; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.util.Predicates; import org.springframework.util.Assert; /** * Use this class to write Liquibase change sets. *

- * First create a {@link MappedTables} instance passing in a RelationalContext to have a model that represents the + * First create a {@link Tables} instance passing in a {@link MappingContext} to have a model that represents the * Table(s)/Column(s) that the code expects to exist. And then optionally create a Liquibase database object that points * to an existing database if one desires to create a changeset that could be applied to that database. If a database * object is not used, then the change set created would be something that could be applied to an empty database to make @@ -69,8 +71,10 @@ */ public class LiquibaseChangeSetWriter { - private final MappedTables sourceModel; - private final Database targetDatabase; + public static final String DEFAULT_AUTHOR = "Spring Data Relational"; + private final MappingContext, ? extends RelationalPersistentProperty> mappingContext; + + private SqlTypeMapping sqlTypeMapping = new DefaultSqlTypeMapping(); private ChangeLogSerializer changeLogSerializer = new YamlChangeLogSerializer(); @@ -79,14 +83,19 @@ public class LiquibaseChangeSetWriter { /** * Predicate to identify Liquibase system tables. */ - private final Predicate liquibaseTables = table -> table.toUpperCase(Locale.ROOT) + private final Predicate isLiquibaseTable = table -> table.toUpperCase(Locale.ROOT) .startsWith("DATABASECHANGELOG"); + /** + * Filter predicate to determine which persistent entities should be used for schema generation. + */ + public Predicate> schemaFilter = Predicates.isTrue(); + /** * Filter predicate used to determine whether an existing table should be removed. Defaults to {@code false} to keep * existing tables. */ - public Predicate dropTableFilter = table -> true; + public Predicate dropTableFilter = Predicates.isTrue(); /** * Filter predicate used to determine whether an existing column should be removed. Defaults to {@code false} to keep @@ -95,32 +104,34 @@ public class LiquibaseChangeSetWriter { public BiPredicate dropColumnFilter = (table, column) -> false; /** - * Use this to generate a ChangeSet that can be used on an empty database + * Use this to generate a ChangeSet that can be used on an empty database. * - * @param sourceModel - Model representing table(s)/column(s) as existing in code + * @param mappingContext source to determine persistent entities, must not be {@literal null}. */ - public LiquibaseChangeSetWriter(MappedTables sourceModel) { + public LiquibaseChangeSetWriter( + MappingContext, ? extends RelationalPersistentProperty> mappingContext) { - this.sourceModel = sourceModel; - this.targetDatabase = null; + Assert.notNull(mappingContext, "MappingContext must not be null"); + + this.mappingContext = mappingContext; } /** - * Use this to generate a ChangeSet against an existing database + * Configure SQL type mapping. Defaults to {@link DefaultSqlTypeMapping}. * - * @param sourceModel model representing table(s)/column(s) as existing in code. - * @param targetDatabase existing Liquibase database. + * @param sqlTypeMapping must not be {@literal null}. */ - public LiquibaseChangeSetWriter(MappedTables sourceModel, Database targetDatabase) { + public void setSqlTypeMapping(SqlTypeMapping sqlTypeMapping) { + + Assert.notNull(sqlTypeMapping, "SqlTypeMapping must not be null"); - this.sourceModel = sourceModel; - this.targetDatabase = targetDatabase; + this.sqlTypeMapping = sqlTypeMapping; } /** * Set the {@link ChangeLogSerializer}. * - * @param changeLogSerializer + * @param changeLogSerializer must not be {@literal null}. */ public void setChangeLogSerializer(ChangeLogSerializer changeLogSerializer) { @@ -132,7 +143,7 @@ public void setChangeLogSerializer(ChangeLogSerializer changeLogSerializer) { /** * Set the {@link ChangeLogParser}. * - * @param changeLogParser + * @param changeLogParser must not be {@literal null}. */ public void setChangeLogParser(ChangeLogParser changeLogParser) { @@ -141,6 +152,20 @@ public void setChangeLogParser(ChangeLogParser changeLogParser) { this.changeLogParser = changeLogParser; } + /** + * Set the filter predicate to identify for which tables to create schema definitions. Existing tables for excluded + * entities will show up in {@link #setDropTableFilter(Predicate)}. Returning {@code true} includes the entity; + * {@code false} excludes the entity from schema creation. + * + * @param schemaFilter must not be {@literal null}. + */ + public void setSchemaFilter(Predicate> schemaFilter) { + + Assert.notNull(schemaFilter, "Schema filter must not be null"); + + this.schemaFilter = schemaFilter; + } + /** * Set the filter predicate to identify tables to drop. The predicate accepts the table name. Returning {@code true} * will delete the table; {@code false} retains the table. @@ -168,48 +193,100 @@ public void setDropColumnFilter(BiPredicate dropColumnFilter) { } /** - * Write a Liquibase changeset. + * Write a Liquibase changeset containing all tables as initial changeset. + * + * @param changeLogResource resource that changeset will be written to (or append to an existing ChangeSet file). The + * resource must resolve to a valid {@link Resource#getFile()}. + * @throws IOException + */ + public void writeChangeSet(Resource changeLogResource) throws IOException { + writeChangeSet(changeLogResource, getChangeSetId(), DEFAULT_AUTHOR); + } + + /** + * Write a Liquibase changeset using a {@link Database} to identify the differences between mapped entities and the + * existing database. * * @param changeLogResource resource that changeset will be written to (or append to an existing ChangeSet file). The * resource must resolve to a valid {@link Resource#getFile()}. + * @param database database to identify the differences. * @throws LiquibaseException * @throws IOException */ - public void writeChangeSet(Resource changeLogResource) throws LiquibaseException, IOException { + public void writeChangeSet(Resource changeLogResource, Database database) throws IOException, LiquibaseException { + writeChangeSet(changeLogResource, getChangeSetId(), DEFAULT_AUTHOR, database); + } + + /** + * Write a Liquibase changeset containing all tables as initial changeset. + * + * @param changeLogResource resource that changeset will be written to (or append to an existing ChangeSet file). + * @param changeSetId unique value to identify the changeset. + * @param changeSetAuthor author information to be written to changeset file. + * @throws IOException + */ + public void writeChangeSet(Resource changeLogResource, String changeSetId, String changeSetAuthor) + throws IOException { - String changeSetId = Long.toString(System.currentTimeMillis()); - writeChangeSet(changeLogResource, changeSetId, "Spring Data Relational"); + DatabaseChangeLog databaseChangeLog = getDatabaseChangeLog(changeLogResource.getFile()); + ChangeSet changeSet = createChangeSet(changeSetId, changeSetAuthor, databaseChangeLog); + + writeChangeSet(databaseChangeLog, changeSet, changeLogResource.getFile()); } /** - * Write a Liquibase changeset. + * Write a Liquibase changeset using a {@link Database} to identify the differences between mapped entities and the + * existing database. * * @param changeLogResource resource that changeset will be written to (or append to an existing ChangeSet file). * @param changeSetId unique value to identify the changeset. * @param changeSetAuthor author information to be written to changeset file. + * @param database database to identify the differences. * @throws LiquibaseException * @throws IOException */ - public void writeChangeSet(Resource changeLogResource, String changeSetId, String changeSetAuthor) + public void writeChangeSet(Resource changeLogResource, String changeSetId, String changeSetAuthor, Database database) throws LiquibaseException, IOException { - SchemaDiff difference; + DatabaseChangeLog databaseChangeLog = getDatabaseChangeLog(changeLogResource.getFile()); + ChangeSet changeSet = createChangeSet(changeSetId, changeSetAuthor, database, databaseChangeLog); - if (targetDatabase != null) { - MappedTables liquibaseModel = getLiquibaseModel(targetDatabase); - difference = new SchemaDiff(sourceModel, liquibaseModel); - } else { - difference = new SchemaDiff(sourceModel, new MappedTables()); - } + writeChangeSet(databaseChangeLog, changeSet, changeLogResource.getFile()); + } + + protected ChangeSet createChangeSet(String changeSetId, String changeSetAuthor, DatabaseChangeLog databaseChangeLog) { + return createChangeSet(changeSetId, changeSetAuthor, createInitialDifference(), databaseChangeLog); + } + + protected ChangeSet createChangeSet(String changeSetId, String changeSetAuthor, Database database, + DatabaseChangeLog databaseChangeLog) throws LiquibaseException { + return createChangeSet(changeSetId, changeSetAuthor, createSchemaDifference(database), databaseChangeLog); + } + + private ChangeSet createChangeSet(String changeSetId, String changeSetAuthor, SchemaDiff difference, + DatabaseChangeLog databaseChangeLog) { - DatabaseChangeLog databaseChangeLog = getDatabaseChangeLog(changeLogResource.getFile()); ChangeSet changeSet = new ChangeSet(changeSetId, changeSetAuthor, false, false, "", "", "", databaseChangeLog); generateTableAdditionsDeletions(changeSet, difference); generateTableModifications(changeSet, difference); + return changeSet; + } - // File changeLogFile = new File(changeLogFilePath); - writeChangeSet(databaseChangeLog, changeSet, changeLogResource.getFile()); + private SchemaDiff createInitialDifference() { + + Tables mappedEntities = Tables.from(mappingContext.getPersistentEntities().stream().filter(schemaFilter), + sqlTypeMapping, null); + return SchemaDiff.diff(mappedEntities, Tables.empty()); + } + + private SchemaDiff createSchemaDifference(Database database) throws LiquibaseException { + + Tables existingTables = getLiquibaseModel(database); + Tables mappedEntities = Tables.from(mappingContext.getPersistentEntities().stream().filter(schemaFilter), + sqlTypeMapping, database.getDefaultCatalogName()); + + return SchemaDiff.diff(mappedEntities, existingTables); } private DatabaseChangeLog getDatabaseChangeLog(File changeLogFile) { @@ -217,10 +294,12 @@ private DatabaseChangeLog getDatabaseChangeLog(File changeLogFile) { DatabaseChangeLog databaseChangeLog; try { + File parentDirectory = changeLogFile.getParentFile(); if (parentDirectory == null) { parentDirectory = new File("./"); } + DirectoryResourceAccessor resourceAccessor = new DirectoryResourceAccessor(parentDirectory); ChangeLogParameters parameters = new ChangeLogParameters(); databaseChangeLog = changeLogParser.parse(changeLogFile.getName(), parameters, resourceAccessor); @@ -233,12 +312,12 @@ private DatabaseChangeLog getDatabaseChangeLog(File changeLogFile) { private void generateTableAdditionsDeletions(ChangeSet changeSet, SchemaDiff difference) { - for (TableModel table : difference.getTableAdditions()) { + for (Table table : difference.tableAdditions()) { CreateTableChange newTable = changeTable(table); changeSet.addChange(newTable); } - for (TableModel table : difference.getTableDeletions()) { + for (Table table : difference.tableDeletions()) { // Do not delete/drop table if it is an external application table if (dropTableFilter.test(table.name())) { changeSet.addChange(dropTable(table)); @@ -248,13 +327,13 @@ private void generateTableAdditionsDeletions(ChangeSet changeSet, SchemaDiff dif private void generateTableModifications(ChangeSet changeSet, SchemaDiff difference) { - for (TableDiff table : difference.getTableDiff()) { + for (TableDiff table : difference.tableDiffs()) { if (!table.columnsToAdd().isEmpty()) { changeSet.addChange(addColumns(table)); } - List deletedColumns = getColumnsToDrop(table); + List deletedColumns = getColumnsToDrop(table); if (deletedColumns.size() > 0) { changeSet.addChange(dropColumns(table, deletedColumns)); @@ -262,13 +341,13 @@ private void generateTableModifications(ChangeSet changeSet, SchemaDiff differen } } - private List getColumnsToDrop(TableDiff table) { + private List getColumnsToDrop(TableDiff table) { - List deletedColumns = new ArrayList<>(); - for (ColumnModel columnModel : table.columnsToDrop()) { + List deletedColumns = new ArrayList<>(); + for (Column column : table.columnsToDrop()) { - if (dropColumnFilter.test(table.table().name(), columnModel.name())) { - deletedColumns.add(columnModel); + if (dropColumnFilter.test(table.table().name(), column.name())) { + deletedColumns.add(column); } } return deletedColumns; @@ -285,63 +364,44 @@ private void writeChangeSet(DatabaseChangeLog databaseChangeLog, ChangeSet chang } } - private MappedTables getLiquibaseModel(Database targetDatabase) throws LiquibaseException { - - MappedTables liquibaseModel = new MappedTables(); + private Tables getLiquibaseModel(Database targetDatabase) throws LiquibaseException { CatalogAndSchema[] schemas = new CatalogAndSchema[] { targetDatabase.getDefaultSchema() }; SnapshotControl snapshotControl = new SnapshotControl(targetDatabase); DatabaseSnapshot snapshot = SnapshotGeneratorFactory.getInstance().createSnapshot(schemas, targetDatabase, snapshotControl); - Set

tables = snapshot.get(liquibase.structure.core.Table.class); - List processed = associateTablesWithSchema(sourceModel.getTableData(), targetDatabase); - - sourceModel.getTableData().clear(); - sourceModel.getTableData().addAll(processed); + Set tables = snapshot.get(liquibase.structure.core.Table.class); + List
existingTables = new ArrayList<>(tables.size()); for (liquibase.structure.core.Table table : tables) { // Exclude internal Liquibase tables from comparison - if (liquibaseTables.test(table.getName())) { + if (isLiquibaseTable.test(table.getName())) { continue; } - TableModel tableModel = new TableModel(table.getSchema().getCatalogName(), table.getName()); + Table tableModel = new Table(table.getSchema().getCatalogName(), table.getName()); - List columns = table.getColumns(); + List columns = table.getColumns(); for (liquibase.structure.core.Column column : columns) { String type = column.getType().toString(); boolean nullable = column.isNullable(); - ColumnModel columnModel = new ColumnModel(column.getName(), type, nullable, false); + Column columnModel = new Column(column.getName(), type, nullable, false); tableModel.columns().add(columnModel); } - liquibaseModel.getTableData().add(tableModel); + existingTables.add(tableModel); } - return liquibaseModel; + return new Tables(existingTables); } - private List associateTablesWithSchema(List tables, Database targetDatabase) { - - List processed = new ArrayList<>(tables.size()); - - for (TableModel currentModel : tables) { - - if (currentModel.schema() == null || currentModel.schema().isEmpty()) { - TableModel newModel = new TableModel(targetDatabase.getDefaultSchema().getCatalogName(), currentModel.name(), - currentModel.columns(), currentModel.keyColumns()); - processed.add(newModel); - } else { - processed.add(currentModel); - } - } - - return processed; + private static String getChangeSetId() { + return Long.toString(System.currentTimeMillis()); } private static AddColumnChange addColumns(TableDiff table) { @@ -350,27 +410,27 @@ private static AddColumnChange addColumns(TableDiff table) { addColumnChange.setSchemaName(table.table().schema()); addColumnChange.setTableName(table.table().name()); - for (ColumnModel column : table.columnsToAdd()) { + for (Column column : table.columnsToAdd()) { AddColumnConfig addColumn = createAddColumnChange(column); addColumnChange.addColumn(addColumn); } return addColumnChange; } - private static AddColumnConfig createAddColumnChange(ColumnModel column) { + private static AddColumnConfig createAddColumnChange(Column column) { AddColumnConfig config = new AddColumnConfig(); config.setName(column.name()); config.setType(column.type()); - if (column.identityColumn()) { + if (column.identity()) { config.setAutoIncrement(true); } return config; } - private static DropColumnChange dropColumns(TableDiff table, Collection deletedColumns) { + private static DropColumnChange dropColumns(TableDiff table, Collection deletedColumns) { DropColumnChange dropColumnChange = new DropColumnChange(); dropColumnChange.setSchemaName(table.table().schema()); @@ -378,7 +438,7 @@ private static DropColumnChange dropColumns(TableDiff table, Collection dropColumns = new ArrayList<>(); - for (ColumnModel column : deletedColumns) { + for (Column column : deletedColumns) { ColumnConfig config = new ColumnConfig(); config.setName(column.name()); dropColumns.add(config); @@ -388,18 +448,18 @@ private static DropColumnChange dropColumns(TableDiff table, Collection tableAdditions, List
tableDeletions, List tableDiffs) { + + public static SchemaDiff diff(Tables mappedEntities, Tables existingTables) { + + Set
existingIndex = new HashSet<>(existingTables.tables()); + Set
mappedIndex = new HashSet<>(mappedEntities.tables()); + + List
toCreate = getTablesToCreate(mappedEntities, existingIndex::contains); + List
toDrop = getTablesToDrop(existingTables, mappedIndex::contains); + + List tableDiffs = diffTable(mappedEntities, existingTables, existingIndex::contains); + + return new SchemaDiff(toCreate, toDrop, tableDiffs); + } + + private static List
getTablesToCreate(Tables mappedEntities, Predicate
excludeTable) { + + List
toCreate = new ArrayList<>(mappedEntities.tables().size()); + + for (Table table : mappedEntities.tables()) { + if (!excludeTable.test(table)) { + toCreate.add(table); + } + } + + return toCreate; + } + + private static List
getTablesToDrop(Tables existingTables, Predicate
excludeTable) { + + List
toDrop = new ArrayList<>(existingTables.tables().size()); + + for (Table table : existingTables.tables()) { + if (!excludeTable.test(table)) { + toDrop.add(table); + } + } + + return toDrop; + } + + private static List diffTable(Tables mappedEntities, Tables existingTables, + Predicate
includeTable) { + + List tableDiffs = new ArrayList<>(); + Map existingIndex = new HashMap<>(existingTables.tables().size()); + existingTables.tables().forEach(it -> existingIndex.put(it, it)); + + for (Table mappedEntity : mappedEntities.tables()) { + + if (!includeTable.test(mappedEntity)) { + continue; + } + + // TODO: How to handle changed columns (type?) + + Table existingTable = existingIndex.get(mappedEntity); + TableDiff tableDiff = new TableDiff(mappedEntity); + + Set mappedColumns = new LinkedHashSet<>(mappedEntity.columns()); + Set existingColumns = new LinkedHashSet<>(existingTable.columns()); + + // Identify deleted columns + Set toDelete = new LinkedHashSet<>(existingColumns); + toDelete.removeAll(mappedColumns); + + tableDiff.columnsToDrop().addAll(toDelete); + + // Identify added columns + Set addedColumns = new LinkedHashSet<>(mappedColumns); + addedColumns.removeAll(existingColumns); + tableDiff.columnsToAdd().addAll(addedColumns); + + tableDiffs.add(tableDiff); + } + + return tableDiffs; + } + +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SqlTypeMapping.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/SqlTypeMapping.java similarity index 93% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SqlTypeMapping.java rename to spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/SqlTypeMapping.java index 1f13ee4c78..6dda98d8bb 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SqlTypeMapping.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/SqlTypeMapping.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.relational.core.mapping.schemasqlgeneration; +package org.springframework.data.relational.core.mapping.schema; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Table.java similarity index 59% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java rename to spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Table.java index 8266e0172f..51220a4dfa 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableModel.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Table.java @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.relational.core.mapping.schemasqlgeneration; +package org.springframework.data.relational.core.mapping.schema; import java.util.ArrayList; import java.util.List; -import java.util.Objects; + +import org.springframework.util.ObjectUtils; /** * Models a Table for generating SQL for Schema generation. @@ -25,13 +26,13 @@ * @author Kurt Niemi * @since 3.2 */ -record TableModel(String schema, String name, List columns, List keyColumns) { +record Table(String schema, String name, List keyColumns, List columns) { - public TableModel(String schema, String name) { + public Table(String schema, String name) { this(schema, name, new ArrayList<>(), new ArrayList<>()); } - public TableModel(String name) { + public Table(String name) { this(null, name); } @@ -46,22 +47,18 @@ public boolean equals(Object o) { return false; } - TableModel that = (TableModel) o; - - // If we are missing the schema for either TableModel we will not treat that as being different - if (schema != null && that.schema != null && !schema.isEmpty() && !that.schema.isEmpty()) { - if (!Objects.equals(schema, that.schema)) { - return false; - } - } - if (!name.equalsIgnoreCase(that.name)) { - return false; - } - return true; + Table table = (Table) o; + return ObjectUtils.nullSafeEquals(schema, table.schema) && ObjectUtils.nullSafeEquals(name, table.name); } @Override public int hashCode() { - return Objects.hash(schema, name.toUpperCase()); + + int result = 17; + + result += ObjectUtils.nullSafeHashCode(this.schema); + result += ObjectUtils.nullSafeHashCode(this.name); + + return result; } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/TableDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/TableDiff.java new file mode 100644 index 0000000000..b15b4c64d9 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/TableDiff.java @@ -0,0 +1,19 @@ +package org.springframework.data.relational.core.mapping.schema; + +import java.util.ArrayList; +import java.util.List; + +/** + * Used to keep track of columns that should be added or deleted, when performing a difference between a source and + * target {@link Tables}. + * + * @author Kurt Niemi + * @since 3.2 + */ +record TableDiff(Table table, List columnsToAdd, List columnsToDrop) { + + public TableDiff(Table table) { + this(table, new ArrayList<>(), new ArrayList<>()); + } + +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Tables.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Tables.java new file mode 100644 index 0000000000..e4c2844969 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Tables.java @@ -0,0 +1,76 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping.schema; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; + +/** + * Model class that contains Table/Column information that can be used to generate SQL for Schema generation. + * + * @author Kurt Niemi + * @since 3.2 + */ +record Tables(List
tables) { + + public static Tables from(RelationalMappingContext context) { + return from(context.getPersistentEntities().stream(), new DefaultSqlTypeMapping(), null); + } + + // TODO: Add support (i.e. create tickets) to support mapped collections, entities, embedded properties, and aggregate + // references. + + public static Tables from(Stream> persistentEntities, + SqlTypeMapping sqlTypeMapping, String defaultSchema) { + + List
tables = persistentEntities + .filter(it -> it.isAnnotationPresent(org.springframework.data.relational.core.mapping.Table.class)) // + .map(entity -> { + + Table table = new Table(defaultSchema, entity.getTableName().getReference()); + + Set identifierColumns = new LinkedHashSet<>(); + entity.getPersistentProperties(Id.class).forEach(identifierColumns::add); + + for (RelationalPersistentProperty property : entity) { + + if (property.isEntity() && !property.isEmbedded()) { + continue; + } + + Column column = new Column(property.getColumnName().getReference(), sqlTypeMapping.getColumnType(property), + true, identifierColumns.contains(property)); + table.columns().add(column); + } + return table; + }).collect(Collectors.toList()); + + return new Tables(tables); + } + + public static Tables empty() { + return new Tables(Collections.emptyList()); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/package-info.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/package-info.java new file mode 100644 index 0000000000..0a404f1247 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/package-info.java @@ -0,0 +1,7 @@ +/** + * Schema creation and schema update integration with Liquibase. + */ +@NonNullApi +package org.springframework.data.relational.core.mapping.schema; + +import org.springframework.lang.NonNullApi; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTables.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTables.java deleted file mode 100644 index fd1ab6a100..0000000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTables.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.core.mapping.schemasqlgeneration; - -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import org.springframework.data.annotation.Id; -import org.springframework.data.relational.core.mapping.RelationalMappingContext; -import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; -import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; - -/** - * Model class that contains Table/Column information that can be used to generate SQL for Schema generation. - * - * @author Kurt Niemi - * @since 3.2 - */ -public class MappedTables { - - private final List tableData = new ArrayList<>(); - public final SqlTypeMapping sqlTypeMapping; - - public MappedTables() { - this.sqlTypeMapping = new DefaultSqlTypeMapping(); - } - - /** - * Create model from a RelationalMappingContext - */ - public MappedTables(RelationalMappingContext context) { - this.sqlTypeMapping = createTypeMapping(context); - } - - // TODO: Add support (i.e. create tickets) to support mapped collections, entities, embedded properties, and aggregate - // references. - private SqlTypeMapping createTypeMapping(RelationalMappingContext context) { - - SqlTypeMapping sqlTypeMapping = new DefaultSqlTypeMapping(); - - for (RelationalPersistentEntity entity : context.getPersistentEntities()) { - - TableModel tableModel = new TableModel(entity.getTableName().getReference()); - - Set identifierColumns = new LinkedHashSet<>(); - entity.getPersistentProperties(Id.class).forEach(identifierColumns::add); - - for (RelationalPersistentProperty property : entity) { - - if (property.isEntity() && !property.isEmbedded()) { - continue; - } - - ColumnModel columnModel = new ColumnModel(property.getColumnName().getReference(), - sqlTypeMapping.getColumnType(property), true, identifierColumns.contains(property)); - tableModel.columns().add(columnModel); - } - - tableData.add(tableModel); - } - - return sqlTypeMapping; - } - - List getTableData() { - return tableData; - } -} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java deleted file mode 100644 index d125b87d12..0000000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/SchemaDiff.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.springframework.data.relational.core.mapping.schemasqlgeneration; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * This class is created to return the difference between a source and target {@link MappedTables} The difference - * consists of Table Additions, Deletions, and Modified Tables (i.e. table exists in both source and target - but has - * columns to add or delete) - * - * @author Kurt Niemi - * @since 3.2 - */ -class SchemaDiff { - - private final MappedTables source; - private final MappedTables target; - - private final List tableAdditions = new ArrayList<>(); - private final List tableDeletions = new ArrayList<>(); - private final List tableDiffs = new ArrayList<>(); - - /** - * Compare two {@link MappedTables} to identify differences. - * - * @param target model reflecting current database state. - * @param source model reflecting desired database state. - */ - public SchemaDiff(MappedTables target, MappedTables source) { - - this.source = source; - this.target = target; - - diffTableAdditionDeletion(); - diffTable(); - } - - public List getTableAdditions() { - return tableAdditions; - } - - public List getTableDeletions() { - return tableDeletions; - } - - public List getTableDiff() { - return tableDiffs; - } - - private void diffTableAdditionDeletion() { - - List sourceTableData = new ArrayList<>(source.getTableData()); - List targetTableData = new ArrayList<>(target.getTableData()); - - // Identify deleted tables - List deletedTables = new ArrayList<>(sourceTableData); - deletedTables.removeAll(targetTableData); - tableDeletions.addAll(deletedTables); - - // Identify added tables - List addedTables = new ArrayList<>(targetTableData); - addedTables.removeAll(sourceTableData); - tableAdditions.addAll(addedTables); - } - - private void diffTable() { - - Map sourceTablesMap = new LinkedHashMap<>(); - for (TableModel table : source.getTableData()) { - sourceTablesMap.put(table.schema() + "." + table.name(), table); - } - - Set existingTables = new LinkedHashSet<>(target.getTableData()); - getTableAdditions().forEach(existingTables::remove); - - for (TableModel table : existingTables) { - TableDiff tableDiff = new TableDiff(table); - tableDiffs.add(tableDiff); - - TableModel sourceTable = sourceTablesMap.get(table.schema() + "." + table.name()); - - Set sourceTableData = new LinkedHashSet<>(sourceTable.columns()); - Set targetTableData = new LinkedHashSet<>(table.columns()); - - // Identify deleted columns - Set deletedColumns = new LinkedHashSet<>(sourceTableData); - deletedColumns.removeAll(targetTableData); - - tableDiff.columnsToDrop().addAll(deletedColumns); - - // Identify added columns - Set addedColumns = new LinkedHashSet<>(targetTableData); - addedColumns.removeAll(sourceTableData); - tableDiff.columnsToAdd().addAll(addedColumns); - } - } -} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java deleted file mode 100644 index 648f813ad8..0000000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/TableDiff.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.springframework.data.relational.core.mapping.schemasqlgeneration; - -import java.util.ArrayList; -import java.util.List; - -/** - * Used to keep track of columns that should be added or deleted, when performing a difference between a source and - * target {@link MappedTables}. - * - * @author Kurt Niemi - * @since 3.2 - */ -record TableDiff(TableModel table, List columnsToAdd, List columnsToDrop) { - - public TableDiff(TableModel tableModel) { - this(tableModel, new ArrayList<>(), new ArrayList<>()); - } - -} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schema/LiquibaseChangeSetWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schema/LiquibaseChangeSetWriterUnitTests.java new file mode 100644 index 0000000000..db6e74d834 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schema/LiquibaseChangeSetWriterUnitTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping.schema; + +import static org.assertj.core.api.Assertions.*; + +import liquibase.change.ColumnConfig; +import liquibase.change.core.CreateTableChange; +import liquibase.changelog.ChangeSet; +import liquibase.changelog.DatabaseChangeLog; +import liquibase.exception.LiquibaseException; + +import org.junit.jupiter.api.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; + +/** + * Unit tests for {@link LiquibaseChangeSetWriter}. + * + * @author Mark Paluch + */ +class LiquibaseChangeSetWriterUnitTests { + + @Test + void newTableShouldCreateChangeSet() throws LiquibaseException { + + RelationalMappingContext context = new RelationalMappingContext(); + context.getRequiredPersistentEntity(VariousTypes.class); + + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context); + + ChangeSet changeSet = writer.createChangeSet("", "", new DatabaseChangeLog()); + + CreateTableChange createTable = (CreateTableChange) changeSet.getChanges().get(0); + + assertThat(createTable.getColumns()).extracting(ColumnConfig::getName).containsSequence("id", + "luke_i_am_your_father", "dark_side", "floater"); + assertThat(createTable.getColumns()).extracting(ColumnConfig::getType).containsSequence("BIGINT", + "VARCHAR(255 BYTE)", "TINYINT", "FLOAT"); + } + + @org.springframework.data.relational.core.mapping.Table + static class VariousTypes { + @Id long id; + String lukeIAmYourFather; + Boolean darkSide; + Float floater; + Double doubleClass; + Integer integerClass; + } + +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schema/SchemaDiffUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schema/SchemaDiffUnitTests.java new file mode 100644 index 0000000000..521adb6126 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schema/SchemaDiffUnitTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping.schema; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; + +/** + * Unit tests for the {@link Tables}. + * + * @author Kurt Niemi + * @author Mark Paluch + */ +class SchemaDiffUnitTests { + + @Test + void testDiffSchema() { + + RelationalMappingContext context = new RelationalMappingContext(); + context.getRequiredPersistentEntity(SchemaDiffUnitTests.Table1.class); + context.getRequiredPersistentEntity(SchemaDiffUnitTests.Table2.class); + + Tables mappedEntities = Tables.from(context); + Tables existingTables = Tables.from(context); + + // Table table1 does not exist on the database yet. + existingTables.tables().remove(new Table("table1")); + + // Add column to table2 + Column newColumn = new Column("newcol", "VARCHAR(255)"); + Table table2 = mappedEntities.tables().get(mappedEntities.tables().indexOf(new Table("table2"))); + table2.columns().add(newColumn); + + // This should be deleted + Table delete_me = new Table(null, "delete_me"); + delete_me.columns().add(newColumn); + existingTables.tables().add(delete_me); + + SchemaDiff diff = SchemaDiff.diff(mappedEntities, existingTables); + + // Verify that newtable is an added table in the diff + assertThat(diff.tableAdditions()).isNotEmpty(); + assertThat(diff.tableAdditions()).extracting(Table::name).containsOnly("table1"); + + assertThat(diff.tableDeletions()).isNotEmpty(); + assertThat(diff.tableDeletions()).extracting(Table::name).containsOnly("delete_me"); + + assertThat(diff.tableDiffs()).hasSize(1); + assertThat(diff.tableDiffs()).extracting(it -> it.table().name()).containsOnly("table2"); + assertThat(diff.tableDiffs().get(0).columnsToAdd()).contains(newColumn); + assertThat(diff.tableDiffs().get(0).columnsToDrop()).isEmpty(); + } + + // Test table classes for performing schema diff + @org.springframework.data.relational.core.mapping.Table + static class Table1 { + String force; + String be; + String with; + String you; + } + + @org.springframework.data.relational.core.mapping.Table + static class Table2 { + String lukeIAmYourFather; + Boolean darkSide; + Float floater; + Double doubleClass; + Integer integerClass; + } + +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTablesUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTablesUnitTests.java deleted file mode 100644 index 78348825de..0000000000 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schemasqlgeneration/MappedTablesUnitTests.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.core.mapping.schemasqlgeneration; - -import static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.Test; -import org.springframework.data.relational.core.mapping.RelationalMappingContext; -import org.springframework.data.relational.core.mapping.Table; - -/** - * Unit tests for the {@link MappedTables}. - * - * @author Kurt Niemi - */ -class MappedTablesUnitTests { - - @Test - void testDiffSchema() { - - RelationalMappingContext context = new RelationalMappingContext(); - context.getRequiredPersistentEntity(MappedTablesUnitTests.Table1.class); - context.getRequiredPersistentEntity(MappedTablesUnitTests.Table2.class); - - MappedTables target = new MappedTables(context); - MappedTables source = new MappedTables(context); - - // Add column to table - ColumnModel newColumn = new ColumnModel("newcol", "VARCHAR(255)"); - source.getTableData().get(0).columns().add(newColumn); - - // Remove table - source.getTableData().remove(1); - - // Add new table - TableModel newTable = new TableModel(null, "newtable"); - newTable.columns().add(newColumn); - source.getTableData().add(newTable); - - SchemaDiff diff = new SchemaDiff(target, source); - - // Verify that newtable is an added table in the diff - assertThat(diff.getTableAdditions()).isNotEmpty(); - assertThat(diff.getTableAdditions().get(0).name()).isEqualTo("table1"); - - assertThat(diff.getTableDeletions()).isNotEmpty(); - assertThat(diff.getTableDeletions().get(0).name()).isEqualTo("newtable"); - - assertThat(diff.getTableDiff()).isNotEmpty(); - assertThat(diff.getTableDiff().get(0).columnsToAdd()).isEmpty(); - assertThat(diff.getTableDiff().get(0).columnsToDrop()).isNotEmpty(); - } - - // Test table classes for performing schema diff - @Table - static class Table1 { - String force; - String be; - String with; - String you; - } - - @Table - static class Table2 { - String lukeIAmYourFather; - Boolean darkSide; - Float floater; - Double doubleClass; - Integer integerClass; - } - -} From e85ab139687e808d9e88fa4a9c6f23334556faff Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 5 Jun 2023 15:25:39 +0200 Subject: [PATCH 31/32] Polishing. Move code to JDBC module. Introduce comparator strategy to customize how table and column names are compared. --- .../jdbc}/core/mapping/schema/Column.java | 2 +- .../mapping/schema/DefaultSqlTypeMapping.java | 36 ++- .../schema/LiquibaseChangeSetWriter.java | 235 ++++++++++++----- .../jdbc/core/mapping/schema/SchemaDiff.java | 147 +++++++++++ .../core/mapping/schema/SqlTypeMapping.java | 101 +++++++ .../data/jdbc}/core/mapping/schema/Table.java | 7 +- .../jdbc/core/mapping/schema/TableDiff.java | 21 +- .../jdbc}/core/mapping/schema/Tables.java | 9 +- .../core/mapping/schema/package-info.java | 2 +- ...uibaseChangeSetWriterIntegrationTests.java | 249 ++++++++++++++++++ .../LiquibaseChangeSetWriterUnitTests.java | 36 ++- .../mapping/schema/SchemaDiffUnitTests.java | 7 +- .../schema/SqlTypeMappingUnitTests.java | 68 +++++ .../jdbc/core/mapping/schema/changelog.yml | 16 ++ .../schema/person-with-id-and-name.sql | 5 + .../jdbc/core/mapping/schema/unused-table.sql | 4 + .../core/mapping/schema/SchemaDiff.java | 99 ------- .../core/mapping/schema/TableDiff.java | 19 -- 18 files changed, 849 insertions(+), 214 deletions(-) rename {spring-data-relational/src/main/java/org/springframework/data/relational => spring-data-jdbc/src/main/java/org/springframework/data/jdbc}/core/mapping/schema/Column.java (95%) rename {spring-data-relational/src/main/java/org/springframework/data/relational => spring-data-jdbc/src/main/java/org/springframework/data/jdbc}/core/mapping/schema/DefaultSqlTypeMapping.java (56%) rename {spring-data-relational/src/main/java/org/springframework/data/relational => spring-data-jdbc/src/main/java/org/springframework/data/jdbc}/core/mapping/schema/LiquibaseChangeSetWriter.java (64%) create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/SchemaDiff.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/SqlTypeMapping.java rename {spring-data-relational/src/main/java/org/springframework/data/relational => spring-data-jdbc/src/main/java/org/springframework/data/jdbc}/core/mapping/schema/Table.java (84%) rename spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/SqlTypeMapping.java => spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/TableDiff.java (54%) rename {spring-data-relational/src/main/java/org/springframework/data/relational => spring-data-jdbc/src/main/java/org/springframework/data/jdbc}/core/mapping/schema/Tables.java (88%) rename {spring-data-relational/src/main/java/org/springframework/data/relational => spring-data-jdbc/src/main/java/org/springframework/data/jdbc}/core/mapping/schema/package-info.java (66%) create mode 100644 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriterIntegrationTests.java rename {spring-data-relational/src/test/java/org/springframework/data/relational => spring-data-jdbc/src/test/java/org/springframework/data/jdbc}/core/mapping/schema/LiquibaseChangeSetWriterUnitTests.java (61%) rename {spring-data-relational/src/test/java/org/springframework/data/relational => spring-data-jdbc/src/test/java/org/springframework/data/jdbc}/core/mapping/schema/SchemaDiffUnitTests.java (94%) create mode 100644 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/SqlTypeMappingUnitTests.java create mode 100644 spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/changelog.yml create mode 100644 spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/person-with-id-and-name.sql create mode 100644 spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/unused-table.sql delete mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/SchemaDiff.java delete mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/TableDiff.java diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Column.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Column.java similarity index 95% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Column.java rename to spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Column.java index 8d728e62de..abf0f37949 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Column.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Column.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.relational.core.mapping.schema; +package org.springframework.data.jdbc.core.mapping.schema; import java.util.Objects; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/DefaultSqlTypeMapping.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/DefaultSqlTypeMapping.java similarity index 56% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/DefaultSqlTypeMapping.java rename to spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/DefaultSqlTypeMapping.java index b3eb3f5aac..526ad5a5fa 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/DefaultSqlTypeMapping.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/DefaultSqlTypeMapping.java @@ -13,9 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.relational.core.mapping.schema; +package org.springframework.data.jdbc.core.mapping.schema; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZonedDateTime; import java.util.HashMap; +import java.util.UUID; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.util.ClassUtils; @@ -29,20 +36,31 @@ */ public class DefaultSqlTypeMapping implements SqlTypeMapping { - private final HashMap, String> mapClassToDatabaseType = new HashMap<>(); + private final HashMap, String> typeMap = new HashMap<>(); public DefaultSqlTypeMapping() { - mapClassToDatabaseType.put(String.class, "VARCHAR(255 BYTE)"); - mapClassToDatabaseType.put(Boolean.class, "TINYINT"); - mapClassToDatabaseType.put(Double.class, "DOUBLE"); - mapClassToDatabaseType.put(Float.class, "FLOAT"); - mapClassToDatabaseType.put(Integer.class, "INT"); - mapClassToDatabaseType.put(Long.class, "BIGINT"); + typeMap.put(String.class, "VARCHAR(255 BYTE)"); + typeMap.put(Boolean.class, "TINYINT"); + typeMap.put(Double.class, "DOUBLE"); + typeMap.put(Float.class, "FLOAT"); + typeMap.put(Integer.class, "INT"); + typeMap.put(Long.class, "BIGINT"); + + typeMap.put(BigInteger.class, "BIGINT"); + typeMap.put(BigDecimal.class, "NUMERIC"); + + typeMap.put(UUID.class, "UUID"); + + typeMap.put(LocalDate.class, "DATE"); + typeMap.put(LocalTime.class, "TIME"); + typeMap.put(LocalDateTime.class, "TIMESTAMP"); + + typeMap.put(ZonedDateTime.class, "TIMESTAMPTZ"); } @Override public String getColumnType(RelationalPersistentProperty property) { - return mapClassToDatabaseType.get(ClassUtils.resolvePrimitiveIfNecessary(property.getActualType())); + return typeMap.get(ClassUtils.resolvePrimitiveIfNecessary(property.getActualType())); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/LiquibaseChangeSetWriter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriter.java similarity index 64% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/LiquibaseChangeSetWriter.java rename to spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriter.java index ea38683f5e..b935127547 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/LiquibaseChangeSetWriter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.data.relational.core.mapping.schema; +package org.springframework.data.jdbc.core.mapping.schema; import liquibase.CatalogAndSchema; import liquibase.change.AddColumnConfig; @@ -29,6 +29,7 @@ import liquibase.changelog.ChangeSet; import liquibase.changelog.DatabaseChangeLog; import liquibase.database.Database; +import liquibase.exception.ChangeLogParseException; import liquibase.exception.LiquibaseException; import liquibase.parser.ChangeLogParser; import liquibase.parser.core.yaml.YamlChangeLogParser; @@ -42,8 +43,10 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.text.Collator; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Set; @@ -55,18 +58,35 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.util.Predicates; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * Use this class to write Liquibase change sets. + * Use this class to write Liquibase ChangeSets. *

- * First create a {@link Tables} instance passing in a {@link MappingContext} to have a model that represents the - * Table(s)/Column(s) that the code expects to exist. And then optionally create a Liquibase database object that points - * to an existing database if one desires to create a changeset that could be applied to that database. If a database - * object is not used, then the change set created would be something that could be applied to an empty database to make - * it match the state of the code. Prior to applying the changeset one should review and make adjustments appropriately. + * This writer uses {@link MappingContext} as input to determine mapped entities. Entities can be filtered through a + * {@link #setSchemaFilter(Predicate) schema filter} to include/exclude entities. By default, all entities within the + * mapping context are considered for computing the expected schema. + *

+ * This writer operates in two modes: + *

    + *
  • Initial Schema Creation
  • + *
  • Differential Schema Change Creation
  • + *
+ * The {@link #writeChangeSet(Resource) initial mode} allows creating the full schema without considering any existing + * tables. The {@link #writeChangeSet(Resource, Database) differential schema mode} uses a {@link Database} object to + * determine existing tables and columns. It creates in addition to table creations also changes to drop tables, drop + * columns and add columns. By default, the {@link #setDropTableFilter(Predicate) DROP TABLE} and the + * {@link #setDropColumnFilter(BiPredicate) DROP COLUMN} filters exclude all tables respective columns from being + * dropped. + *

+ * In differential schema mode, table and column names are compared using a case-insensitive comparator, see + * {@link Collator#PRIMARY}. + *

+ * The writer can be configured to use specific ChangeLogSerializers and ChangeLogParsers defaulting to YAML. * * @author Kurt Niemi + * @author Mark Paluch * @since 3.2 */ public class LiquibaseChangeSetWriter { @@ -86,22 +106,35 @@ public class LiquibaseChangeSetWriter { private final Predicate isLiquibaseTable = table -> table.toUpperCase(Locale.ROOT) .startsWith("DATABASECHANGELOG"); + /** + * Comparator to compare table and column names. + */ + private final Comparator nameComparator = createComparator(); + + private static Comparator createComparator() { + + Collator instance = Collator.getInstance(Locale.ROOT); + instance.setStrength(Collator.PRIMARY); + + return instance::compare; + } + /** * Filter predicate to determine which persistent entities should be used for schema generation. */ - public Predicate> schemaFilter = Predicates.isTrue(); + private Predicate> schemaFilter = Predicates.isTrue(); /** * Filter predicate used to determine whether an existing table should be removed. Defaults to {@code false} to keep * existing tables. */ - public Predicate dropTableFilter = Predicates.isTrue(); + private Predicate dropTableFilter = Predicates.isFalse(); /** * Filter predicate used to determine whether an existing column should be removed. Defaults to {@code false} to keep * existing columns. */ - public BiPredicate dropColumnFilter = (table, column) -> false; + private BiPredicate dropColumnFilter = (table, column) -> false; /** * Use this to generate a ChangeSet that can be used on an empty database. @@ -153,7 +186,7 @@ public void setChangeLogParser(ChangeLogParser changeLogParser) { } /** - * Set the filter predicate to identify for which tables to create schema definitions. Existing tables for excluded + * Set the filter predicate to identify for which entities to create schema definitions. Existing tables for excluded * entities will show up in {@link #setDropTableFilter(Predicate)}. Returning {@code true} includes the entity; * {@code false} excludes the entity from schema creation. * @@ -193,105 +226,125 @@ public void setDropColumnFilter(BiPredicate dropColumnFilter) { } /** - * Write a Liquibase changeset containing all tables as initial changeset. + * Write a Liquibase ChangeSet containing all tables as initial ChangeSet. * - * @param changeLogResource resource that changeset will be written to (or append to an existing ChangeSet file). The + * @param changeLogResource resource that ChangeSet will be written to (or append to an existing ChangeSet file). The * resource must resolve to a valid {@link Resource#getFile()}. - * @throws IOException + * @throws IOException in case of I/O errors. */ public void writeChangeSet(Resource changeLogResource) throws IOException { - writeChangeSet(changeLogResource, getChangeSetId(), DEFAULT_AUTHOR); + writeChangeSet(changeLogResource, ChangeSetMetadata.create()); } /** - * Write a Liquibase changeset using a {@link Database} to identify the differences between mapped entities and the + * Write a Liquibase ChangeSet using a {@link Database} to identify the differences between mapped entities and the * existing database. * - * @param changeLogResource resource that changeset will be written to (or append to an existing ChangeSet file). The + * @param changeLogResource resource that ChangeSet will be written to (or append to an existing ChangeSet file). The * resource must resolve to a valid {@link Resource#getFile()}. * @param database database to identify the differences. * @throws LiquibaseException - * @throws IOException + * @throws IOException in case of I/O errors. */ public void writeChangeSet(Resource changeLogResource, Database database) throws IOException, LiquibaseException { - writeChangeSet(changeLogResource, getChangeSetId(), DEFAULT_AUTHOR, database); + writeChangeSet(changeLogResource, ChangeSetMetadata.create(), database); } /** - * Write a Liquibase changeset containing all tables as initial changeset. + * Write a Liquibase ChangeSet containing all tables as initial ChangeSet. * - * @param changeLogResource resource that changeset will be written to (or append to an existing ChangeSet file). - * @param changeSetId unique value to identify the changeset. - * @param changeSetAuthor author information to be written to changeset file. - * @throws IOException + * @param changeLogResource resource that ChangeSet will be written to (or append to an existing ChangeSet file). + * @param metadata the ChangeSet metadata. + * @throws IOException in case of I/O errors. */ - public void writeChangeSet(Resource changeLogResource, String changeSetId, String changeSetAuthor) - throws IOException { + public void writeChangeSet(Resource changeLogResource, ChangeSetMetadata metadata) throws IOException { - DatabaseChangeLog databaseChangeLog = getDatabaseChangeLog(changeLogResource.getFile()); - ChangeSet changeSet = createChangeSet(changeSetId, changeSetAuthor, databaseChangeLog); + DatabaseChangeLog databaseChangeLog = getDatabaseChangeLog(changeLogResource.getFile(), null); + ChangeSet changeSet = createChangeSet(metadata, databaseChangeLog); writeChangeSet(databaseChangeLog, changeSet, changeLogResource.getFile()); } /** - * Write a Liquibase changeset using a {@link Database} to identify the differences between mapped entities and the + * Write a Liquibase ChangeSet using a {@link Database} to identify the differences between mapped entities and the * existing database. * - * @param changeLogResource resource that changeset will be written to (or append to an existing ChangeSet file). - * @param changeSetId unique value to identify the changeset. - * @param changeSetAuthor author information to be written to changeset file. + * @param changeLogResource resource that ChangeSet will be written to (or append to an existing ChangeSet file). + * @param metadata the ChangeSet metadata. * @param database database to identify the differences. * @throws LiquibaseException - * @throws IOException + * @throws IOException in case of I/O errors. */ - public void writeChangeSet(Resource changeLogResource, String changeSetId, String changeSetAuthor, Database database) + public void writeChangeSet(Resource changeLogResource, ChangeSetMetadata metadata, Database database) throws LiquibaseException, IOException { - DatabaseChangeLog databaseChangeLog = getDatabaseChangeLog(changeLogResource.getFile()); - ChangeSet changeSet = createChangeSet(changeSetId, changeSetAuthor, database, databaseChangeLog); + DatabaseChangeLog databaseChangeLog = getDatabaseChangeLog(changeLogResource.getFile(), database); + ChangeSet changeSet = createChangeSet(metadata, database, databaseChangeLog); writeChangeSet(databaseChangeLog, changeSet, changeLogResource.getFile()); } - protected ChangeSet createChangeSet(String changeSetId, String changeSetAuthor, DatabaseChangeLog databaseChangeLog) { - return createChangeSet(changeSetId, changeSetAuthor, createInitialDifference(), databaseChangeLog); + /** + * Creates an initial ChangeSet. + * + * @param metadata must not be {@literal null}. + * @param databaseChangeLog must not be {@literal null}. + * @return the initial ChangeSet. + */ + protected ChangeSet createChangeSet(ChangeSetMetadata metadata, DatabaseChangeLog databaseChangeLog) { + return createChangeSet(metadata, initial(), databaseChangeLog); } - protected ChangeSet createChangeSet(String changeSetId, String changeSetAuthor, Database database, + /** + * Creates a diff ChangeSet by comparing {@link Database} with {@link MappingContext mapped entities}. + * + * @param metadata must not be {@literal null}. + * @param databaseChangeLog must not be {@literal null}. + * @return the diff ChangeSet. + */ + protected ChangeSet createChangeSet(ChangeSetMetadata metadata, Database database, DatabaseChangeLog databaseChangeLog) throws LiquibaseException { - return createChangeSet(changeSetId, changeSetAuthor, createSchemaDifference(database), databaseChangeLog); + return createChangeSet(metadata, differenceOf(database), databaseChangeLog); } - private ChangeSet createChangeSet(String changeSetId, String changeSetAuthor, SchemaDiff difference, + private ChangeSet createChangeSet(ChangeSetMetadata metadata, SchemaDiff difference, DatabaseChangeLog databaseChangeLog) { - ChangeSet changeSet = new ChangeSet(changeSetId, changeSetAuthor, false, false, "", "", "", databaseChangeLog); + ChangeSet changeSet = new ChangeSet(metadata.getId(), metadata.getAuthor(), false, false, "", "", "", + databaseChangeLog); generateTableAdditionsDeletions(changeSet, difference); generateTableModifications(changeSet, difference); return changeSet; } - private SchemaDiff createInitialDifference() { + private SchemaDiff initial() { Tables mappedEntities = Tables.from(mappingContext.getPersistentEntities().stream().filter(schemaFilter), sqlTypeMapping, null); - return SchemaDiff.diff(mappedEntities, Tables.empty()); + return SchemaDiff.diff(mappedEntities, Tables.empty(), nameComparator); } - private SchemaDiff createSchemaDifference(Database database) throws LiquibaseException { + private SchemaDiff differenceOf(Database database) throws LiquibaseException { Tables existingTables = getLiquibaseModel(database); Tables mappedEntities = Tables.from(mappingContext.getPersistentEntities().stream().filter(schemaFilter), sqlTypeMapping, database.getDefaultCatalogName()); - return SchemaDiff.diff(mappedEntities, existingTables); + return SchemaDiff.diff(mappedEntities, existingTables, nameComparator); } - private DatabaseChangeLog getDatabaseChangeLog(File changeLogFile) { + private DatabaseChangeLog getDatabaseChangeLog(File changeLogFile, @Nullable Database database) throws IOException { + + ChangeLogParameters parameters = database != null ? new ChangeLogParameters(database) : new ChangeLogParameters(); - DatabaseChangeLog databaseChangeLog; + if (!changeLogFile.exists()) { + DatabaseChangeLog databaseChangeLog = new DatabaseChangeLog(changeLogFile.getName()); + if (database != null) { + databaseChangeLog.setChangeLogParameters(parameters); + } + return databaseChangeLog; + } try { @@ -301,13 +354,10 @@ private DatabaseChangeLog getDatabaseChangeLog(File changeLogFile) { } DirectoryResourceAccessor resourceAccessor = new DirectoryResourceAccessor(parentDirectory); - ChangeLogParameters parameters = new ChangeLogParameters(); - databaseChangeLog = changeLogParser.parse(changeLogFile.getName(), parameters, resourceAccessor); - } catch (Exception ex) { - databaseChangeLog = new DatabaseChangeLog(changeLogFile.getAbsolutePath()); + return changeLogParser.parse(changeLogFile.getName(), parameters, resourceAccessor); + } catch (ChangeLogParseException ex) { + throw new IOException(ex); } - - return databaseChangeLog; } private void generateTableAdditionsDeletions(ChangeSet changeSet, SchemaDiff difference) { @@ -335,7 +385,7 @@ private void generateTableModifications(ChangeSet changeSet, SchemaDiff differen List deletedColumns = getColumnsToDrop(table); - if (deletedColumns.size() > 0) { + if (!deletedColumns.isEmpty()) { changeSet.addChange(dropColumns(table, deletedColumns)); } } @@ -400,10 +450,6 @@ private Tables getLiquibaseModel(Database targetDatabase) throws LiquibaseExcept return new Tables(existingTables); } - private static String getChangeSetId() { - return Long.toString(System.currentTimeMillis()); - } - private static AddColumnChange addColumns(TableDiff table) { AddColumnChange addColumnChange = new AddColumnChange(); @@ -455,16 +501,21 @@ private static CreateTableChange changeTable(Table table) { change.setTableName(table.name()); for (Column column : table.columns()) { + ColumnConfig columnConfig = new ColumnConfig(); columnConfig.setName(column.name()); columnConfig.setType(column.type()); + ConstraintsConfig constraints = new ConstraintsConfig(); + constraints.setNullable(column.nullable()); + if (column.identity()) { + columnConfig.setAutoIncrement(true); - ConstraintsConfig constraints = new ConstraintsConfig(); constraints.setPrimaryKey(true); - columnConfig.setConstraints(constraints); } + + columnConfig.setConstraints(constraints); change.addColumn(columnConfig); } @@ -480,4 +531,66 @@ private static DropTableChange dropTable(Table table) { return change; } + + /** + * Metadata for a ChangeSet. + */ + interface ChangeSetMetadata { + + /** + * Creates a new default {@link ChangeSetMetadata} using the {@link #DEFAULT_AUTHOR default author}. + * + * @return a new default {@link ChangeSetMetadata} using the {@link #DEFAULT_AUTHOR default author}. + */ + static ChangeSetMetadata create() { + return ofAuthor(LiquibaseChangeSetWriter.DEFAULT_AUTHOR); + } + + /** + * Creates a new default {@link ChangeSetMetadata} using a generated {@code identifier} and provided {@code author}. + * + * @return a new default {@link ChangeSetMetadata} using a generated {@code identifier} and provided {@code author}. + */ + static ChangeSetMetadata ofAuthor(String author) { + return of(Long.toString(System.currentTimeMillis()), author); + } + + /** + * Creates a new default {@link ChangeSetMetadata} using the provided {@code identifier} and {@code author}. + * + * @return a new default {@link ChangeSetMetadata} using the provided {@code identifier} and {@code author}. + */ + static ChangeSetMetadata of(String identifier, String author) { + return new DefaultChangeSetMetadata(identifier, author); + } + + /** + * @return the ChangeSet identifier. + */ + String getId(); + + /** + * @return the ChangeSet author. + */ + String getAuthor(); + } + + private record DefaultChangeSetMetadata(String id, String author) implements ChangeSetMetadata { + + private DefaultChangeSetMetadata { + + Assert.hasText(id, "ChangeSet identifier must not be empty or null"); + Assert.hasText(author, "Author must not be empty or null"); + } + + @Override + public String getId() { + return id(); + } + + @Override + public String getAuthor() { + return author(); + } + } } 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 new file mode 100644 index 0000000000..079c40dde1 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/SchemaDiff.java @@ -0,0 +1,147 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.mapping.schema; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * This class is created to return the difference between a source and target {@link Tables} The difference consists of + * Table Additions, Deletions, and Modified Tables (i.e. table exists in both source and target - but has columns to add + * or delete) + * + * @author Kurt Niemi + * @since 3.2 + */ +record SchemaDiff(List

tableAdditions, List
tableDeletions, List tableDiffs) { + + public static SchemaDiff diff(Tables mappedEntities, Tables existingTables, Comparator nameComparator) { + + Map existingIndex = createMapping(existingTables.tables(), SchemaDiff::getKey, nameComparator); + Map mappedIndex = createMapping(mappedEntities.tables(), SchemaDiff::getKey, nameComparator); + + List
toCreate = getTablesToCreate(mappedEntities, withTableKey(existingIndex::containsKey)); + List
toDrop = getTablesToDrop(existingTables, withTableKey(mappedIndex::containsKey)); + + List tableDiffs = diffTable(mappedEntities, existingIndex, withTableKey(existingIndex::containsKey), + nameComparator); + + return new SchemaDiff(toCreate, toDrop, tableDiffs); + } + + private static List
getTablesToCreate(Tables mappedEntities, Predicate
excludeTable) { + + List
toCreate = new ArrayList<>(mappedEntities.tables().size()); + + for (Table table : mappedEntities.tables()) { + if (!excludeTable.test(table)) { + toCreate.add(table); + } + } + + return toCreate; + } + + private static List
getTablesToDrop(Tables existingTables, Predicate
excludeTable) { + + List
toDrop = new ArrayList<>(existingTables.tables().size()); + + for (Table table : existingTables.tables()) { + if (!excludeTable.test(table)) { + toDrop.add(table); + } + } + + return toDrop; + } + + private static List diffTable(Tables mappedEntities, Map existingIndex, + Predicate
includeTable, Comparator nameComparator) { + + List tableDiffs = new ArrayList<>(); + + for (Table mappedEntity : mappedEntities.tables()) { + + if (!includeTable.test(mappedEntity)) { + continue; + } + + // TODO: How to handle changed columns (type?) + + Table existingTable = existingIndex.get(getKey(mappedEntity)); + TableDiff tableDiff = new TableDiff(mappedEntity); + + 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); + } + } + + for (Column column : mappedEntity.columns()) { + if (addedColumns.containsKey(column.name())) { + tableDiff.columnsToAdd().add(column); + } + } + + tableDiffs.add(tableDiff); + } + + return tableDiffs; + } + + private static SortedMap createMapping(List items, Function keyFunction, + Comparator nameComparator) { + + SortedMap mapping = new TreeMap<>(nameComparator); + items.forEach(it -> mapping.put(keyFunction.apply(it), it)); + return mapping; + } + + private static String getKey(Table table) { + return table.schema() + "." + table.name(); + } + + private static Predicate
withTableKey(Predicate predicate) { + return it -> predicate.test(getKey(it)); + } + +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/SqlTypeMapping.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/SqlTypeMapping.java new file mode 100644 index 0000000000..d66f932ca4 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/SqlTypeMapping.java @@ -0,0 +1,101 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.mapping.schema; + +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Strategy interface for mapping a {@link RelationalPersistentProperty} to a Database type. + * + * @author Kurt Niemi + * @author Mark Paluch + * @since 3.2 + */ +@FunctionalInterface +public interface SqlTypeMapping { + + /** + * Determines a column type for a persistent property. + * + * @param property the property for which the type should be determined. + * @return the SQL type to use, such as {@code VARCHAR} or {@code NUMERIC}. Can be {@literal null} if the strategy + * cannot provide a column type. + */ + @Nullable + String getColumnType(RelationalPersistentProperty property); + + /** + * Returns the required column type for a persistent property or throws {@link IllegalArgumentException} if the type + * cannot be determined. + * + * @param property the property for which the type should be determined. + * @return the SQL type to use, such as {@code VARCHAR} or {@code NUMERIC}. Can be {@literal null} if the strategy + * cannot provide a column type. + * @throws IllegalArgumentException if the column type cannot be determined. + */ + default String getRequiredColumnType(RelationalPersistentProperty property) { + + String columnType = getColumnType(property); + + if (ObjectUtils.isEmpty(columnType)) { + throw new IllegalArgumentException(String.format("Cannot determined required column type for %s", property)); + } + + return columnType; + } + + /** + * Determine whether a column is nullable. + * + * @param property the property for which nullability should be determined. + * @return whether the property is nullable. + */ + default boolean isNullable(RelationalPersistentProperty property) { + return !property.getActualType().isPrimitive(); + } + + /** + * Returns a composed {@link SqlTypeMapping} that represents a fallback of this type mapping and another. When + * evaluating the composed predicate, if this mapping does not contain a column mapping (i.e. + * {@link #getColumnType(RelationalPersistentProperty)} returns{@literal null}), then the {@code other} mapping is + * evaluated. + *

+ * Any exceptions thrown during evaluation of either type mapping are relayed to the caller; if evaluation of this + * type mapping throws an exception, the {@code other} predicate will not be evaluated. + * + * @param other a type mapping that will be used as fallback, must not be {@literal null}. + * @return a composed type mapping + */ + default SqlTypeMapping and(SqlTypeMapping other) { + + Assert.notNull(other, "Other SqlTypeMapping must not be null"); + + return property -> { + + String columnType = getColumnType(property); + + if (ObjectUtils.isEmpty(columnType)) { + return other.getColumnType(property); + } + + return columnType; + }; + } + +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Table.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Table.java similarity index 84% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Table.java rename to spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Table.java index 51220a4dfa..43b465d9a7 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Table.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Table.java @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.relational.core.mapping.schema; +package org.springframework.data.jdbc.core.mapping.schema; import java.util.ArrayList; import java.util.List; +import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -26,9 +27,9 @@ * @author Kurt Niemi * @since 3.2 */ -record Table(String schema, String name, List keyColumns, List columns) { +record Table(@Nullable String schema, String name, List keyColumns, List columns) { - public Table(String schema, String name) { + public Table(@Nullable String schema, String name) { this(schema, name, new ArrayList<>(), new ArrayList<>()); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/SqlTypeMapping.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/TableDiff.java similarity index 54% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/SqlTypeMapping.java rename to spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/TableDiff.java index 6dda98d8bb..5ff5e01e71 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/SqlTypeMapping.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/TableDiff.java @@ -13,23 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.relational.core.mapping.schema; +package org.springframework.data.jdbc.core.mapping.schema; -import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import java.util.ArrayList; +import java.util.List; /** - * Interface for mapping a {@link RelationalPersistentProperty} to a Database type. + * Used to keep track of columns that should be added or deleted, when performing a difference between a source and + * target {@link Tables}. * * @author Kurt Niemi * @since 3.2 */ -public interface SqlTypeMapping { +record TableDiff(Table table, List columnsToAdd, List columnsToDrop) { + + public TableDiff(Table table) { + this(table, new ArrayList<>(), new ArrayList<>()); + } - /** - * Determine a column type for a persistent property. - * - * @param property the property for which the type should be determined. - * @return the SQL type to use, such as {@code VARCHAR} or {@code NUMERIC}. - */ - String getColumnType(RelationalPersistentProperty property); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Tables.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Tables.java similarity index 88% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Tables.java rename to spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Tables.java index e4c2844969..12a35ce535 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/Tables.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Tables.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.relational.core.mapping.schema; +package org.springframework.data.jdbc.core.mapping.schema; import java.util.Collections; import java.util.LinkedHashSet; @@ -26,6 +26,7 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.lang.Nullable; /** * Model class that contains Table/Column information that can be used to generate SQL for Schema generation. @@ -43,7 +44,7 @@ public static Tables from(RelationalMappingContext context) { // references. public static Tables from(Stream> persistentEntities, - SqlTypeMapping sqlTypeMapping, String defaultSchema) { + SqlTypeMapping sqlTypeMapping, @Nullable String defaultSchema) { List

tables = persistentEntities .filter(it -> it.isAnnotationPresent(org.springframework.data.relational.core.mapping.Table.class)) // @@ -60,8 +61,10 @@ public static Tables from(Stream> persis continue; } + String columnType = sqlTypeMapping.getRequiredColumnType(property); + Column column = new Column(property.getColumnName().getReference(), sqlTypeMapping.getColumnType(property), - true, identifierColumns.contains(property)); + sqlTypeMapping.isNullable(property), identifierColumns.contains(property)); table.columns().add(column); } return table; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/package-info.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/package-info.java similarity index 66% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/package-info.java rename to spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/package-info.java index 0a404f1247..2173c50d6f 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/package-info.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/package-info.java @@ -2,6 +2,6 @@ * Schema creation and schema update integration with Liquibase. */ @NonNullApi -package org.springframework.data.relational.core.mapping.schema; +package org.springframework.data.jdbc.core.mapping.schema; import org.springframework.lang.NonNullApi; 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 new file mode 100644 index 0000000000..d27e59a37e --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriterIntegrationTests.java @@ -0,0 +1,249 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.mapping.schema; + +import static org.assertj.core.api.Assertions.*; + +import liquibase.change.AddColumnConfig; +import liquibase.change.ColumnConfig; +import liquibase.change.core.AddColumnChange; +import liquibase.change.core.DropColumnChange; +import liquibase.change.core.DropTableChange; +import liquibase.changelog.ChangeSet; +import liquibase.changelog.DatabaseChangeLog; +import liquibase.database.core.H2Database; +import liquibase.database.jvm.JdbcConnection; + +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Set; + +import org.assertj.core.api.ThrowingConsumer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.core.io.ClassRelativeResourceLoader; +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.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.Table; +import org.springframework.data.util.Predicates; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; + +/** + * Integration tests for {@link LiquibaseChangeSetWriter}. + * + * @author Mark Paluch + */ +class LiquibaseChangeSetWriterIntegrationTests { + + @Test // GH-1430 + void shouldRemoveUnusedTable() { + + withEmbeddedDatabase("unused-table.sql", c -> { + + H2Database h2Database = new H2Database(); + h2Database.setConnection(new JdbcConnection(c)); + + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(new RelationalMappingContext()); + writer.setDropTableFilter(Predicates.isTrue()); + + ChangeSet changeSet = writer.createChangeSet(ChangeSetMetadata.create(), h2Database, new DatabaseChangeLog()); + + assertThat(changeSet.getChanges()).hasSize(1); + assertThat(changeSet.getChanges().get(0)).isInstanceOf(DropTableChange.class); + + DropTableChange drop = (DropTableChange) changeSet.getChanges().get(0); + assertThat(drop.getTableName()).isEqualToIgnoringCase("DELETE_ME"); + }); + } + + @Test // GH-1430 + void shouldNotDropTablesByDefault() { + + withEmbeddedDatabase("unused-table.sql", c -> { + + H2Database h2Database = new H2Database(); + h2Database.setConnection(new JdbcConnection(c)); + + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(new RelationalMappingContext()); + + ChangeSet changeSet = writer.createChangeSet(ChangeSetMetadata.create(), h2Database, new DatabaseChangeLog()); + + assertThat(changeSet.getChanges()).isEmpty(); + }); + } + + @Test // GH-1430 + void shouldAddColumnToTable() { + + withEmbeddedDatabase("person-with-id-and-name.sql", c -> { + + H2Database h2Database = new H2Database(); + h2Database.setConnection(new JdbcConnection(c)); + + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(contextOf(Person.class)); + + ChangeSet changeSet = writer.createChangeSet(ChangeSetMetadata.create(), h2Database, new DatabaseChangeLog()); + + assertThat(changeSet.getChanges()).hasSize(1); + assertThat(changeSet.getChanges().get(0)).isInstanceOf(AddColumnChange.class); + + AddColumnChange addColumns = (AddColumnChange) changeSet.getChanges().get(0); + assertThat(addColumns.getTableName()).isEqualToIgnoringCase("PERSON"); + assertThat(addColumns.getColumns()).hasSize(1); + + AddColumnConfig addColumn = addColumns.getColumns().get(0); + assertThat(addColumn.getName()).isEqualTo("last_name"); + assertThat(addColumn.getType()).isEqualTo("VARCHAR(255 BYTE)"); + }); + } + + @Test // GH-1430 + void shouldRemoveColumnFromTable() { + + withEmbeddedDatabase("person-with-id-and-name.sql", c -> { + + H2Database h2Database = new H2Database(); + h2Database.setConnection(new JdbcConnection(c)); + + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(contextOf(DifferentPerson.class)); + writer.setDropColumnFilter((s, s2) -> true); + + ChangeSet changeSet = writer.createChangeSet(ChangeSetMetadata.create(), h2Database, new DatabaseChangeLog()); + + assertThat(changeSet.getChanges()).hasSize(2); + assertThat(changeSet.getChanges().get(0)).isInstanceOf(AddColumnChange.class); + + AddColumnChange addColumns = (AddColumnChange) changeSet.getChanges().get(0); + assertThat(addColumns.getTableName()).isEqualToIgnoringCase("PERSON"); + assertThat(addColumns.getColumns()).hasSize(2); + assertThat(addColumns.getColumns()).extracting(AddColumnConfig::getName).containsExactly("my_id", "hello"); + + DropColumnChange dropColumns = (DropColumnChange) changeSet.getChanges().get(1); + assertThat(dropColumns.getTableName()).isEqualToIgnoringCase("PERSON"); + assertThat(dropColumns.getColumns()).hasSize(2); + assertThat(dropColumns.getColumns()).extracting(ColumnConfig::getName).map(String::toUpperCase).contains("ID", + "FIRST_NAME"); + }); + } + + @Test // GH-1430 + void doesNotRemoveColumnsByDefault() { + + withEmbeddedDatabase("person-with-id-and-name.sql", c -> { + + H2Database h2Database = new H2Database(); + h2Database.setConnection(new JdbcConnection(c)); + + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(contextOf(DifferentPerson.class)); + + ChangeSet changeSet = writer.createChangeSet(ChangeSetMetadata.create(), h2Database, new DatabaseChangeLog()); + + assertThat(changeSet.getChanges()).hasSize(1); + assertThat(changeSet.getChanges().get(0)).isInstanceOf(AddColumnChange.class); + }); + } + + @Test // GH-1430 + void shouldCreateNewChangeLog(@TempDir File tempDir) { + + withEmbeddedDatabase("person-with-id-and-name.sql", c -> { + + File changelogYml = new File(tempDir, "changelog.yml"); + H2Database h2Database = new H2Database(); + h2Database.setConnection(new JdbcConnection(c)); + + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(contextOf(DifferentPerson.class)); + writer.writeChangeSet(new FileSystemResource(changelogYml)); + + assertThat(tempDir).isDirectoryContaining(it -> it.getName().equalsIgnoreCase("changelog.yml")); + + assertThat(changelogYml).content().contains("author: Spring Data Relational").contains("name: hello"); + }); + } + + @Test // GH-1430 + void shouldAppendToChangeLog(@TempDir File tempDir) { + + withEmbeddedDatabase("person-with-id-and-name.sql", c -> { + + H2Database h2Database = new H2Database(); + h2Database.setConnection(new JdbcConnection(c)); + + File changelogYml = new File(tempDir, "changelog.yml"); + try (InputStream is = getClass().getResourceAsStream("changelog.yml")) { + Files.copy(is, changelogYml.toPath()); + } + + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(contextOf(DifferentPerson.class)); + writer.writeChangeSet(new FileSystemResource(new File(tempDir, "changelog.yml"))); + + assertThat(changelogYml).content().contains("author: Someone").contains("author: Spring Data Relational") + .contains("name: hello"); + }); + } + + RelationalMappingContext contextOf(Class... classes) { + + RelationalMappingContext context = new RelationalMappingContext(); + context.setInitialEntitySet(Set.of(classes)); + context.afterPropertiesSet(); + return context; + } + + void withEmbeddedDatabase(String script, ThrowingConsumer c) { + + EmbeddedDatabase embeddedDatabase = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass())) // + .generateUniqueName(true) // + .setType(EmbeddedDatabaseType.H2) // + .setScriptEncoding("UTF-8") // + .ignoreFailedDrops(true) // + .addScript(script) // + .build(); + + try { + + try (Connection connection = embeddedDatabase.getConnection()) { + c.accept(connection); + } + + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + embeddedDatabase.shutdown(); + } + } + + @Table + static class Person { + @Id int id; + String firstName; + String lastName; + } + + @Table("person") + static class DifferentPerson { + @Id int my_id; + String hello; + } + +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schema/LiquibaseChangeSetWriterUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriterUnitTests.java similarity index 61% rename from spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schema/LiquibaseChangeSetWriterUnitTests.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriterUnitTests.java index db6e74d834..314bbea8f4 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schema/LiquibaseChangeSetWriterUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/LiquibaseChangeSetWriterUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.relational.core.mapping.schema; +package org.springframework.data.jdbc.core.mapping.schema; import static org.assertj.core.api.Assertions.*; @@ -21,10 +21,10 @@ import liquibase.change.core.CreateTableChange; import liquibase.changelog.ChangeSet; import liquibase.changelog.DatabaseChangeLog; -import liquibase.exception.LiquibaseException; 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.RelationalMappingContext; /** @@ -34,15 +34,15 @@ */ class LiquibaseChangeSetWriterUnitTests { - @Test - void newTableShouldCreateChangeSet() throws LiquibaseException { + @Test // GH-1480 + void newTableShouldCreateChangeSet() { RelationalMappingContext context = new RelationalMappingContext(); context.getRequiredPersistentEntity(VariousTypes.class); LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context); - ChangeSet changeSet = writer.createChangeSet("", "", new DatabaseChangeLog()); + ChangeSet changeSet = writer.createChangeSet(ChangeSetMetadata.create(), new DatabaseChangeLog()); CreateTableChange createTable = (CreateTableChange) changeSet.getChanges().get(0); @@ -50,6 +50,27 @@ void newTableShouldCreateChangeSet() throws LiquibaseException { "luke_i_am_your_father", "dark_side", "floater"); assertThat(createTable.getColumns()).extracting(ColumnConfig::getType).containsSequence("BIGINT", "VARCHAR(255 BYTE)", "TINYINT", "FLOAT"); + + ColumnConfig id = createTable.getColumns().get(0); + assertThat(id.getConstraints().isNullable()).isFalse(); + } + + @Test // GH-1480 + void shouldApplySchemaFilter() { + + RelationalMappingContext context = new RelationalMappingContext(); + context.getRequiredPersistentEntity(VariousTypes.class); + context.getRequiredPersistentEntity(OtherTable.class); + + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context); + writer.setSchemaFilter(it -> it.getName().contains("OtherTable")); + + ChangeSet changeSet = writer.createChangeSet(ChangeSetMetadata.create(), new DatabaseChangeLog()); + + assertThat(changeSet.getChanges()).hasSize(1); + CreateTableChange createTable = (CreateTableChange) changeSet.getChanges().get(0); + + assertThat(createTable.getTableName()).isEqualTo("other_table"); } @org.springframework.data.relational.core.mapping.Table @@ -62,4 +83,9 @@ static class VariousTypes { Integer integerClass; } + @org.springframework.data.relational.core.mapping.Table + static class OtherTable { + @Id long id; + } + } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schema/SchemaDiffUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/SchemaDiffUnitTests.java similarity index 94% rename from spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schema/SchemaDiffUnitTests.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/SchemaDiffUnitTests.java index 521adb6126..f44372da22 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/schema/SchemaDiffUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/SchemaDiffUnitTests.java @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.relational.core.mapping.schema; +package org.springframework.data.jdbc.core.mapping.schema; import static org.assertj.core.api.Assertions.*; +import java.text.Collator; +import java.util.Locale; + import org.junit.jupiter.api.Test; import org.springframework.data.relational.core.mapping.RelationalMappingContext; @@ -51,7 +54,7 @@ void testDiffSchema() { delete_me.columns().add(newColumn); existingTables.tables().add(delete_me); - SchemaDiff diff = SchemaDiff.diff(mappedEntities, existingTables); + SchemaDiff diff = SchemaDiff.diff(mappedEntities, existingTables, Collator.getInstance(Locale.ROOT)::compare); // Verify that newtable is an added table in the diff assertThat(diff.tableAdditions()).isNotEmpty(); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/SqlTypeMappingUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/SqlTypeMappingUnitTests.java new file mode 100644 index 0000000000..600fcd53dd --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/schema/SqlTypeMappingUnitTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.mapping.schema; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.nio.charset.Charset; +import java.time.Duration; +import java.time.ZoneId; + +import org.junit.jupiter.api.Test; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; + +/** + * Unit tests for {@link SqlTypeMapping}. + * + * @author Mark Paluch + */ +class SqlTypeMappingUnitTests { + + SqlTypeMapping typeMapping = new DefaultSqlTypeMapping() // + .and(property -> property.getActualType().equals(ZoneId.class) ? "ZONEID" : null) + .and(property -> property.getActualType().equals(Duration.class) ? "INTERVAL" : null); + + @Test // GH-1480 + void shouldComposeTypeMapping() { + + RelationalPersistentProperty p = mock(RelationalPersistentProperty.class); + doReturn(String.class).when(p).getActualType(); + + assertThat(typeMapping.getColumnType(p)).isEqualTo("VARCHAR(255 BYTE)"); + assertThat(typeMapping.getRequiredColumnType(p)).isEqualTo("VARCHAR(255 BYTE)"); + } + + @Test // GH-1480 + void shouldDelegateToCompositeTypeMapping() { + + RelationalPersistentProperty p = mock(RelationalPersistentProperty.class); + doReturn(Duration.class).when(p).getActualType(); + + assertThat(typeMapping.getColumnType(p)).isEqualTo("INTERVAL"); + assertThat(typeMapping.getRequiredColumnType(p)).isEqualTo("INTERVAL"); + } + + @Test // GH-1480 + void shouldPassThruNullValues() { + + RelationalPersistentProperty p = mock(RelationalPersistentProperty.class); + doReturn(Charset.class).when(p).getActualType(); + + assertThat(typeMapping.getColumnType(p)).isNull(); + assertThatIllegalArgumentException().isThrownBy(() -> typeMapping.getRequiredColumnType(p)); + } +} diff --git a/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/changelog.yml b/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/changelog.yml new file mode 100644 index 0000000000..0e7566de1c --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/changelog.yml @@ -0,0 +1,16 @@ +databaseChangeLog: + - changeSet: + id: '123' + author: Someone + objectQuotingStrategy: LEGACY + changes: + - createTable: + columns: + - column: + autoIncrement: true + constraints: + nullable: false + primaryKey: true + name: id + type: INT + tableName: foo diff --git a/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/person-with-id-and-name.sql b/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/person-with-id-and-name.sql new file mode 100644 index 0000000000..226bde05eb --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/person-with-id-and-name.sql @@ -0,0 +1,5 @@ +CREATE TABLE person +( + id int, + first_name varchar(255) +); diff --git a/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/unused-table.sql b/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/unused-table.sql new file mode 100644 index 0000000000..efbc517647 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org/springframework/data/jdbc/core/mapping/schema/unused-table.sql @@ -0,0 +1,4 @@ +CREATE TABLE DELETE_ME +( + id int +); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/SchemaDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/SchemaDiff.java deleted file mode 100644 index afb6d72709..0000000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/SchemaDiff.java +++ /dev/null @@ -1,99 +0,0 @@ -package org.springframework.data.relational.core.mapping.schema; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Predicate; - -/** - * This class is created to return the difference between a source and target {@link Tables} The difference consists of - * Table Additions, Deletions, and Modified Tables (i.e. table exists in both source and target - but has columns to add - * or delete) - * - * @author Kurt Niemi - * @since 3.2 - */ -record SchemaDiff(List
tableAdditions, List
tableDeletions, List tableDiffs) { - - public static SchemaDiff diff(Tables mappedEntities, Tables existingTables) { - - Set
existingIndex = new HashSet<>(existingTables.tables()); - Set
mappedIndex = new HashSet<>(mappedEntities.tables()); - - List
toCreate = getTablesToCreate(mappedEntities, existingIndex::contains); - List
toDrop = getTablesToDrop(existingTables, mappedIndex::contains); - - List tableDiffs = diffTable(mappedEntities, existingTables, existingIndex::contains); - - return new SchemaDiff(toCreate, toDrop, tableDiffs); - } - - private static List
getTablesToCreate(Tables mappedEntities, Predicate
excludeTable) { - - List
toCreate = new ArrayList<>(mappedEntities.tables().size()); - - for (Table table : mappedEntities.tables()) { - if (!excludeTable.test(table)) { - toCreate.add(table); - } - } - - return toCreate; - } - - private static List
getTablesToDrop(Tables existingTables, Predicate
excludeTable) { - - List
toDrop = new ArrayList<>(existingTables.tables().size()); - - for (Table table : existingTables.tables()) { - if (!excludeTable.test(table)) { - toDrop.add(table); - } - } - - return toDrop; - } - - private static List diffTable(Tables mappedEntities, Tables existingTables, - Predicate
includeTable) { - - List tableDiffs = new ArrayList<>(); - Map existingIndex = new HashMap<>(existingTables.tables().size()); - existingTables.tables().forEach(it -> existingIndex.put(it, it)); - - for (Table mappedEntity : mappedEntities.tables()) { - - if (!includeTable.test(mappedEntity)) { - continue; - } - - // TODO: How to handle changed columns (type?) - - Table existingTable = existingIndex.get(mappedEntity); - TableDiff tableDiff = new TableDiff(mappedEntity); - - Set mappedColumns = new LinkedHashSet<>(mappedEntity.columns()); - Set existingColumns = new LinkedHashSet<>(existingTable.columns()); - - // Identify deleted columns - Set toDelete = new LinkedHashSet<>(existingColumns); - toDelete.removeAll(mappedColumns); - - tableDiff.columnsToDrop().addAll(toDelete); - - // Identify added columns - Set addedColumns = new LinkedHashSet<>(mappedColumns); - addedColumns.removeAll(existingColumns); - tableDiff.columnsToAdd().addAll(addedColumns); - - tableDiffs.add(tableDiff); - } - - return tableDiffs; - } - -} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/TableDiff.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/TableDiff.java deleted file mode 100644 index b15b4c64d9..0000000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/schema/TableDiff.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.springframework.data.relational.core.mapping.schema; - -import java.util.ArrayList; -import java.util.List; - -/** - * Used to keep track of columns that should be added or deleted, when performing a difference between a source and - * target {@link Tables}. - * - * @author Kurt Niemi - * @since 3.2 - */ -record TableDiff(Table table, List columnsToAdd, List columnsToDrop) { - - public TableDiff(Table table) { - this(table, new ArrayList<>(), new ArrayList<>()); - } - -} From cd8d0415cf203c1ed9174efab593b44f280697e9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 5 Jun 2023 15:28:04 +0200 Subject: [PATCH 32/32] Add documentation. --- src/main/asciidoc/index.adoc | 1 + src/main/asciidoc/schema-support.adoc | 90 +++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 src/main/asciidoc/schema-support.adoc diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 0016afe037..4b815d1c65 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -23,6 +23,7 @@ include::{spring-data-commons-docs}/repositories.adoc[leveloffset=+1] = Reference Documentation include::jdbc.adoc[leveloffset=+1] +include::schema-support.adoc[leveloffset=+1] [[appendix]] = Appendix diff --git a/src/main/asciidoc/schema-support.adoc b/src/main/asciidoc/schema-support.adoc new file mode 100644 index 0000000000..6845c05ef8 --- /dev/null +++ b/src/main/asciidoc/schema-support.adoc @@ -0,0 +1,90 @@ +[[jdbc.schema]] += Schema Creation + +When working with SQL databases, the schema is an essential part. +Spring Data JDBC supports a wide range of schema options yet when starting with a domain model it can be challenging to come up with an initial domain model. +To assist you with a code-first approach, Spring Data JDBC ships with an integration to create database change sets using https://www.liquibase.org/[Liquibase]. + +Consider the following domain entity: + +[source,java] +---- +@Table +class Person { + @Id long id; + String firstName; + String lastName; + LocalDate birthday; + boolean active; +} +---- + +Rendering the initial ChangeSet through the following code: + +[source,java] +---- + +RelationalMappingContext context = … // The context contains the Person entity, ideally initialized through initialEntitySet +LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context); + +writer.writeChangeSet(new FileSystemResource(new File(…))); +---- + +yields the following change log: + +[source,yaml] +---- +databaseChangeLog: +- changeSet: + id: '1685969572426' + author: Spring Data Relational + objectQuotingStrategy: LEGACY + changes: + - createTable: + columns: + - column: + autoIncrement: true + constraints: + nullable: false + primaryKey: true + name: id + type: BIGINT + - column: + constraints: + nullable: true + name: first_name + type: VARCHAR(255 BYTE) + - column: + constraints: + nullable: true + name: last_name + type: VARCHAR(255 BYTE) + - column: + constraints: + nullable: true + name: birthday + type: DATE + - column: + constraints: + nullable: false + name: active + type: TINYINT + tableName: person +---- + +Column types are computed from an object implementing the `SqlTypeMapping` strategy interface. +Nullability is inferred from the type and set to `false` if a property type use primitive Java types. + +Schema support can assist you throughout the application development lifecycle. +In differential mode, you provide an existing Liquibase `Database` to the schema writer instance and the schema writer compares existing tables to mapped entities and derives from the difference which tables and columns to create/to drop. +By default, no tables and no columns are dropped unless you configure `dropTableFilter` and `dropColumnFilter`. +Both filter predicate provide the table name respective column name so your code can computer which tables and columns can be dropped. + +[source,java] +---- +writer.setDropTableFilter(tableName -> …); +writer.setDropColumnFilter((tableName, columnName) -> …); +---- + +NOTE: Schema support can only identify additions and removals in the sense of removing tables/columns that are not mapped or adding columns that do not exist in the database. +Columns cannot be renamed nor data cannot be migrated because entity mapping does not provide details of how the schema has evolved.