JPA 를 사용할 때 조건 조회에 활용할 수 있는 Query Method 와 @Query 다음으로 Criteria API
에 대해서 알아보겠습니다.
Criteria API 는 JPQL을 Java 코드로 작성할 수 있게 지원하는 빌드 클래스 API 입니다. String이 아닌 java code로 작성하기 때문에 문법, 타입 오류를 컴파일 단계에서 검출할 수 있고, JPQL보다 동적 쿼리를 생성하는데 유용합니다. 하지만 복잡한 쿼리를 작성해야 할 경우에는 코드가 직관적으로 이해가 힘들어 유지보수 측면에선 좋지 않습니다.
기존에 작성했던 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
로도 받을 수 있습니다. 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();
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]));
@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 은 Root 에서 join method 를 사용합니다. alias
와 JoinType
매개변수로 전달하여 설정합니다.
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")));
CriteriaBuilder 의 in
method 호출로 in문을 작성할 수 있습니다. 값은 value
method 로 추가합니다.
Predicate memberNameIn = criteriaBuilder.in(
rootMember.get("memberName")).value("이건").value("박건");
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 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;
}