Skip to content

Commit a003571

Browse files
authored
Add example generating Liquibase table creation with Spring Data JDBC (#226)
1 parent 4599a65 commit a003571

21 files changed

+746
-0
lines changed

README.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ All tutorials are documented in AsciiDoc format and published as an https://anto
4747
|link:data-domain-events[Spring Data Domain Events] |Publish Domain Events with Spring Data
4848
|link:data-envers-audit[Spring Data Envers Audit] |Enable with Entity Revisions using Spring Data Envers
4949
|link:data-jdbc-audit[Spring Data JDBC Audit] |Enable Audit with Spring Data JDBC
50+
|link:data-jdbc-schema-generation[Spring Data JDBC: Generate Liquibase Changeset] |Generate Liquibase changeset with Spring Data JDBC
5051
|link:data-jpa-audit[Spring Data JPA Audit] |Enable Audit with Spring Data JPA
5152
|link:data-jpa-event[Spring Data JPA: Event Driven] |Implement `Entity` validation at `Repository` level through `EventListeners`
5253
|link:data-jpa-filtered-query[Spring Data JPA: Global Filtered Query] |Implement global filtered query with Spring Data JPA by defining `repositoryBaseClass`
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/gradlew text eol=lf
2+
*.bat text eol=crlf
3+
*.jar binary
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
HELP.md
2+
.gradle
3+
build/
4+
!gradle/wrapper/gradle-wrapper.jar
5+
!**/src/main/**/build/
6+
!**/src/test/**/build/
7+
8+
### STS ###
9+
.apt_generated
10+
.classpath
11+
.factorypath
12+
.project
13+
.settings
14+
.springBeans
15+
.sts4-cache
16+
bin/
17+
!**/src/main/**/bin/
18+
!**/src/test/**/bin/
19+
20+
### IntelliJ IDEA ###
21+
.idea
22+
*.iws
23+
*.iml
24+
*.ipr
25+
out/
26+
!**/src/main/**/out/
27+
!**/src/test/**/out/
28+
29+
### NetBeans ###
30+
/nbproject/private/
31+
/nbbuild/
32+
/dist/
33+
/nbdist/
34+
/.nb-gradle/
35+
36+
### VS Code ###
37+
.vscode/
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
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.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
plugins {
2+
id 'java'
3+
id 'org.springframework.boot' version '3.4.4'
4+
id 'io.spring.dependency-management' version '1.1.7'
5+
}
6+
7+
group = 'zin.rashidi.boot'
8+
version = '0.0.1-SNAPSHOT'
9+
10+
java {
11+
toolchain {
12+
languageVersion = JavaLanguageVersion.of(21)
13+
}
14+
}
15+
16+
repositories {
17+
mavenCentral()
18+
}
19+
20+
dependencies {
21+
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
22+
implementation 'org.liquibase:liquibase-core'
23+
runtimeOnly 'org.postgresql:postgresql'
24+
testImplementation 'org.springframework.boot:spring-boot-starter-test'
25+
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
26+
testImplementation 'org.testcontainers:junit-jupiter'
27+
testImplementation 'org.testcontainers:postgresql'
28+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
29+
}
30+
31+
tasks.named('test') {
32+
useJUnitPlatform()
33+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = 'data-jdbc-schema-generation'
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package zin.rashidi.boot.jdbcscgm;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class DataJdbcSchemaGenerationApplication {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(DataJdbcSchemaGenerationApplication.class, args);
11+
}
12+
13+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package zin.rashidi.boot.jdbcscgm.book;
2+
3+
import org.springframework.data.annotation.Id;
4+
import org.springframework.data.relational.core.mapping.Table;
5+
6+
/**
7+
* @author Rashidi Zin
8+
*/
9+
@Table
10+
class Author {
11+
12+
@Id
13+
private Long id;
14+
private String name;
15+
16+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package zin.rashidi.boot.jdbcscgm.book;
2+
3+
import org.springframework.data.repository.CrudRepository;
4+
5+
/**
6+
* @author Rashidi Zin
7+
*/
8+
interface AuthorRepository extends CrudRepository<Author, Long> {
9+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package zin.rashidi.boot.jdbcscgm.book;
2+
3+
import org.springframework.data.annotation.Id;
4+
import org.springframework.data.relational.core.mapping.MappedCollection;
5+
import org.springframework.data.relational.core.mapping.Table;
6+
7+
/**
8+
* @author Rashidi Zin
9+
*/
10+
@Table
11+
class Book {
12+
13+
@Id
14+
private Long isbn;
15+
private String title;
16+
17+
@MappedCollection
18+
private Author author;
19+
20+
}

0 commit comments

Comments
 (0)