Skip to content

Commit e9298ac

Browse files
authored
BAEL-8794: Dynamic Spring Data JPA Repository Query with Arbitrary AND Clauses (#18066)
1 parent e1d5213 commit e9298ac

File tree

9 files changed

+291
-0
lines changed

9 files changed

+291
-0
lines changed

spring-boot-modules/spring-boot-data-3/pom.xml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@
4343
<artifactId>spring-boot-starter-test</artifactId>
4444
<scope>test</scope>
4545
</dependency>
46+
<dependency>
47+
<groupId>com.querydsl</groupId>
48+
<artifactId>querydsl-jpa</artifactId>
49+
<version>${querydsl.version}</version>
50+
<classifier>jakarta</classifier>
51+
</dependency>
52+
<dependency>
53+
<groupId>com.querydsl</groupId>
54+
<artifactId>querydsl-apt</artifactId>
55+
<version>${querydsl.version}</version>
56+
<classifier>jakarta</classifier>
57+
<scope>provided</scope>
58+
</dependency>
4659
<dependency>
4760
<groupId>org.openjfx</groupId>
4861
<artifactId>javafx-controls</artifactId>
@@ -53,6 +66,12 @@
5366
<artifactId>javafx-fxml</artifactId>
5467
<version>${javafx.version}</version>
5568
</dependency>
69+
<dependency>
70+
<groupId>org.projectlombok</groupId>
71+
<artifactId>lombok</artifactId>
72+
<version>${lombok.version}</version>
73+
<scope>compile</scope>
74+
</dependency>
5675
</dependencies>
5776

5877
<build>
@@ -61,12 +80,38 @@
6180
<groupId>org.springframework.boot</groupId>
6281
<artifactId>spring-boot-maven-plugin</artifactId>
6382
</plugin>
83+
<plugin>
84+
<groupId>org.apache.maven.plugins</groupId>
85+
<artifactId>maven-compiler-plugin</artifactId>
86+
<configuration>
87+
<source>11</source>
88+
<target>11</target>
89+
</configuration>
90+
</plugin>
91+
<plugin>
92+
<groupId>com.mysema.maven</groupId>
93+
<artifactId>apt-maven-plugin</artifactId>
94+
<version>1.1.3</version>
95+
<executions>
96+
<execution>
97+
<phase>generate-sources</phase>
98+
<goals>
99+
<goal>process</goal>
100+
</goals>
101+
<configuration>
102+
<outputDirectory>target/generated-sources</outputDirectory>
103+
<processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
104+
</configuration>
105+
</execution>
106+
</executions>
107+
</plugin>
64108
</plugins>
65109
</build>
66110

67111
<properties>
68112
<start-class>com.baeldung.startwithoutdb.StartWithoutDbApplication</start-class>
69113
<hypersistence-utils-hibernate-60.version>3.7.3</hypersistence-utils-hibernate-60.version>
114+
<querydsl.version>5.1.0</querydsl.version>
70115
<javafx.version>19</javafx.version>
71116
</properties>
72117

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.baeldung.dynamicquery;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
6+
7+
@SpringBootApplication
8+
@EnableJpaRepositories("com.baeldung.dynamicquery.repository")
9+
public class DynamicQueryApplication {
10+
11+
public static void main(String[] args) {
12+
SpringApplication.run(DynamicQueryApplication.class, args);
13+
}
14+
15+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.baeldung.dynamicquery.model;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.GeneratedValue;
6+
import jakarta.persistence.GenerationType;
7+
import jakarta.persistence.Id;
8+
import jakarta.persistence.OneToMany;
9+
import jakarta.persistence.Table;
10+
import lombok.AllArgsConstructor;
11+
import lombok.Builder;
12+
import lombok.EqualsAndHashCode;
13+
import lombok.Getter;
14+
import lombok.NoArgsConstructor;
15+
import lombok.Setter;
16+
17+
import java.util.List;
18+
19+
@Entity
20+
@Table
21+
@Getter
22+
@Setter
23+
@EqualsAndHashCode(of = "id")
24+
@Builder
25+
@NoArgsConstructor
26+
@AllArgsConstructor
27+
public class School {
28+
29+
@Id
30+
@GeneratedValue(strategy = GenerationType.IDENTITY)
31+
@Column
32+
private Long id;
33+
34+
@Column
35+
private String name;
36+
37+
@Column
38+
private String borough;
39+
40+
@OneToMany(mappedBy = "school")
41+
private List<Student> studentList;
42+
43+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.baeldung.dynamicquery.model;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.GeneratedValue;
6+
import jakarta.persistence.GenerationType;
7+
import jakarta.persistence.Id;
8+
import jakarta.persistence.ManyToOne;
9+
import jakarta.persistence.OneToMany;
10+
import jakarta.persistence.Table;
11+
import lombok.AllArgsConstructor;
12+
import lombok.Builder;
13+
import lombok.EqualsAndHashCode;
14+
import lombok.Getter;
15+
import lombok.NoArgsConstructor;
16+
import lombok.Setter;
17+
18+
@Entity
19+
@Table
20+
@Getter
21+
@Setter
22+
@EqualsAndHashCode(of = "id")
23+
@Builder
24+
@NoArgsConstructor
25+
@AllArgsConstructor
26+
public class Student {
27+
28+
@Id
29+
@GeneratedValue(strategy = GenerationType.IDENTITY)
30+
@Column
31+
private Long id;
32+
33+
@Column
34+
private String name;
35+
36+
@Column
37+
private Integer age;
38+
39+
@ManyToOne
40+
private School school;
41+
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.baeldung.dynamicquery.repository;
2+
3+
import com.baeldung.dynamicquery.model.School;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface SchoolRepository extends JpaRepository<School, Long> {
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.baeldung.dynamicquery.repository;
2+
3+
import com.baeldung.dynamicquery.model.Student;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
6+
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
7+
8+
public interface StudentRepository extends JpaRepository<Student, Long>, QuerydslPredicateExecutor<Student>, JpaSpecificationExecutor<Student> {
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.baeldung.dynamicquery.spec;
2+
3+
import com.baeldung.dynamicquery.model.School;
4+
import com.baeldung.dynamicquery.model.Student;
5+
import jakarta.persistence.criteria.Join;
6+
import org.springframework.data.jpa.domain.Specification;
7+
8+
public class StudentSpecification {
9+
10+
public static Specification<Student> nameEndsWithIgnoreCase(String name) {
11+
return (root, query, criteriaBuilder) ->
12+
criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), "%" + name.toLowerCase());
13+
}
14+
15+
public static Specification<Student> isAge(int age) {
16+
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("age"), age);
17+
}
18+
19+
public static Specification<Student> isSchoolBorough(String borough) {
20+
return (root, query, criteriaBuilder) -> {
21+
Join<Student, School> scchoolJoin = root.join("school");
22+
return criteriaBuilder.equal(scchoolJoin.get("borough"), borough);
23+
};
24+
}
25+
26+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
spring.datasource.url=jdbc:h2:mem:testdb
2+
spring.datasource.driverClassName=org.h2.Driver
3+
spring.datasource.username=sa
4+
spring.datasource.password=
5+
spring.h2.console.enabled=true
6+
7+
# JPA Configuration
8+
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
9+
spring.jpa.hibernate.ddl-auto=update
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.baeldung.dynamicquery;
2+
3+
import com.baeldung.dynamicquery.model.QStudent;
4+
import com.baeldung.dynamicquery.model.School;
5+
import com.baeldung.dynamicquery.model.Student;
6+
import com.baeldung.dynamicquery.repository.SchoolRepository;
7+
import com.baeldung.dynamicquery.repository.StudentRepository;
8+
import com.baeldung.dynamicquery.spec.StudentSpecification;
9+
import com.querydsl.core.types.dsl.*;
10+
import jakarta.inject.Inject;
11+
import java.util.List;
12+
import org.junit.jupiter.api.BeforeAll;
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.TestInstance;
15+
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
16+
import org.springframework.data.domain.Example;
17+
import org.springframework.data.domain.ExampleMatcher;
18+
import org.springframework.data.jpa.domain.Specification;
19+
import org.springframework.test.context.ActiveProfiles;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
23+
@DataJpaTest
24+
@ActiveProfiles("dynamicquery")
25+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
26+
public class DynamicQueryIntegrationTest {
27+
28+
@Inject
29+
private SchoolRepository schoolRepository;
30+
31+
@Inject
32+
private StudentRepository studentRepository;
33+
34+
@BeforeAll
35+
void setup() {
36+
School school1 = schoolRepository.save(School.builder()
37+
.name("University of West London")
38+
.borough("Ealing")
39+
.build());
40+
41+
School school2 = schoolRepository.save(School.builder()
42+
.name("Kingston University")
43+
.borough("Kingston upon Thames")
44+
.build());
45+
46+
studentRepository.saveAll(List.of(
47+
Student.builder().name("Emily Smith").age(20).school(school2).build(),
48+
Student.builder().name("James Smith").age(20).school(school1).build(),
49+
Student.builder().name("Maria Johnson").age(22).school(school1).build(),
50+
Student.builder().name("Michael Brown").age(21).school(school1).build(),
51+
Student.builder().name("Sophia Smith").age(22).school(school1).build()
52+
));
53+
}
54+
55+
@Test
56+
void givenQueryByExample_whenSelectExample_thenReturnStudentsWhoAreAge20() {
57+
School schoolExample = new School();
58+
schoolExample.setBorough("Ealing");
59+
60+
Student studentExample = new Student();
61+
studentExample.setAge(20);
62+
studentExample.setName("Smith");
63+
studentExample.setSchool(schoolExample);
64+
65+
ExampleMatcher customExampleMatcher = ExampleMatcher.matching()
66+
.withMatcher("name", ExampleMatcher.GenericPropertyMatchers.endsWith().ignoreCase());
67+
68+
Example<Student> example = Example.of(studentExample, customExampleMatcher);
69+
List<Student> studentList = studentRepository.findAll(example);
70+
assertThat(studentList).hasSize(1);
71+
}
72+
73+
@Test
74+
void givenQueryBySpecification_whenSelectByDsl_thenReturn() {
75+
Specification<Student> studentSpec = Specification
76+
.where(StudentSpecification.nameEndsWithIgnoreCase("smith"))
77+
.and(StudentSpecification.isAge(20))
78+
.and(StudentSpecification.isSchoolBorough("Ealing"));
79+
80+
List<Student> studentList = studentRepository.findAll(studentSpec);
81+
assertThat(studentList).hasSize(1);
82+
}
83+
84+
@Test
85+
void givenQueryByQueryDsl_whenSelectByDsl_thenReturn() {
86+
QStudent qStudent = QStudent.student;
87+
BooleanExpression predicate = qStudent.name.endsWithIgnoreCase("smith")
88+
.and(qStudent.age.eq(20))
89+
.and(qStudent.school.borough.eq("Ealing"));
90+
91+
List<Student> studentList = (List<Student>) studentRepository.findAll(predicate);
92+
assertThat(studentList).hasSize(1);
93+
}
94+
95+
}

0 commit comments

Comments
 (0)