Skip to content

Commit 931a895

Browse files
committed
feat(endpoint-userfilter): add jpa for api criteria
1 parent b748631 commit 931a895

7 files changed

Lines changed: 151 additions & 65 deletions

File tree

pom.xml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
</dependency>
5151
<dependency>
5252
<groupId>org.springframework.boot</groupId>
53-
<artifactId>spring-boot-starter-data-jdbc</artifactId>
53+
<artifactId>spring-boot-starter-data-jpa</artifactId>
5454
</dependency>
5555
<dependency>
5656
<groupId>org.springframework.boot</groupId>
@@ -78,11 +78,6 @@
7878
<groupId>org.springframework.boot</groupId>
7979
<artifactId>spring-boot-starter-actuator-test</artifactId>
8080
<scope>test</scope>
81-
</dependency>
82-
<dependency>
83-
<groupId>org.springframework.boot</groupId>
84-
<artifactId>spring-boot-starter-data-jdbc-test</artifactId>
85-
<scope>test</scope>
8681
</dependency>
8782
<dependency>
8883
<groupId>org.springframework.boot</groupId>

src/main/java/com/xpeho/spring_boot_java_random_user/data/models/database/User.java

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
11
package com.xpeho.spring_boot_java_random_user.data.models.database;
22

3-
import org.springframework.data.annotation.Id;
4-
import org.springframework.data.relational.core.mapping.Column;
5-
import org.springframework.data.relational.core.mapping.Table;
6-
7-
@Table("users")
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.Table;
9+
10+
@Entity
11+
@Table(name = "users")
812
public class User {
913
@Id
10-
@Column("id")
14+
@GeneratedValue(strategy = GenerationType.IDENTITY)
15+
@Column(name = "id")
1116
private Long id;
12-
@Column("gender")
17+
@Column(name = "gender")
1318
private String gender;
14-
@Column("firstname")
19+
@Column(name = "firstname")
1520
private String firstname;
16-
@Column("lastname")
21+
@Column(name = "lastname")
1722
private String lastname;
18-
@Column("civility")
23+
@Column(name = "civility")
1924
private String civility;
20-
@Column("email")
25+
@Column(name = "email")
2126
private String email;
22-
@Column("phone")
27+
@Column(name = "phone")
2328
private String phone;
24-
@Column("picture")
29+
@Column(name = "picture")
2530
private String picture;
26-
@Column("nationality")
31+
@Column(name = "nationality")
2732
private String nationality;
2833

29-
// Required by Spring Data JDBC to instantiate the entity via reflection
34+
// Required by JPA
3035
public User() {
31-
// No initialization needed; fields are populated by Spring Data JDBC after instantiation
36+
// No initialization needed
3237
}
3338

3439
public Long getId() {

src/main/java/com/xpeho/spring_boot_java_random_user/data/services/UserServiceImpl.java

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.xpeho.spring_boot_java_random_user.data.converters.UserConverter;
44
import com.xpeho.spring_boot_java_random_user.data.models.database.User;
55
import com.xpeho.spring_boot_java_random_user.data.sources.database.UserRepository;
6+
import com.xpeho.spring_boot_java_random_user.data.sources.database.UserSpecifications;
67
import com.xpeho.spring_boot_java_random_user.domain.entities.UserEntity;
78
import com.xpeho.spring_boot_java_random_user.domain.entities.UserFilter;
89
import com.xpeho.spring_boot_java_random_user.domain.services.LocalUserService;
@@ -51,17 +52,7 @@ public void deleteById(long id) {
5152

5253
@Override
5354
public List<UserEntity> filterUsers(UserFilter filter) {
54-
String gender = filter.gender() != null ? filter.gender().name().toLowerCase() : null;
55-
56-
return userRepository.findByFilters(
57-
gender,
58-
filter.firstname(),
59-
filter.lastname(),
60-
filter.civility(),
61-
filter.email(),
62-
filter.phone(),
63-
filter.nat()
64-
).stream()
55+
return userRepository.findAll(UserSpecifications.byFilter(filter)).stream()
6556
.map(userConverter::toDomain)
6657
.toList();
6758
}
Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,8 @@
11
package com.xpeho.spring_boot_java_random_user.data.sources.database;
22

33
import com.xpeho.spring_boot_java_random_user.data.models.database.User;
4-
import org.springframework.data.jdbc.repository.query.Query;
5-
import org.springframework.data.repository.CrudRepository;
6-
import org.springframework.data.repository.query.Param;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
76

8-
import java.util.List;
9-
10-
public interface UserRepository extends CrudRepository<User, Long> {
11-
12-
@Query("SELECT * FROM users WHERE " +
13-
"(:gender IS NULL OR LOWER(gender) = LOWER(:gender)) AND " +
14-
"(:firstname IS NULL OR LOWER(firstname) LIKE LOWER(CONCAT('%', :firstname, '%'))) AND " +
15-
"(:lastname IS NULL OR LOWER(lastname) LIKE LOWER(CONCAT('%', :lastname, '%'))) AND " +
16-
"(:civility IS NULL OR civility LIKE CONCAT('%', :civility, '%')) AND " +
17-
"(:email IS NULL OR email LIKE CONCAT('%', :email, '%')) AND " +
18-
"(:phone IS NULL OR phone LIKE CONCAT('%', :phone, '%')) AND " +
19-
"(:nationality IS NULL OR LOWER(nationality) LIKE LOWER(CONCAT('%', :nationality, '%')))")
20-
List<User> findByFilters(
21-
@Param("gender") String gender,
22-
@Param("firstname") String firstname,
23-
@Param("lastname") String lastname,
24-
@Param("civility") String civility,
25-
@Param("email") String email,
26-
@Param("phone") String phone,
27-
@Param("nationality") String nationality
28-
);
7+
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
298
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.xpeho.spring_boot_java_random_user.data.sources.database;
2+
3+
import com.xpeho.spring_boot_java_random_user.data.models.database.User;
4+
import com.xpeho.spring_boot_java_random_user.domain.entities.UserFilter;
5+
import jakarta.persistence.criteria.CriteriaBuilder;
6+
import jakarta.persistence.criteria.Path;
7+
import jakarta.persistence.criteria.Predicate;
8+
import org.springframework.data.jpa.domain.Specification;
9+
import org.springframework.util.StringUtils;
10+
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
14+
public final class UserSpecifications {
15+
private UserSpecifications() {
16+
}
17+
18+
public static Specification<User> byFilter(UserFilter filter) {
19+
return (root, query, criteriaBuilder) -> {
20+
List<Predicate> predicates = new ArrayList<>();
21+
22+
if (filter.gender() != null) {
23+
predicates.add(criteriaBuilder.equal(
24+
criteriaBuilder.lower(root.get("gender")),
25+
filter.gender().name().toLowerCase()
26+
));
27+
}
28+
29+
addContainsPredicate(predicates, criteriaBuilder, root.get("firstname"), filter.firstname());
30+
addContainsPredicate(predicates, criteriaBuilder, root.get("lastname"), filter.lastname());
31+
addContainsPredicate(predicates, criteriaBuilder, root.get("civility"), filter.civility());
32+
addContainsPredicate(predicates, criteriaBuilder, root.get("email"), filter.email());
33+
addContainsPredicate(predicates, criteriaBuilder, root.get("phone"), filter.phone());
34+
addContainsPredicate(predicates, criteriaBuilder, root.get("nationality"), filter.nat());
35+
36+
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
37+
};
38+
}
39+
40+
private static void addContainsPredicate(
41+
List<Predicate> predicates,
42+
CriteriaBuilder criteriaBuilder,
43+
Path<String> field,
44+
String value
45+
) {
46+
if (!StringUtils.hasText(value)) {
47+
return;
48+
}
49+
50+
predicates.add(criteriaBuilder.like(
51+
criteriaBuilder.lower(field),
52+
"%" + value.toLowerCase() + "%"
53+
));
54+
}
55+
}

src/test/java/com/xpeho/spring_boot_java_random_user/data/services/UserServiceImplTest.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.junit.jupiter.api.BeforeEach;
1010
import org.junit.jupiter.api.DisplayName;
1111
import org.junit.jupiter.api.Test;
12+
import org.springframework.data.jpa.domain.Specification;
1213

1314
import java.util.Collections;
1415
import java.util.List;
@@ -90,7 +91,7 @@ void shouldSaveMappedUserAndReturnMappedDomainEntity() {
9091
}
9192

9293
@Test
93-
@DisplayName("Should convert gender enum to lowercase and call repository with filter values")
94+
@DisplayName("Should build a specification and call repository for filtered users")
9495
void shouldFilterUsersWithGender() {
9596
UserFilter filter = new UserFilter(Gender.MALE, "John", null, null, null, null, null);
9697

@@ -100,20 +101,20 @@ void shouldFilterUsersWithGender() {
100101

101102
UserEntity expected = new UserEntity(1L, "male", "John", "Doe", "Mr", "john@doe.com", "1234", "pic.jpg", "FR");
102103

103-
when(userRepository.findByFilters("male", "John", null, null, null, null, null))
104+
when(userRepository.findAll(org.mockito.ArgumentMatchers.<Specification<User>>any()))
104105
.thenReturn(List.of(dao));
105106
when(userConverter.toDomain(dao)).thenReturn(expected);
106107

107108
List<UserEntity> result = userService.filterUsers(filter);
108109

109110
assertEquals(1, result.size());
110111
assertEquals(expected, result.get(0));
111-
verify(userRepository).findByFilters("male", "John", null, null, null, null, null);
112+
verify(userRepository).findAll(org.mockito.ArgumentMatchers.<Specification<User>>any());
112113
verify(userConverter).toDomain(dao);
113114
}
114115

115116
@Test
116-
@DisplayName("Should pass null gender when filter gender is null")
117+
@DisplayName("Should call repository when gender filter is null")
117118
void shouldFilterUsersWithNullGender() {
118119
UserFilter filter = new UserFilter(null, null, "Smith", null, null, null, null);
119120

@@ -123,28 +124,28 @@ void shouldFilterUsersWithNullGender() {
123124

124125
UserEntity expected = new UserEntity(2L, "female", "Alice", "Smith", "Ms", "alice@smith.com", "5678", "pic2.jpg", "US");
125126

126-
when(userRepository.findByFilters(null, null, "Smith", null, null, null, null))
127+
when(userRepository.findAll(org.mockito.ArgumentMatchers.<Specification<User>>any()))
127128
.thenReturn(List.of(dao));
128129
when(userConverter.toDomain(dao)).thenReturn(expected);
129130

130131
List<UserEntity> result = userService.filterUsers(filter);
131132

132133
assertEquals(1, result.size());
133134
assertEquals(expected, result.get(0));
134-
verify(userRepository).findByFilters(null, null, "Smith", null, null, null, null);
135+
verify(userRepository).findAll(org.mockito.ArgumentMatchers.<Specification<User>>any());
135136
}
136137

137138
@Test
138139
@DisplayName("Should return empty list when no users match filter")
139140
void shouldReturnEmptyListWhenNoUsersMatchFilter() {
140141
UserFilter filter = new UserFilter(Gender.FEMALE, "Unknown", null, null, null, null, null);
141142

142-
when(userRepository.findByFilters("female", "Unknown", null, null, null, null, null))
143+
when(userRepository.findAll(org.mockito.ArgumentMatchers.<Specification<User>>any()))
143144
.thenReturn(Collections.emptyList());
144145

145146
List<UserEntity> result = userService.filterUsers(filter);
146147

147148
assertTrue(result.isEmpty());
148-
verify(userRepository).findByFilters("female", "Unknown", null, null, null, null, null);
149+
verify(userRepository).findAll(org.mockito.ArgumentMatchers.<Specification<User>>any());
149150
}
150151
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.xpeho.spring_boot_java_random_user.data.sources.database;
2+
3+
import com.xpeho.spring_boot_java_random_user.data.models.database.User;
4+
import com.xpeho.spring_boot_java_random_user.domain.entities.UserFilter;
5+
import com.xpeho.spring_boot_java_random_user.domain.enums.Gender;
6+
import jakarta.persistence.criteria.*;
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.DisplayName;
9+
import org.junit.jupiter.api.Test;
10+
import org.springframework.data.jpa.domain.Specification;
11+
12+
import static org.mockito.Mockito.*;
13+
14+
class UserSpecificationsTest {
15+
private Root<User> user;
16+
private CriteriaQuery<?> query;
17+
private CriteriaBuilder cb;
18+
private Expression<String> lowerExpr;
19+
private Predicate predicate;
20+
21+
@BeforeEach
22+
@SuppressWarnings("unchecked")
23+
void setUp() {
24+
user = mock(Root.class);
25+
query = mock(CriteriaQuery.class);
26+
cb = mock(CriteriaBuilder.class);
27+
lowerExpr = mock(Expression.class);
28+
predicate = mock(Predicate.class);
29+
when(cb.lower(any())).thenReturn(lowerExpr);
30+
when(cb.equal(any(), anyString())).thenReturn(predicate);
31+
when(cb.like(any(Expression.class), anyString())).thenReturn(predicate);
32+
when(cb.and(any(Predicate[].class))).thenReturn(predicate);
33+
}
34+
35+
36+
37+
@Test
38+
@DisplayName("Should add like predicates for all text fields")
39+
void shouldAddLikePredicatesForAllTextFields() {
40+
UserFilter filter = new UserFilter(null, "John", "Doe", "Mr", "john@doe.com", "1234", "FR");
41+
42+
Specification<User> spec = UserSpecifications.byFilter(filter);
43+
spec.toPredicate(user, query, cb);
44+
45+
verify(user).get("firstname");
46+
verify(user).get("lastname");
47+
verify(user).get("civility");
48+
verify(user).get("email");
49+
verify(user).get("phone");
50+
verify(user).get("nationality");
51+
verify(cb).like(lowerExpr, "%john%");
52+
verify(cb).like(lowerExpr, "%doe%");
53+
verify(cb).like(lowerExpr, "%mr%");
54+
verify(cb).like(lowerExpr, "%john@doe.com%");
55+
verify(cb).like(lowerExpr, "%1234%");
56+
verify(cb).like(lowerExpr, "%fr%");
57+
}
58+
59+
60+
}

0 commit comments

Comments
 (0)