JPA Search By Condition(3) - Criteria API

GEONNY·2024년 8월 24일
0
post-thumbnail

JPA 를 사용할 때 조건 조회에 활용할 수 있는 Query Method@Query 다음으로 Criteria API 에 대해서 알아보겠습니다.

📌Criteria API

Criteria API 는 JPQL을 Java 코드로 작성할 수 있게 지원하는 빌드 클래스 API 입니다. String이 아닌 java code로 작성하기 때문에 문법, 타입 오류를 컴파일 단계에서 검출할 수 있고, JPQL보다 동적 쿼리를 생성하는데 유용합니다. 하지만 복잡한 쿼리를 작성해야 할 경우에는 코드가 직관적으로 이해가 힘들어 유지보수 측면에선 좋지 않습니다.

📌Example

기존에 작성했던 MemberService 구현체인 MemberServiceImpl 을 대체할 MemberServiceCriteriaImpl 을 생성합니다. MemberServiceImpl 과 같이 MemberService 를 implements 합니다.
domain.member.MemberServiceCriteriaImpl

@Service
@RequiredArgsConstructor
public class MemberServiceCriteriaImpl implements MemberService {

    @Override
    public MemberSearchResponse getMemberById(String memberId) {
        return null;
    }

    @Override
    public List<MemberSearchResponse> getMembers() {
        return null;
    }

    @Override
    public MemberCreateResponse createMember(MemberCreateRequest parameter) {
        return null;
    }

    @Override
    public MemberModifyResponse modifyMember(MemberModifyRequest parameter) {
        return null;
    }

    @Override
    public Long deleteMember(String memberId) {
        return null;
    }
}

MemberController 에서 MemberServiceImpl의 의존성 주입을 MemberServiceCriteriaImpl 로 변경해줍니다.

@RestController
@RequiredArgsConstructor
@Validated({MemberValidationGroup.User.class})
@Tag(name = "회원 관리", description = "회원에 대한 조회/추가/수정/삭제 기능")
@RequestMapping("v1")
public class MemberController {

    private final MemberServiceCriteriaImpl memberService; //MemberServiceImpl 에서 변경
    //이하 생략..

MemberServiceCriteriaImpl 의 getMemberById method 를 구현하겠습니다.

@Service
@RequiredArgsConstructor
public class MemberServiceCriteriaImpl implements MemberService {

