728x90
๐ค ๋ค์ค ์กฐ๊ฑด ๊ฒ์์ด๋?
- ๋ค์ค ์กฐ๊ฑด ๊ฒ์์ด๋ ์นดํ ๊ณ ๋ฆฌ ๊ฒ์, ์์ธ ๊ฒ์ ๋ฑ๊ณผ ๊ฐ์ด ์ผํ๋ชฐ ๋ฑ์์ ์ฌ๋ฌ ํํฐ ์กฐ๊ฑด์ผ๋ก ๊ฒ์๋ ๊ฒฐ๊ณผ๋ฅผ ์ถ์์์ผ ์ฌ์ฉ์๊ฐ ์ํ๋ ๊ฒฐ๊ด๊ฐ๋ง์ ๋์ถํ๊ธฐ ์ํด ์ฌ์ฉํ๋ค.
- ์ด๋ฅผ ์ด๋ป๊ฒ ๊ตฌํํ ์ ์์๊น ๊ณ ๋ฏผํด๋ณด๋ฉด ํ ๊ฐ์ง ๋ฐฉ๋ฒ์ผ๋ก ๊ท๊ฒฐ๋๋๋ฐ ๋ฐ๋ก '๋์ ์ฟผ๋ฆฌ'์ด๋ค.
- ์ฌ์ฉ์๊ฐ ์ํ๋ ํํฐ๋ง์ ๋ง์ถฐ ์กฐํ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ ค์ฃผ๋ฉด ๋๋ ๊ฒ์ธ๋ฐ ๊ฒฐ๊ตญ ๋งค ์์ฒญ๋ง๋ค ์ฟผ๋ฆฌ๋ฌธ์ด ๋ฌ๋ผ์ง๊ฒ ๋๋ค๋ ์ด์ผ๊ธฐ์ด๋ฏ๋ก Jpa์์ ์ ๊ณตํ๋ ๊ธฐ๋ณธ ๋ฉ์๋์ ๊ฐ์ด ์ ํด์ง ์ฟผ๋ฆฌ์ value๋ง ๋ค๋ฅด๊ฒ ๋ณด๋ด๋ ๊ฒ๊ณผ๋ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ ์ฌ์ฉํด์ผ ํ๋ค.
๐ ํ ์คํธ์ ์ฌ์ฉ๋ View
- member ํ ์ด๋ธ๊ณผ team ํ ์ด๋ธ์์ ์ถ์ถํ์ฌ ๋ง๋ view์ด๋ค.
- view๋ ํ ์ด๋ธ ๊ตฌ์กฐ์ ๋ํด ์์ธํ ์๊ณ ์ถ๋ค๋ฉด ์ด ๊ธ์ ์ฐธ๊ณ ํ์.
๐ก ๋ํ์ ์ธ ๋ฐฉ๋ฒ ์ธ ๊ฐ์ง
๋ฌธ์์ด ์ฟผ๋ฆฌ ๋ง๋ค๊ธฐ
String jpql = "select * from TestView v"; // JPQL ๊ธฐ๋ณธ ์ฟผ๋ฆฌ
if (memberName != null) {
jpql += " where v.memberName = :memberName"; // ๋์ ์ผ๋ก ์กฐ๊ฑด ์ถ๊ฐ
}
// EntityManager๋ฅผ ์ฌ์ฉํ์ฌ JPQL ์ฟผ๋ฆฌ ์คํ
TypedQuery<TestView> query = entityManager.createQuery(jpql, View.class);
if (memberName != null) {
query.setParameter("memberName", memberName); // ๋์ ์ผ๋ก ํ๋ผ๋ฏธํฐ ์ค์
}
List<TestView> results = query.getResultList();
- ๋ง ๊ทธ๋๋ก ๋ฌธ์์ด๋ก ์ฟผ๋ฆฌ๋ฅผ ๋ง๋ค์ด๋ด์ด @query ์ ๋ํ ์ด์ ์ ์ฃ๋ ๊ฒ์ด๋ค.
- jpql์ ์๋ก ๋ ๋ค๋ฉด ์์ ๋น์ทํ ๋ก์ง์ด ๋์ค๋๋ฐ ์ง๊ด์ ์ด์ง๋ง ์ฐ์ฐ์ด ์ถ๊ฐ๋๊ฑฐ๋ ๋ณต์กํด์ง์๋ก ์ฝ๋๊ฐ ๊ธธ์ด์ง๊ณ ๋ ธ๊ฐ๋ค ์์ ์ด ์ฌํด์ง๊ณ ๋ฌธ์์ด์ ๋ค๋ฃจ๊ธฐ ๋๋ฌธ์ ์ค์์ ์ฝ๊ฒ ๋ ธ์ถ๋๋ฉฐ ์ ์ง๋ณด์์๋ ์ด๋ ค์์ด ์๋ค.
Querydsl
- Querydsl์ ์๋ฐ ๊ธฐ๋ฐ์ ๋์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๊ธฐ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก SQL๊ณผ ์ ์ฌํ ๋ฌธ๋ฒ์ ์ฌ์ฉํ์ฌ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ์ ์๋ค.
- ์ปดํ์ผ ์์ ์ ํ์ ์ฒดํฌ๊ฐ ๊ฐ๋ฅํ๊ณ ๊ฐ๋ ์ฑ์ด ์ข์ผ๋ฉฐ IDE์ ์ง์์ ๋ฐ์ ์ ์๋ค.
- ๋์ ๊ฒฝ์ฐ์๋ ํ์ฌ ์ฌ๋ด์์ ์ฌ์ฉํ์ง ์๊ณ ์ธ๋ถ ๋ชจ๋๋ก ๋ณ๋์ ์์กด์ฑ ์ถ๊ฐ๊ฐ ํ์ํ๋ค๋ ์ ์์ ๋ฐฐ์ ํ๋ค.
- ํ์ง๋ง ์ธ์ ๊ฐ ํ์์ ์ํด Querydsl์ด ๋์ ๋๋ค๋ฉด ์ ๊ทน์ ์ผ๋ก ๋ฆฌํฉํ ๋ง์ ์ํ ์๊ฐ์ด๋ค.
CreteriaQuery & JPA Specification
CreteriaQuery
- CriteriaQuery๋ JPA์์ ์ฌ์ฉ๋๋ ์ฟผ๋ฆฌ ์์ฑ์ ์ํ ์ธํฐํ์ด์ค์ด๋ค.
- CriteriaQuery๋ฅผ ์ฌ์ฉํ๋ฉด SQL ์ฟผ๋ฆฌ๋ฅผ ์ง์ ์์ฑํ๋ ๋์ ์ฝ๋๋ก ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ์ ์๋ค.
- ๊ฐ์ฒด ์งํฅ์ ์ธ ๋ฐฉ์์ผ๋ก ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ์ ์๋ค.
- CriteriaQuery๋ CriteriaBuilder๋ฅผ ์ฌ์ฉํ์ฌ ์์ฑํ๊ณ select, from, where, orderBy ๋ฑ์ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์ฟผ๋ฆฌ์ ๋ฐํ ํ์ , ์กฐ๊ฑด, ์ ๋ ฌ ๋ฑ์ ์ค์ ํ๋ค.
JPA Specification
- Spring Data JPA์์๋ Specification์ด๋ผ๋ ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํ๋ค.
- ๋ฐ๋ผ์ Querydsl์ฒ๋ผ ๋ณ๋์ ์ธํ ์ด ํ์์๋ค.
- CriteriaQuery๋ฅผ ์ฌ์ฉํ์ฌ ๋์ ์ธ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๋๋ฐ CriteriaQuery๋ฅผ ๋ ๊ฐํธํ๊ฒ ์์ฑํ ์ ์๊ฒ ๋์์ค๋ค.
- Specification์ ์ฌ์ฉํ๋ฉด ๋์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๊ธฐ ์ํ ์กฐ๊ฑด์ ๋ฉ์๋๋ก ์ ์ํ๊ณ ์ด๋ฅผ ์กฐํฉํ์ฌ ๋ค์ํ ๊ฒ์ ์กฐ๊ฑด์ ์ฒ๋ฆฌํ ์ ์๋ค.
- ์ฝ๋์ ์ฌ์ฌ์ฉ์ฑ๊ณผ ๊ฐ๋ ์ฑ์ ๋์ผ ์ ์์ผ๋ฉฐ Spring Data JPA๊ฐ ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ ํ์ฉํ ์ ์๋ค.
- ์น๋ช
์ ์ธ ๋จ์ ์ด ํ๋ ์๋๋ฐ ์ํ๋ ์ปฌ๋ผ์ด๋ ์ฐ์ฐ๊ฒฐ๊ณผ๋ฅผ ์กฐํํ ์ ์๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ์ง ์๋๋ค. ๋ฐ๋ผ์ ์ปฌ๋ผ ์ ์ฒด๋ฅผ ์ ๋ถ ๊ฐ์ ธ์์ผํ๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก ํ์ํ ์ปฌ๋ผ์ด๋ ์ฐ์ฐ ๊ฒฐ๊ณผ๋ง์ ๊ฐ์ ธ์ค๋๋ก view์ ํจ๊ป ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ ๋ณด์ธ๋ค.
- ์ผ๋ฐ ํ ์ด๋ธ์ ์ฌ์ฉํ๊ฒ ๋๋ฉด ์ฌ์ฉํ์ง ์๋ ์ปฌ๋ผ๊น์ง ๋ชจ๋ ๊ฐ์ ธ์์ผ ํ๋ค.
- ์์ ๋ด์ฉ์ ํ ๋๋ก ๋๋ JPA Specification์ ์ฌ์ฉํ๊ธฐ๋ก ๊ฒฐ์ ํ๋ค.
๐ View
@Getter
@Entity
@Table(name = "test_view")
@Immutable // ์ฝ๊ธฐ ์ ์ฉ ์ํฐํฐ์์ ๋ช
์
public class View {
@Id
private Long memberId;
private String playerName;
private int age;
@Enumerated(EnumType.STRING)
private Sex sex;
private String teamName;
private String city;
}
- ๋ทฐ์ ๋งคํ๋ ์ํฐํฐ ํด๋์ค์ด๋ค.
- ์ผ๋ฐ ํด๋์ค์ ํ ์คํธํ๋ ๊ฒ๊ณผ ์ด๋ ํ ์ฐจ์ด๋ ์๋ค.
๐ ViewRepository
public interface ViewRepository extends JpaRepository<View, Long>, JpaSpecificationExecutor<View> {
}
- JPA Specification ์ฌ์ฉ์ ์ํด JpaSpecificationExecutor<View>์ ์์์ ์ถ๊ฐํ๋ค.
๐ ViewSpecification
public class ViewSpecification {
public static Specification<View> likePlayerName(String playerName) {
return (root, query, CriteriaBuilder) -> CriteriaBuilder.like(root.get("playerName"), "%" + playerName + "%");
}
public static Specification<View> likeTeamName(String teamName) {
return (root, query, CriteriaBuilder) -> CriteriaBuilder.like(root.get("teamName"), "%" + teamName + "%");
}
public static Specification<View> rangeAge(int min, int max) {
return (root, query, CriteriaBuilder) -> CriteriaBuilder.between(root.get("age"), min, max);
}
public static Specification<View> equalsSex(Sex sex) {
return (root, query, CriteriaBuilder) -> CriteriaBuilder.equal(root.get("sex"), String.valueOf(sex));
}
public static Specification<View> equalsCity(String city) {
return (root, query, CriteriaBuilder) -> CriteriaBuilder.equal(root.get("city"), city);
}
}
- ๋งจ ์ likePlayerName() ๋ฉ์๋ ์์ฃผ๋ก ์ค๋ช
ํด ๋ณด๊ฒ ๋ค.
- ๋๋จธ์ง ๋ฐฉ์๋ ๊ฑฐ์ ๋น์ทํ๋ค.
- Enum ํ์ ์ ๊ฒฝ์ฐ equalsSex() ๋ฉ์๋์ ๊ฐ์ด ์คํธ๋ง ํ์์ผ๋ก ๋ณํํด ์ค์ผ ์๋ฌ๊ฐ ๋ฐ์ํ์ง ์๋๋ค.
- public static Specification<View> likePlayerName(String playerName):
- ์ด ๋ฉ์๋๋ playerName์ ๋ํ ๋ถ๋ถ ์ผ์น ๊ฒ์์ ์ํํ๋ Specification ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค.
- View๋ ๊ฒ์ ๋์ ์ํฐํฐ ํด๋์ค์ด๋ค.
- (root, query, CriteriaBuilder) -> CriteriaBuilder.like(root.get("playerName"), "%" + playerName + "%"):
- ์ด ๋ถ๋ถ์ Specification์ ์ค์ ๊ตฌํ์ ์ ์ํ๋ ๋๋ค ํํ์์ด๋ค.
- root๋ ์ฟผ๋ฆฌ์ ๋ฃจํธ ์ํฐํฐ๋ฅผ ๋ํ๋ด๋ Root ๊ฐ์ฒด์ด๋ค.
- query๋ CriteriaQuery ๊ฐ์ฒด๋ก ์ฟผ๋ฆฌ ์์ฑ์ ์ฌ์ฉ๋๋ค.
- CriteriaBuilder๋ CriteriaQuery๋ฅผ ์์ฑํ๊ณ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๋ ๋ฐ ์ฌ์ฉ๋๋ ๋น๋ ๊ฐ์ฒด์ด๋ค.
- CriteriaBuilder.like() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ๋ถ๋ถ ์ผ์น ๊ฒ์์ ์์ฑํฉ๋๋ค.
- ๋ค๋ฅธ ๋ฉ์๋๋ฅผ ๋ณด๋ฉด ์๊ฒ ์ง๋ง equal(), between() ๋ฑ SQL ์กฐ๊ฑด๋ฌธ์ ํด๋นํ๋ ์ฌ๋ฌ ๋ฉ์๋๋ฅผ ์ ๊ณตํ๋ค.
- root.get("playerName")๋ root ์ํฐํฐ์์ "playerName" ์์ฑ์ ๊ฐ์ ธ์จ๋ค.
- CriteriaBuilder.like() ๋ฉ์๋์ ์ฒซ ๋ฒ์งธ ๋งค๊ฐ๋ณ์๋ก ํด๋น ์์ฑ์ ์ ๋ฌํ๊ณ , ๋ ๋ฒ์งธ ๋งค๊ฐ๋ณ์๋ก ๊ฒ์์ด๋ฅผ ์ ๋ฌํ์ฌ ๋ถ๋ถ ์ผ์น ๊ฒ์์ ์ํํ๋ค.
- "%" + playerName + "%"๋ ๊ฒ์์ด ์๋ค์ "%"๋ฅผ ์ถ๊ฐํ์ฌ ๋ถ๋ถ ์ผ์น ๊ฒ์์ ์ํํ๋ค.
- ์ด๋ ๊ฒ ์์ฑ๋ ์ฝ๋๋ก ์๋น์ค๋จ์์ likePlayerName ๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ playerName์ ๋ํ ๋ถ๋ถ ์ผ์น ๊ฒ์์ ์ํํ ์ ์๋ค.
- ์ด ๋ฉ์๋๋ Specification ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ฏ๋ก ํด๋น Specification์ JPA Specification์ ์ง์ํ๋ ์ฟผ๋ฆฌ ๋ฉ์๋์ ์ ๋ฌํ์ฌ ๊ฒ์์ ์ํํ๋ค.
๐ ๊ธฐํ ๋ฉ์๋
// ์ฐ๋ น๋ ๊ทธ๋ฃน ๊ฒ์
public static Specification<View> containsAgeGroup(String[] ageGroup) {
return (root, query, CriteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
for (String age : ageGroup) {
predicates.add(CriteriaBuilder.equal(root.get("ageGroup"), age));
}
return CriteriaBuilder.or(predicates.toArray(new Predicate[0]));
};
}
// ์๋
์์ผ ๊ธฐ๊ฐ ๊ฒ์
public static Specification<FilterMember> betweenBirth(LocalDateTime startDate, LocalDateTime endDate) {
return (root, query, CriteriaBuilder) -> CriteriaBuilder.between(root.get("birth"), startDate, endDate);
}
- ์ค์ ๋ก ํ ์คํธ์์๋ ์ฌ์ฉํ์ง ์์์ง๋ง ์ด๋ ์ฐ๋ น๋์ ํฌํจ๋๋์ง ํ์ธํ ๋, ์๋ ์์ผ ๊ธฐ๊ฐ์ผ๋ก ๊ฒ์ํ ๋๋ ์์ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์ฌ์ฉํ๋ฉด ๋๋ค.
๐ ViewController
@Slf4j
@RestController
@RequiredArgsConstructor
public class SearchController {
private final ViewRepository viewRepository;
@GetMapping("/views")
public ResponseEntity<?> getSearchResults(
@RequestParam(value = "playerName", required = false) String playerName,
@RequestParam(value = "teamName", required = false) String teamName,
@RequestParam(value = "minAge", required = false) Integer minAge,
@RequestParam(value = "maxAge", required = false) Integer maxAge,
@RequestParam(value = "sex", required = false) Sex sex,
@RequestParam(value = "city", required = false) String city,
Pageable pageable
) {
Specification<View> spec = (root, query, criteriaBuilder) -> null;
if (playerName != null) {
spec = spec.and(ViewSpecification.likePlayerName(playerName));
}
if (teamName != null) {
spec = spec.and(ViewSpecification.likeTeamName(teamName));
}
if (minAge != null) {
spec = spec.and(ViewSpecification.rangeAge(minAge, maxAge));
}
if (sex != null) {
spec = spec.and(ViewSpecification.equalsSex(sex));
}
if (city != null) {
spec = spec.and(ViewSpecification.equalsCity(city));
}
Page<View> response = viewRepository.findAll(spec, pageable);
return new ResponseEntity<>(response, HttpStatus.OK);
}
}
- ๊ฐ๋จํ ํ ์คํธ๋ฅผ ์ํด ์ปจํธ๋กค๋ฌ์ ์๋น์ค ๋ก์ง์ ์ถ๊ฐํ๋ค.
- @RequestParam ์ ๋ํ ์ด์ ์ ์ด์ฉํด ํ์ํ ๊ฐ์ ๋ฐ๋๋ค.
- required์ deault๋ true์ธ๋ฐ false๋ก ์ค์ ํ๊ฒ ๋๋ฉด ํด๋น ์์ฒญ ๋งค๊ฐ๋ณ์๊ฐ ํ์๊ฐ ์๋๋ผ๋ ์๋ฏธ์ด๋ค.
- ์กฐ๊ฑด ๊ฒ์์ ํฌํจ๋์ง ์์ ์ ์์ผ๋ฏ๋ก ์ถ๊ฐํด์ค์ผ ํ๋ค.
- Specification<View> spec = (root, query, criteriaBuilder) -> null;
- spec ๋ณ์๋ Specification<View> ํ์ ์ ๊ฐ์ฒด๋ฅผ ๊ฐ๋ฆฌํค๋ ๋ณ์์ด๋ค.
- ์ด๊ธฐํ๋ฅผ ์ํด ๋น Specification์ ํ ๋นํ๋ค.
- ์ด๋ค ์กฐ๊ฑด ์์ด ์ ์ฒด ์กฐํ๋ ๊ฐ๋ฅํด์ผ ํ๊ธฐ ๋๋ฌธ์ด๋ค.
- ๋ค์์ผ๋ก if ๋ฌธ๋ค์ ํตํด ๊ฐ๊ฐ์ ๋งค๊ฐ๋ณ์(playerName, teamName, minAge, sex, city)์ ๊ฐ์ด null์ด ์๋์ง ํ์ธํ๊ณ ํด๋นํ๋ ViewSpecification์ spec์ ์ถ๊ฐํ๋ค.
- and ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ spec์ ๋งค๊ฐ๋ณ์ ์กฐ๊ฑด์ ์ถ๊ฐํ๋ค.
- ๋ง์ง๋ง์ผ๋ก viewRepository.findAll(spec, pageable)์ ํธ์ถํ์ฌ spec์ ํด๋นํ๋ ๋์ ์ธ ์ฟผ๋ฆฌ๋ฅผ ์คํํ๊ณ ๊ฒฐ๊ณผ๋ฅผ Page<View> ํ์
์ ๋ณ์์ธ response์ ๋ฃ๋๋ค.
- pageable์ ํ์ด์ง๋ค์ด์ ์ ๋ณด๋ฅผ ๋ด๊ณ ์๋ ๊ฐ์ฒด๋ก, ์กฐํ ๊ฒฐ๊ณผ๋ฅผ ํ์ด์ง ๋จ์๋ก ๋ฐํํ ์ ์๋๋ก ๋์์ค๋ค.
- ์์ ํ๋ผ๋ฏธํฐ์ pageable ํ๋๋ง ์ถ๊ฐํด๋ ํ๋ผ๋ฏธํฐ ๊ฐ์ผ๋ก page, size, sort์ ๋ํ ์ ๋ณด๋ฅผ ๊ธฐ์ ๋ฐ์ ์ ์๋ค.
- ์์ธํ ๊ฑด ํ์ด์ง ์ฒ๋ฆฌ์ ๋ํด ๊ฒ์ํด ๋ณด์.
๐งช PostMan์ผ๋ก ํ ์คํธํด๋ณด๊ธฐ
- ์์ ๊ฐ์ด ํ๋ผ๋ฏธํฐ์ ๊ฐ์ ์ถ๊ฐํ๊ณ ๋นผ๋ณด๋ฉด์ ํน์ ์กฐ๊ฑด ๋ณ๋ก ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ๋ค๋ฅด๊ฒ ๋์ด์ ํ์ธํ ์ ์์๋ค.
๐ ์ ์ฒด ์กฐํ ์ Json Response ์์
{
"content": [
{
"memberId": 1,
"playerName": "๋๊ทธ๋",
"age": 39,
"sex": "MAN",
"teamName": "ํ ํธ๋",
"city": "๋ฐ๋"
},
{
"memberId": 2,
"playerName": "๋ฒค์ ๋ง",
"age": 23,
"sex": "WOMAN",
"teamName": "ํ ํธ๋",
"city": "๋ฐ๋"
},
...
]
"pageable": {
"pageNumber": 0,
"pageSize": 20,
"sort": {
"empty": true,
"sorted": false,
"unsorted": true
},
"offset": 0,
"paged": true,
"unpaged": false
},
"last": false,
"totalPages": 2,
"totalElements": 40,
"first": true,
"size": 20,
"number": 0,
"sort": {
"empty": true,
"sorted": false,
"unsorted": true
},
"numberOfElements": 20,
"empty": false
}
์ฐธ๊ณ
๋คผํผ
https://dev-setung.tistory.com/20
https://velog.io/@sierra9707/TIP-JPA์์-๋์ -์ฟผ๋ฆฌ๋ฅผ-์ฒ๋ฆฌํ๋-๋ฐฉ๋ฒ
https://bsssss.tistory.com/1280
https://itecnote.com/tecnote/java-spring-data-jpa-specification-to-select-specific-columns/
728x90