Skip to content

Commit 568bba3

Browse files
authored
Add example for Spring Data RepositoryDefinition
Introduce a new `data-repository-definition` module demonstrating the use of `@RepositoryDefinition`. Refactor existing repositories to use the annotation for cleaner and more focused APIs.
1 parent 7183030 commit 568bba3

File tree

14 files changed

+286
-0
lines changed

14 files changed

+286
-0
lines changed

README.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ with https://plugins.gradle.org/plugin/org.springframework.boot/3.4.3[Spring Boo
4747
|link:data-mongodb-audit[Spring Data MongoDB Audit] |Enable Audit with Spring Data MongoDB
4848
|link:data-mongodb-full-text-search[Spring Data MongoDB: Full Text Search] |Implement link:https://docs.mongodb.com/manual/text-search/[MongoDB Full Text Search] with Spring Data MongoDB
4949
|link:data-mongodb-transactional[Spring Data MongoDB: Transactional] |Enable `@Transactional` support for Spring Data MongoDB
50+
|link:data-repository-definition[Spring Data: Repository Definition] |Implement custom repository interfaces with `@RepositoryDefinition` annotation
5051
|link:data-rest-validation[Spring Data REST: Validation] |Perform validation with Spring Data REST
5152
|link:graphql[Spring GraphQL Server] |Implement GraphQL server with Spring GraphQL Server
5253
|link:jooq[jOOQ] | Implement an alternative to Jpa using https://www.jooq.org/[jOOQ] and Gradle
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