    private final MemberMapper memberMapper = Mappers.getMapper(MemberMapper.class);

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    @Transactional
    public MemberSearchResponse getMemberById(String memberId) {
    
    	//CriteriaBuilder 인스턴스 생성
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		// 쿼리 구조 정의 return type (Member) 지정
        CriteriaQuery<Member> criteriaQuery = criteriaBuilder.createQuery(Member.class);
        // 쿼리에서 사용할 Entity 정의 (from절 생성)
        Root<Member> rootMember = criteriaQuery.from(Member.class);
        // 조회 조건 생성
        Predicate memberIdEqual = 
        	criteriaBuilder.equal(rootMember.get("memberId"), memberId);
        // 쿼리에서 사용할 select, where절 생성
        // select m from member m where m.memberId = :memberId
        criteriaQuery.select(rootMember)
                      .where(memberIdEqual);
		//CriteriaQuery 기반 JPQL 쿼리 생성, 단일 결과 반환
        //createQuery 시 return type 설정해서 반환 타입을 지정하지 않아도 됨
        Member result = entityManager.createQuery(criteriaQuery).getSingleResult();

        return memberMapper.toRecord(result);
    }
    //이하 생략..

라인별 설명은 주석을 참고하시기 바랍니다. 이전에 @Query를 사용해서 JPQL을 작성하던 방식과 다르게 Java code를 사용해서 생성하고 있습니다.

📍반환 타입

🎈반환 타입을 지정할 수 없을 때

위 코드에서는 반환 타입을 Member로 설정했지만, 반환 타입을 지정할 수 없거나 둘 이상이면 Object, Object[] 를 사용하여 결과를 반환할 수 있습니다.

CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery(Object.class);
criteriaQuery.select(rootMember.get("memberId"))
                .where(predicates.toArray(new Predicate[0]));
List<Object> result = entityManager.createQuery(criteriaQuery).getResultList();
CriteriaQuery<Object[]> criteriaQuery = criteriaBuilder.createQuery(Object[].class);
criteriaQuery.multiselect(rootMember.get("memberId"), rootMember.get("memberName"))
                .where(predicates.toArray(new Predicate[0]));
List<Object[]> result = entityManager.createQuery(criteriaQuery).getResultList();

select를 사용하여 Entity를 전체 조회하거나 단일 조회할 때는 반환 타입을 Object로, multiSelect 를 사용하여 복수 필드를 조횔할 때는 Object[] 로 받습니다.
multiSelect 를 사용하면서 Object 타입으로 설정은 가능하지만, 결국 필드를 사용하기 위해선 Object[] 로 캐스팅을 해야합니다. 유지보수 측면에서도 명확하지 않으니 select - Object, multiSelect - Object[] 로 타입에 맞게 사용하는 것을 권장합니다.

🎈Tuple

반환 타입을 Tuple 로도 받을 수 있습니다. jakarta.persistence.Tuple 은 Criteria 에서 Map 과 비슷한 자료구조 입니다.
Tuple 을 사용하려면 criteriaBuilder.createTupleQuery(); or criteriaBuilder.createQuery(Tuple.class); 로 생성하야 합니다.

CriteriaQuery<Tuple> criteriaQuery = criteriaBuilder.createTupleQuery();
criteriaQuery.multiselect(rootMember.get("memberId")
						, rootMember.get("memberName").alias("memberName"))
             .where(predicates.toArray(new Predicate[0]));
List<Tuple> tupleList = entityManager.createQuery(criteriaQuery).getResultList();

alias*

Tuple 데이터에 접근하기 위해서는 위와 같이 alias 를 필수로 설정해야 합니다. alias 를 설정하지 않은 tuple에 접근 시 IllegalArgumentException 가 발생합니다.

for(Tuple tuple : tupleList) {
	String memberId = tuple.get("memberId", String.class); //IllegalArgumentException
    String memberName = tuple.get("memberName", String.class);
}

Tuple 은 multiSelect 대신에 select(criteriaBuilder.tuple(...) 로도 사용 가능합니다.

criteriaQuery.select(criteriaBuilder.tuple(
                     rootMember.get("memberId").alias("memberId"),
                     rootMember.get("memberName").alias("memberName")
              ))
              .where(predicates.toArray(new Predicate[0]));

📍Record or DTO mapping

@Query 에서는 select new 생성자() 를 사용하여 Record or DTO로 매핑을 했습니다. Criteria 에서는 criteriaBuilder.construct 를 사용하여 매핑합니다.

CriteriaQuery<MemberSearchResponse> criteriaQuery = 
	criteriaBuilder.createQuery(MemberSearchResponse.class);
    
        criteriaQuery.select(criteriaBuilder.construct(MemberSearchResponse.class
                           , rootMember.get("memberId")
                           , rootMember.get("memberName")))
                      .where(predicates.toArray(new Predicate[0]));

📍정렬

정렬은 CriteriaQuery 에 orderBy method 호출로 설정합니다. 매개변수는 단일 Order 객체나, List<Order> 객체를 전달할 수 있습니다.

CriteriaQuery<T> orderBy(Order... o);
CriteriaQuery<T> orderBy(List<Order> o);

Order 객체의 생성은 criteriaBuilder.asc(...), criteriaBuilder.desc(...) 를 사용합니다.

criteriaQuery.select(criteriaBuilder.tuple(
                    rootMember.get("memberId").alias("memberId"),
                    rootMember.get("memberName").alias("memberName")
              ))
              .where(predicates.toArray(new Predicate[0]))
              .orderBy(criteriaBuilder.asc(rootMember.get("memberName"))); //정렬 추가

📍Join

Join 은 Root 에서 join method 를 사용합니다. aliasJoinType 매개변수로 전달하여 설정합니다.
jakarta.persistence.criteria

public enum JoinType {

    /** Inner join. */
    INNER, 

    /** Left outer join. */
    LEFT, 

    /** Right outer join. */
    RIGHT
}

Member - Authority 간 left outer join 으로 authorityName 을 추가로 가져오도록 하겠습니다.

Root<Member> rootMember = criteriaQuery.from(Member.class);
Join<Member, Authority> authorityJoin = rootMember.join("authority", JoinType.LEFT);

criteriaQuery.select(criteriaBuilder.tuple(
                        rootMember.get("memberId").alias("memberId"),
                        rootMember.get("memberName").alias("memberName"),
                         //Add Join Field
                        authorityJoin.get("authorityName").alias("authorityName")
              ))
              .where(predicates.toArray(new Predicate[0]))
              .orderBy(criteriaBuilder.asc(rootMember.get("memberName")));

📍in문

CriteriaBuilder 의 in method 호출로 in문을 작성할 수 있습니다. 값은 value method 로 추가합니다.

Predicate memberNameIn = criteriaBuilder.in(
	rootMember.get("memberName")).value("이건").value("박건");

📍case 문

CriteriaBuilder 의 selectCase method 호출로 case문을 작성할 수 있습니다. when, otherwise method 로 분기합니다.
Case<R> when(Expression<Boolean> condition, R result);

criteriaQuery.select(criteriaBuilder.tuple(
                rootMember.get("memberId").alias("memberId"),
                rootMember.get("memberName").alias("memberName"),
                authorityJoin.get("authorityName").alias("authorityName"),
                criteriaBuilder.selectCase()
	                           .when(criteriaBuilder.equal(
                               		rootMember.get("useYn"), UseYn.Y), "정상 회원")
                               .when(criteriaBuilder.equal(
                               		rootMember.get("useYn"), UseYn.N), "잠긴 회원")
                               .otherwise("알수 없음")
                ))
              .where(predicates.toArray(new Predicate[0]))
              .orderBy(criteriaBuilder.asc(rootMember.get("memberName")));

📍동적 조회 조건 추가

List<Predicate> 에 추가하여 동적으로 조건을 추가할 수 있습니다.

List<Predicate> predicates = new ArrayList<>();
if (StringUtils.isNotEmpty(memberId)) {
   predicates.add(criteriaBuilder.equal(rootMember.get("memberId"), memberId));
}
if (StringUtils.isNotEmpty(memberName)) {
    predicates.add(criteriaBuilder.like(rootMember.get("memberName"), memberName));
}
criteriaQuery.select(rootMember)
              .where(predicates.toArray(new Predicate[0]));

📚참고

📕List.toArray

List.toArray method 는 List 의 요소들을 배열로 리턴해주는 Method 입니다.

Object[] toArray();
<T> T[] toArray(T[] a);

위와 같이 매개변수가 없으면, Object 배열로, 매개변수타입을 전달하면 해당 매개변수 타입의 배열로 리턴해 줍니다.

predicates.toArray(new Predicate[0])

위의 경우는 Predicate type의 배열로 리턴이 되는데, 배열 사이즈를 0으로 전달하는 이유는 ArrayList 의 toArray 구현부를 보면 알 수 있습니다. 매개변수로 전달된 배열의 크기가 List의 크기보다 작으면 List 사이즈의 배열을 생성하고 값을 복사하여 리턴해 줍니다.

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}
profile
Back-end developer

0개의 댓글