|
| 1 | += Spring Data JDBC: Generate Liquibase Changeset |
| 2 | +Rashidi Zin <rashidi@zin.my> |
| 3 | +1.0, April 13, 2025: Initial version |
| 4 | +:toc: |
| 5 | +:icons: font |
| 6 | +:source-highlighter: highlight.js |
| 7 | +:url-quickref: https://github.com/rashidi/spring-boot-tutorials/tree/master/data-jdbc-schema-generation |
| 8 | +:source-main: {url-quickref}/src/main/java/zin/rashidi/boot/jdbcscgm |
| 9 | +:source-test: {url-quickref}/src/test/java/zin/rashidi/boot/jdbcscgm |
| 10 | + |
| 11 | +This tutorial demonstrates how to generate a Liquibase changeset from Spring Data JDBC entities. This is useful when you want to automatically create database schema migration scripts based on your domain model. |
| 12 | + |
| 13 | +== Background |
| 14 | + |
| 15 | +https://www.liquibase.org/[Liquibase] is a database schema migration tool that helps manage database changes across different environments. Spring Data JDBC provides a convenient way to generate Liquibase changesets from your entity classes using the `LiquibaseChangeSetWriter` class. |
| 16 | + |
| 17 | +This approach allows you to: |
| 18 | + |
| 19 | +- Automatically generate database schema from your domain model |
| 20 | +- Keep your database schema in sync with your code |
| 21 | +- Version control your database changes |
| 22 | +- Apply changes consistently across different environments |
| 23 | + |
| 24 | +== Domain Model |
| 25 | + |
| 26 | +For this tutorial, we'll use a simple book catalog domain model with two entities: `Book` and `Author`. |
| 27 | + |
| 28 | +=== Book Entity |
| 29 | + |
| 30 | +The `Book` entity represents a book in our catalog: |
| 31 | + |
| 32 | +[source,java] |
| 33 | +---- |
| 34 | +@Table |
| 35 | +class Book { |
| 36 | +
|
| 37 | + @Id |
| 38 | + private Long isbn; |
| 39 | + private String title; |
| 40 | +
|
| 41 | + @MappedCollection |
| 42 | + private Author author; |
| 43 | +
|
| 44 | +} |
| 45 | +---- |
| 46 | + |
| 47 | +link:{source-main}/book/Book.java[`Book`] is a simple POJO with an ISBN as the primary key, a title, and a reference to an `Author` using the `@MappedCollection` annotation. |
| 48 | + |
| 49 | +=== Author Entity |
| 50 | + |
| 51 | +The `Author` entity represents the author of a book: |
| 52 | + |
| 53 | +[source,java] |
| 54 | +---- |
| 55 | +@Table |
| 56 | +class Author { |
| 57 | +
|
| 58 | + @Id |
| 59 | + private Long id; |
| 60 | + private String name; |
| 61 | +
|
| 62 | +} |
| 63 | +---- |
| 64 | + |
| 65 | +link:{source-main}/book/Author.java[`Author`] is also a simple POJO with an ID and a name. |
| 66 | + |
| 67 | +== Repository Interfaces |
| 68 | + |
| 69 | +We'll need repository interfaces for our entities to enable Spring Data JDBC functionality: |
| 70 | + |
| 71 | +[source,java] |
| 72 | +---- |
| 73 | +interface BookRepository extends CrudRepository<Book, Long> { |
| 74 | +} |
| 75 | +---- |
| 76 | + |
| 77 | +[source,java] |
| 78 | +---- |
| 79 | +interface AuthorRepository extends CrudRepository<Author, Long> { |
| 80 | +} |
| 81 | +---- |
| 82 | + |
| 83 | +These interfaces extend `CrudRepository` to provide basic CRUD operations for our entities. |
| 84 | + |
| 85 | +== Generating the Changeset |
| 86 | + |
| 87 | +To generate a Liquibase changeset from our entities, we'll use the `LiquibaseChangeSetWriter` class from Spring Data JDBC. This can be done in a test class: |
| 88 | + |
| 89 | +[source,java] |
| 90 | +---- |
| 91 | +@Import(TestcontainersConfiguration.class) |
| 92 | +@DataJdbcTest(properties = "spring.liquibase.enabled=false") |
| 93 | +class BookRepositoryTests { |
| 94 | +
|
| 95 | + @BeforeAll |
| 96 | + static void generateSchema(@Autowired RelationalMappingContext context) throws IOException { |
| 97 | + context.setInitialEntitySet(Set.of(Author.class, Book.class)); |
| 98 | +
|
| 99 | + var writer = new LiquibaseChangeSetWriter(context); |
| 100 | + writer.writeChangeSet(new FileSystemResource("user.yaml")); |
| 101 | + } |
| 102 | +
|
| 103 | + @Autowired |
| 104 | + private BookRepository books; |
| 105 | +
|
| 106 | + @Test |
| 107 | + void findAll() { |
| 108 | + books.findAll(); |
| 109 | + } |
| 110 | +
|
| 111 | +} |
| 112 | +---- |
| 113 | + |
| 114 | +[WARNING] |
| 115 | +==== |
| 116 | +When you run this test for the first time, it will fail because the changeset for Book and Author is missing. The test will generate the changeset file, but it won't be applied automatically. After the first run, you'll need to move the generated changeset to your Liquibase changelog directory and configure Liquibase to use it. |
| 117 | +Once done, remove the configuration that disables Liquibase (`"spring.liquibase.enabled=false"`) |
| 118 | +==== |
| 119 | + |
| 120 | +Let's break down the key parts of this code: |
| 121 | + |
| 122 | +1. We use `@DataJdbcTest` to set up a Spring Data JDBC test environment. |
| 123 | + |
| 124 | +2. The `@BeforeAll` method is where we generate the changeset: |
| 125 | + - We inject the `RelationalMappingContext` which contains metadata about our entities |
| 126 | + - We set the initial entity set to include our `Author` and `Book` classes |
| 127 | + - We create a new `LiquibaseChangeSetWriter` with the context |
| 128 | + - We write the changeset to a file named "user.yaml" |
| 129 | + |
| 130 | +== Test Configuration |
| 131 | + |
| 132 | +For testing, we use Testcontainers to provide a PostgreSQL database: |
| 133 | + |
| 134 | +[source,java] |
| 135 | +---- |
| 136 | +@TestConfiguration(proxyBeanMethods = false) |
| 137 | +public class TestcontainersConfiguration { |
| 138 | +
|
| 139 | + @Bean |
| 140 | + @ServiceConnection |
| 141 | + PostgreSQLContainer<?> postgresContainer() { |
| 142 | + return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest")); |
| 143 | + } |
| 144 | +
|
| 145 | +} |
| 146 | +---- |
| 147 | + |
| 148 | +This configuration creates a PostgreSQL container for our tests and automatically configures the connection using Spring Boot's `@ServiceConnection` annotation. |
| 149 | + |
| 150 | +== Generated Changeset |
| 151 | + |
| 152 | +The generated changeset will look something like this: |
| 153 | + |
| 154 | +[source,yaml] |
| 155 | +---- |
| 156 | +databaseChangeLog: |
| 157 | +- changeSet: |
| 158 | + id: '1744500868871' |
| 159 | + author: Spring Data Relational |
| 160 | + objectQuotingStrategy: LEGACY |
| 161 | + changes: |
| 162 | + - createTable: |
| 163 | + columns: |
| 164 | + - column: |
| 165 | + autoIncrement: true |
| 166 | + constraints: |
| 167 | + nullable: true |
| 168 | + primaryKey: true |
| 169 | + name: id |
| 170 | + type: BIGINT |
| 171 | + - column: |
| 172 | + constraints: |
| 173 | + nullable: true |
| 174 | + name: name |
| 175 | + type: VARCHAR(255 BYTE) |
| 176 | + - column: |
| 177 | + constraints: |
| 178 | + nullable: false |
| 179 | + name: book |
| 180 | + type: BIGINT |
| 181 | + tableName: author |
| 182 | + - createTable: |
| 183 | + columns: |
| 184 | + - column: |
| 185 | + autoIncrement: true |
| 186 | + constraints: |
| 187 | + nullable: true |
| 188 | + primaryKey: true |
| 189 | + name: isbn |
| 190 | + type: BIGINT |
| 191 | + - column: |
| 192 | + constraints: |
| 193 | + nullable: true |
| 194 | + name: title |
| 195 | + type: VARCHAR(255 BYTE) |
| 196 | + tableName: book |
| 197 | + - addForeignKeyConstraint: |
| 198 | + baseColumnNames: book |
| 199 | + baseTableName: author |
| 200 | + constraintName: book_isbn_fk |
| 201 | + referencedColumnNames: isbn |
| 202 | + referencedTableName: book |
| 203 | +---- |
| 204 | + |
| 205 | +This changeset includes: |
| 206 | +- Creation of the `author` table with columns for id, name, and a foreign key to book |
| 207 | +- Creation of the `book` table with columns for isbn and title |
| 208 | +- Addition of a foreign key constraint from author to book |
| 209 | + |
| 210 | +== Using the Generated Changeset |
| 211 | + |
| 212 | +To use the generated changeset in your application: |
| 213 | + |
| 214 | +1. Move the generated file to your Liquibase changelog directory (e.g., `src/main/resources/db/changelog/`) |
| 215 | +2. Include it in your master changelog file: |
| 216 | + |
| 217 | +[source,yaml] |
| 218 | +---- |
| 219 | +databaseChangeLog: |
| 220 | + - include: |
| 221 | + file: db/changelog/user.yaml |
| 222 | +---- |
| 223 | + |
| 224 | +== Conclusion |
| 225 | + |
| 226 | +In this tutorial, we've demonstrated how to generate a Liquibase changeset from Spring Data JDBC entities. This approach provides a convenient way to keep your database schema in sync with your domain model, making it easier to manage database changes across different environments. |
| 227 | + |
| 228 | +The key components we used are: |
| 229 | +- Spring Data JDBC entities with appropriate annotations |
| 230 | +- Repository interfaces extending `CrudRepository` |
| 231 | +- `LiquibaseChangeSetWriter` to generate the changeset |
| 232 | +- Testcontainers for testing with a real database |
| 233 | + |
| 234 | +By following this approach, you can automate the creation of database migration scripts and ensure that your database schema always matches your domain model. |
0 commit comments