diff --git a/jdbc/howto/pom.xml b/jdbc/howto/pom.xml index 5da1ec32..0ac931b3 100644 --- a/jdbc/howto/pom.xml +++ b/jdbc/howto/pom.xml @@ -24,6 +24,7 @@ caching idgeneration selectiveupdate + schema-generation diff --git a/jdbc/howto/schema-generation/.gitignore b/jdbc/howto/schema-generation/.gitignore new file mode 100644 index 00000000..a718e994 --- /dev/null +++ b/jdbc/howto/schema-generation/.gitignore @@ -0,0 +1 @@ +cs-*.yaml \ No newline at end of file diff --git a/jdbc/howto/schema-generation/README.adoc b/jdbc/howto/schema-generation/README.adoc new file mode 100644 index 00000000..3f27e40f --- /dev/null +++ b/jdbc/howto/schema-generation/README.adoc @@ -0,0 +1,5 @@ +== Spring Data JDBC How To Generate the Database Schema. + +Spring Data JDBC offers an API to generate Liquibase Changesets from your domain model and optionally an existing database. + +This project demonstrates how to do this. diff --git a/jdbc/howto/schema-generation/pom.xml b/jdbc/howto/schema-generation/pom.xml new file mode 100644 index 00000000..29f5c92e --- /dev/null +++ b/jdbc/howto/schema-generation/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + spring-data-jdbc-how-to-schema-generation + + + org.springframework.data.examples + spring-data-jdbc-how-to + 2.0.0.BUILD-SNAPSHOT + ../pom.xml + + + Spring Data JDBC - How to do schema generation + Sample project for Spring Data JDBC demonstrating how it can be used to generate and update your + schema. + + https://projects.spring.io/spring-data-jdbc + 2023 + + + 2023.1.0-M2 + + + + + + org.liquibase + liquibase-core + + + + diff --git a/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Minion.java b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Minion.java new file mode 100644 index 00000000..0ac2e9fb --- /dev/null +++ b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Minion.java @@ -0,0 +1,45 @@ +/* + * 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 example.springdata.jdbc.howto.caching; + +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Table; + + +/** + * Entity we use as a fixture for demonstrating Schema Generation. + * See SchemaGenerationTest in the test directory for the interesting stuff. + * + * @author Jens Schauder + * @since 3.2 + */ +@Table +class Minion { + + @Id + Long id; + + @Name + String name; + + Minion(String name) { + this.name = name; + } + + public Long getId() { + return id; + } +} diff --git a/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Name.java b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Name.java new file mode 100644 index 00000000..91d4158f --- /dev/null +++ b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Name.java @@ -0,0 +1,30 @@ +/* + * 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 example.springdata.jdbc.howto.caching; + + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Annotation to mark a property as a Name which should have a very specify database type. + * + * @author Jens Schauder + */ +@Varchar(20) +@Retention(RetentionPolicy.RUNTIME) +public @interface Name { +} diff --git a/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/SchemaGenerationExampleApplication.java b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/SchemaGenerationExampleApplication.java new file mode 100644 index 00000000..75fe10ef --- /dev/null +++ b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/SchemaGenerationExampleApplication.java @@ -0,0 +1,37 @@ +/* + * 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 example.springdata.jdbc.howto.caching; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; + +/** + * Boot application that we use as a fixture for demonstrating Schema Generation. + * See SchemaGenerationTest in the test directory for the interesting stuff. + * + * @author Jens Schauder + * @since 3.2 + */ +@EnableCaching +@SpringBootApplication +class SchemaGenerationExampleApplication { + + public static void main(String[] args) { + SpringApplication.run(SchemaGenerationExampleApplication.class, args); + } + +} diff --git a/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Varchar.java b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Varchar.java new file mode 100644 index 00000000..626af7e1 --- /dev/null +++ b/jdbc/howto/schema-generation/src/main/java/example.springdata/jdbc/howto/caching/Varchar.java @@ -0,0 +1,33 @@ +/* + * 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 example.springdata.jdbc.howto.caching; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Marks a property as to be represented by a VARCHAR database type. + * + * @author Jens Schauder + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface Varchar { + + /** + * the size of the varchar. + */ + int value(); +} diff --git a/jdbc/howto/schema-generation/src/main/resources/application.properties b/jdbc/howto/schema-generation/src/main/resources/application.properties new file mode 100644 index 00000000..07ddc16f --- /dev/null +++ b/jdbc/howto/schema-generation/src/main/resources/application.properties @@ -0,0 +1,2 @@ +# Disable Liquibase when you don't have changesets configured yet. +# spring.liquibase.enabled=false \ No newline at end of file diff --git a/jdbc/howto/schema-generation/src/main/resources/db/changelog/db.changelog-master.yaml b/jdbc/howto/schema-generation/src/main/resources/db/changelog/db.changelog-master.yaml new file mode 100644 index 00000000..615ec854 --- /dev/null +++ b/jdbc/howto/schema-generation/src/main/resources/db/changelog/db.changelog-master.yaml @@ -0,0 +1,32 @@ +databaseChangeLog: +- changeSet: + id: '1692725820135' + author: Spring Data Relational + objectQuotingStrategy: LEGACY + changes: + - createTable: + columns: + - column: + autoIncrement: true + constraints: + nullable: true + primaryKey: true + name: id + type: BIGINT + - column: + constraints: + nullable: true + name: firstname + type: VARCHAR(255 BYTE) + - column: + constraints: + nullable: true + name: lastname + type: VARCHAR(255 BYTE) + - column: + constraints: + nullable: true + name: special + type: VARCHAR(255 BYTE) + tableName: minion + diff --git a/jdbc/howto/schema-generation/src/test/java/example/springdata/jdbc/howto/caching/SchemaGenerationTest.java b/jdbc/howto/schema-generation/src/test/java/example/springdata/jdbc/howto/caching/SchemaGenerationTest.java new file mode 100644 index 00000000..5f915e43 --- /dev/null +++ b/jdbc/howto/schema-generation/src/test/java/example/springdata/jdbc/howto/caching/SchemaGenerationTest.java @@ -0,0 +1,140 @@ +/* + * 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 example.springdata.jdbc.howto.caching; + +import liquibase.database.Database; +import liquibase.database.core.HsqlDatabase; +import liquibase.database.jvm.JdbcConnection; +import liquibase.exception.LiquibaseException; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.FileSystemResource; +import org.springframework.data.jdbc.core.mapping.schema.DefaultSqlTypeMapping; +import org.springframework.data.jdbc.core.mapping.schema.LiquibaseChangeSetWriter; +import org.springframework.data.jdbc.core.mapping.schema.SqlTypeMapping; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; + +import javax.sql.DataSource; +import java.io.File; +import java.io.IOException; +import java.sql.SQLException; +import java.util.Collections; + +/** + * Example code demonstrating how to use the Schema Generation Feature. + * + * @author Jens Schauder + * @since 3.2 + */ +@SpringBootTest +class SchemaGenerationTest { + + @Autowired + RelationalMappingContext context; + + @Autowired + DataSource ds; + + @Test + void minimumExample() throws IOException { + + // the change set will get appended, so we delete any pre existing file. + new File("cs-minimum.yaml").delete(); + + context.setInitialEntitySet(Collections.singleton(Minion.class)); + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context); + + writer.writeChangeSet(new FileSystemResource("cs-minimum.yaml")); + } + + + @Test + void diffing() { + + // the change set will get appended, so we delete any pre existing file. + new File("cs-diff.yaml").delete(); + + context.setInitialEntitySet(Collections.singleton(Minion.class)); + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context); + + // drop unused columns + writer.setDropColumnFilter((table, column) -> !column.equalsIgnoreCase("special")); + + // for comparison with existing schema + try (Database db = new HsqlDatabase()) { + + db.setConnection(new JdbcConnection(ds.getConnection())); + + writer.writeChangeSet(new FileSystemResource("cs-diff.yaml"), db); + + } catch (IOException | SQLException | LiquibaseException e) { + throw new RuntimeException("Changeset generation failed", e); + } + } + + @Test + void customizingTypes() throws IOException { + + // the change set will get appended, so we delete any pre existing file. + new File("cs-custom.yaml").delete(); + + context.setInitialEntitySet(Collections.singleton(Minion.class)); + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context); + + writer.setSqlTypeMapping(((SqlTypeMapping) property -> { + if (property.getName().equalsIgnoreCase("name")) { + return "VARCHAR(500)"; + } + return null; + }).and(new DefaultSqlTypeMapping())); + + writer.writeChangeSet(new FileSystemResource("cs-custom.yaml")); + + } + + @Test + void customizingTypesUsingAnnotations() throws IOException { + + // the change set will get appended, so we delete any pre existing file. + new File("cs-annotation.yaml").delete(); + + context.setInitialEntitySet(Collections.singleton(Minion.class)); + LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context); + + writer.setSqlTypeMapping(((SqlTypeMapping) property -> { + + if (!property.getType().equals(String.class)) { + return null; + } + + // findAnnotation will find meta annotations + Varchar varchar = property.findAnnotation(Varchar.class); + int value = varchar.value(); + + if (varchar == null) { + return null; + } + return "VARCHAR(" + + varchar.value() + + ")"; + + }).and(new DefaultSqlTypeMapping())); + + writer.writeChangeSet(new FileSystemResource("cs-annotation.yaml")); + + } +}