data-repository-definition/.gitignore

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: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
= Spring Data: Repository Definition
2+
:source-highlighter: highlight.js
3+
Rashidi Zin <rashidi@zin.my>
4+
1.0, March 22, 2025
5+
:toc:
6+
:nofooter:
7+
:icons: font
8+
:url-quickref: https://github.com/rashidi/spring-boot-tutorials/tree/master/data-repository-definition
9+
10+
Implement custom repository interfaces with @RepositoryDefinition annotation.
11+
12+
include::../docs/badges.adoc[]
13+
14+
== Background
15+
16+
link:https://spring.io/projects/spring-data[Spring Data] provides a consistent programming model for data access while still retaining the special traits of the underlying data store. It makes it easy to use data access technologies, relational and non-relational databases, map-reduce frameworks, and cloud-based data services.
17+
18+
When working with Spring Data, we typically create repository interfaces by extending one of the provided base interfaces such as `CrudRepository`, `JpaRepository`, or `MongoRepository`. However, sometimes we may want to define a repository with only specific methods, without inheriting all the methods from these base interfaces.
19+
20+
This is where the `@RepositoryDefinition` annotation comes in. It allows us to define a repository interface with only the methods we need, providing more control over the repository's API.
21+
22+
== Domain Class
23+
24+
We have a simple domain class, link:{url-quickref}/src/main/java/zin/rashidi/data/repositorydefinition/note/Note.java[Note], which is a Java record with three fields: `id`, `title`, and `content`.
25+
26+
[source,java]
27+
----
28+
record Note(@Id Long id, String title, String content) {
29+
}
30+
----
31+
32+
The `@Id` annotation from Spring Data marks the `id` field as the primary key.
33+
34+
== Repository Definition
35+
36+
Instead of extending a base repository interface, we use the `@RepositoryDefinition` annotation to define our repository interface, link:{url-quickref}/src/main/java/zin/rashidi/data/repositorydefinition/note/NoteRepository.java[NoteRepository].
37+
38+
[source,java]
39+
----
40+
@RepositoryDefinition(domainClass = Note.class, idClass = Long.class)
41+
interface NoteRepository {
42+
43+
List<Note> findByTitleContainingIgnoreCase(String title);
44+
45+
}
46+
----
47+
48+
The `@RepositoryDefinition` annotation takes two parameters:
49+
- `domainClass`: The entity class that this repository manages (in this case, `Note.class`)
50+
- `idClass`: The type of the entity's ID field (in this case, `Long.class`)
51+
52+
With this annotation, Spring Data will create a repository implementation for us, just like it would for a repository that extends a base interface. The difference is that our repository only has the methods we explicitly define, in this case, just `findByTitleContainingIgnoreCase`.
53+
54+
== Benefits of @RepositoryDefinition
55+
56+
Using `@RepositoryDefinition` offers several benefits:
57+
58+
1. **Minimalist API**: You only expose the methods you need, making the API cleaner and more focused.
59+
2. **Explicit Contract**: The repository interface clearly shows what operations are supported.
60+
3. **Reduced Surface Area**: By not inheriting methods from base interfaces, you reduce the risk of unintended operations being performed.
61+
4. **Flexibility**: You can define repositories for any domain class without being tied to a specific persistence technology's base interface.
62+
63+
== Testing
64+
65+
We can link:{url-quickref}/src/test/java/zin/rashidi/data/repositorydefinition/note/NoteRepositoryTests.java[test our repository] using Spring Boot's testing support with Testcontainers for PostgreSQL.
66+
67+
[source,java]
68+
----
69+
@Import(TestcontainersConfiguration.class)
70+
@DataJdbcTest
71+
@SqlMergeMode(MERGE)
72+
@Sql(statements = "CREATE TABLE note (id BIGINT PRIMARY KEY, title VARCHAR(50), content TEXT);", executionPhase = BEFORE_TEST_CLASS)
73+
class NoteRepositoryTests {
74+
75+
@Autowired
76+
private NoteRepository notes;
77+
78+
@Test
79+
@Sql(statements = {
80+
"INSERT INTO note (id, title, content) VALUES ('1', 'Right Turn', 'Step forward. Step forward and turn right. Collect.')",
81+
"INSERT INTO note (id, title, content) VALUES ('2', 'Left Turn', 'Step forward. Reverse and turn left. Collect.')",
82+
"INSERT INTO note (id, title, content) VALUES ('3', 'Double Spin', 'Syncopated. Double spin. Collect.')"
83+
})
84+
@DisplayName("Given there are two entries with the word 'turn' in the title When I search by 'turn' in title Then Right Turn And Left Turn should be returned")
85+
void findByTitleContainingIgnoreCase() {
86+
var turns = notes.findByTitleContainingIgnoreCase("turn");
87+
88+
assertThat(turns)
89+
.extracting("title")
90+
.containsOnly("Right Turn", "Left Turn");
91+
}
92+
}
93+
----
94+
95+
The test verifies that our repository method `findByTitleContainingIgnoreCase` correctly finds notes with titles containing the word "turn", ignoring case.
96+
97+
== Conclusion
98+
99+
The `@RepositoryDefinition` annotation provides a way to create custom repository interfaces with only the methods you need, without inheriting all the methods from base interfaces. This gives you more control over your repository's API and makes your code more explicit about what operations are supported.
100+
101+
While extending base interfaces like `CrudRepository` or `JpaRepository` is convenient for most cases, using `@RepositoryDefinition` can be a good choice when you want to limit the operations that can be performed on your entities or when you want to create a more focused and explicit API.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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'
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+
runtimeOnly 'org.postgresql:postgresql'
23+
testImplementation 'org.springframework.boot:spring-boot-starter-test'
24+
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
25+
testImplementation 'org.testcontainers:junit-jupiter'
26+
testImplementation 'org.testcontainers:postgresql'
27+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
28+
}
29+
30+
tasks.named('test') {
31+
useJUnitPlatform()
32+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = 'data-repository-definition'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package zin.rashidi.data.repositorydefinition;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class DataRepositoryDefinitionApplication {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(DataRepositoryDefinitionApplication.class, args);
11+
}
12+
13+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package zin.rashidi.data.repositorydefinition.note;
2+
3+
import org.springframework.data.annotation.Id;
4+
5+
/**
6+
* @author Rashidi Zin
7+
*/
8+
record Note(@Id Long id, String title, String content) {
9+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package zin.rashidi.data.repositorydefinition.note;
2+
3+
import org.springframework.data.repository.RepositoryDefinition;
4+
5+
import java.util.List;
6+
7+
/**
8+
* @author Rashidi Zin
9+
*/
10+
@RepositoryDefinition(domainClass = Note.class, idClass = Long.class)
11+
interface NoteRepository {
12+
13+
List<Note> findByTitleContainingIgnoreCase(String title);
14+
15+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
spring.application.name=data-repository-definition
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package zin.rashidi.data.repositorydefinition;
2+
3+
import org.springframework.boot.SpringApplication;
4+
5+
public class TestDataRepositoryDefinitionApplication {
6+
7+
public static void main(String[] args) {
8+
SpringApplication.from(DataRepositoryDefinitionApplication::main).with(TestcontainersConfiguration.class).run(args);
9+
}
10+
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package zin.rashidi.data.repositorydefinition;
2+
3+
import org.springframework.boot.test.context.TestConfiguration;
4+
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
5+
import org.springframework.context.annotation.Bean;
6+
import org.testcontainers.containers.PostgreSQLContainer;
7+
import org.testcontainers.utility.DockerImageName;
8+
9+
@TestConfiguration(proxyBeanMethods = false)
10+
public class TestcontainersConfiguration {
11+
12+
@Bean
13+
@ServiceConnection
14+
PostgreSQLContainer<?> postgresContainer() {
15+
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"));
16+
}
17+
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package zin.rashidi.data.repositorydefinition.note;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Test;
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest;
7+
import org.springframework.context.annotation.Import;
8+
import org.springframework.test.context.jdbc.Sql;
9+
import org.springframework.test.context.jdbc.SqlMergeMode;
10+
import zin.rashidi.data.repositorydefinition.TestcontainersConfiguration;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS;
14+
import static org.springframework.test.context.jdbc.SqlMergeMode.MergeMode.MERGE;
15+
16+
/**
17+
* @author Rashidi Zin
18+
*/
19+
@Import(TestcontainersConfiguration.class)
20+
@DataJdbcTest
21+
@SqlMergeMode(MERGE)
22+
@Sql(statements = "CREATE TABLE note (id BIGINT PRIMARY KEY, title VARCHAR(50), content TEXT);", executionPhase = BEFORE_TEST_CLASS)
23+
class NoteRepositoryTests {
24+
25+
@Autowired
26+
private NoteRepository notes;
27+
28+
@Test
29+
@Sql(statements = {
30+
"INSERT INTO note (id, title, content) VALUES ('1', 'Right Turn', 'Step forward. Step forward and turn right. Collect.')",
31+
"INSERT INTO note (id, title, content) VALUES ('2', 'Left Turn', 'Step forward. Reverse and turn left. Collect.')",
32+
"INSERT INTO note (id, title, content) VALUES ('3', 'Double Spin', 'Syncopated. Double spin. Collect.')"
33+
})
34+
@DisplayName("Given there are two entries with the word 'turn' in the title When I search by 'turn' in title Then Right Turn And Left Turn should be returned")
35+
void findByTitleContainingIgnoreCase() {
36+
var turns = notes.findByTitleContainingIgnoreCase("turn");
37+
38+
assertThat(turns)
39+
.extracting("title")
40+
.containsOnly("Right Turn", "Left Turn");
41+
}
42+
43+
}

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ include('data-mongodb-audit')
1313
include('data-mongodb-full-text-search')
1414
include('data-mongodb-tc-data-load')
1515
include('data-mongodb-transactional')
16+
include('data-repository-definition')
1617
include('data-rest-validation')
1718
include('graphql')
1819
include('jooq')

0 commit comments

Comments
 (0)