Skip to content

Commit 7b17405

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

6 files changed

Lines changed: 92 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: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
import java.util.Locale;
14+
15+
public final class UserSpecifications {
16+
private UserSpecifications() {
17+
}
18+
19+
public static Specification<User> byFilter(UserFilter filter) {
20+
return (root, query, criteriaBuilder) -> {
21+
List<Predicate> predicates = new ArrayList<>();
22+
23+
if (filter.gender() != null) {
24+
predicates.add(criteriaBuilder.equal(
25+
criteriaBuilder.lower(root.get("gender")),
26+
filter.gender().name().toLowerCase(Locale.ROOT)
27+
));
28+
}
29+
30+
addContainsPredicate(predicates, criteriaBuilder, root.get("firstname"), filter.firstname());
31+
addContainsPredicate(predicates, criteriaBuilder, root.get("lastname"), filter.lastname());
32+
addContainsPredicate(predicates, criteriaBuilder, root.get("civility"), filter.civility());
33+
addContainsPredicate(predicates, criteriaBuilder, root.get("email"), filter.email());
34+
addContainsPredicate(predicates, criteriaBuilder, root.get("phone"), filter.phone());
35+
addContainsPredicate(predicates, criteriaBuilder, root.get("nationality"), filter.nat());
36+
37+
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
38+
};
39+
}
40+
41+
private static void addContainsPredicate(
42+
List<Predicate> predicates,
43+
CriteriaBuilder criteriaBuilder,
44+
Path<String> field,
45+
String value
46+
) {
47+
if (!StringUtils.hasText(value)) {
48+
return;
49+
}
50+
51+
predicates.add(cb.like(
52+
criteriaBuilder.lower(field),
53+
"%" + value.toLowerCase(Locale.ROOT) + "%"
54+
));
55+
}
56+
}

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
}

0 commit comments

Comments
 (0)