Home 14. 크리테리아 API를 이용한 쿼리
Post
Cancel

14. 크리테리아 API를 이용한 쿼리

01. 크리테리아 API

크리테리아 API는 자바 코드를 이용해서 작성하는 쿼리이다.

1
2
3
4
5
6
7
8
CritetiaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> root = cq.from(User.class);
cq.select(root);
cq.where(cb.equal(root.get("name"), "고길동");

TypedQuert<User> query = em.createQuery(cq);
List<User> users = query.getResultLiat();

JPQL보다 복잡하지만 사용하는 이유는 크리테리아가 가진 장점이 있기 때문이다.

  1. 다양한 조건을 조합하기 쉽다.
  2. 문자열과 달리 자바 코드를 사용하기 때문에 타입에 안전한 쿼리를 만들 수 있다.
  3. 자바 코드이기 때문에 IDE의 코드 자동 완성 기능을 활용할 수 있다.

02. 크리테리아 기본 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1) EntityManager에서 CritetiaBuilder 구한다.
CritetiaBuilder cb = em.getCriteriaBuilder();

//2) User 타입의 CriteriaQuery 생성 => createQuery 메서드의 인자는 조회 결과 타입을 지정
CriteriaQuery<User> cq = cb.createQuery(User.class);

//3) from User
Root<User> root = cq.from(User.class);

//4) root를 조회 결과로 사용 => select root from User root
cq.select(root);

//5) CriteriaQuery로부터 Query 생성
TypedQuert<User> query = em.createQuery(cq);

03. 검색 조건 지정

1
2
3
//CritetiaBuilder는 Predicate를 생성하는 메서드를 제공한다. => cb.equal(비교할 대상, 비교할 값)
Predicate namePredicate = cb.equal(root.get("name")."고길동");
cq.where(namePredicate);

04. 속성 경로 구하기

root.get(“name”)은 User 엔티티의 name 속성에 해당하는 Path 객체를 구한다. Path 인터페이스는 엔티티나 밸류, 콜렉션 등의 속성의 경로를 표현한다.

1
2
3
4
public interface Path<X> extends Expression<X> {...}

//타입 파라미터 X는 Path 인터페이스가 나타내는 대상 속성의 타입이다. 따라서 다음과 같이 속성 타입을 지정해서 Path를 수할 수 있다.
Path<Stirng> name = root.get("name");

이름 대신에 정적 메타모델을 사용해서 속성을 지정할 수도 있다. 정적 메타모델은 엔티티의 속성에 대한 메타 정보를 담고 있는 클래스로서 다음과 같이 생겼다.

1
2
3
4
5
6
7
8
9
10
@StaticMetamodel(User.class)
public class User_ {
  public static SingularAttribute<User, String> email;
  public static SingularAttribute<User, String> name;

  //...
}

//정적 메타모델을 사용하면 속성 경ㅇ로를 구할 때 정적 메타모델의 필드를 사용할 수 있다.
Predicate namePredicate = cb.equal(root.get(User_.name), "고길동");

정적 메타모델을 사용하면 타입이나 이름을 오류 없이 알맞은 Path 타입으로 구할 수 있기 때문에 컴파일 시점에 타입에 안전한 코드를 작성할 수 있다는 장점이 있다.

4-1. 중첩 속성 경로 구하기

1:1 연관관계를 맺고 있는 경우 연관된 엔티티의 속성을 검색조건으로 사용해서 검색하고 싶은 경우 중첩해서 속성을 구하면 된다.

1
Predicate namePredicate = cb.equal(root.get("user").get("name"), "고길동");

05. CriteriaQuery와 CriteriaBuilder 구분

  • CriteriaQuery - select, from, where, groupBy, having, order by 등 쿼리의 절을 생성
  • CriteriaBuilder - 각 절의 구성 요소를 생성

CriteriaBuilder는 where 절에서 사용할 Preidcate 구성 요소를 생성하거나, 정렬 순서를 표현하는 Order 구성요소를 생성할 때 등에 사용할 수 있다.

06. Expression과 하위 타입

Path 등 크리테리아를 이용해서 쿼리를 구성할 때 사용하는 타입은 Expression 인터페이스를 상속하고 있다.

CriteriaQuery와 CriteriaBuilder의 많은 메서드가 Expression 타입의 파라미터를 갖는다. 따라서 Expression이 올 수 있는 곳에는 이들 타입을 알맞게 사용할 수 있다.

07. 비교 연산자

7-1. 기본 비교 연산자

각 메서드의 첫 번째 파라미터는 비교 대상을 지정한다. 주요 비교 대상은 엔티티의 속성이므로 주로 Root.get()을 이용해서 비교할 대상을 구한다.

1
2
Predicate namePredicate = cb.equal(root.get("name"), "고길동");
Predicate nameEmaileq = cb.equal(root.get("name"), root.get("email"));

7-2. in 비교 연산자

비교 대상이 특정 값 중 하나인지 비교할 때에는 in() 메서드를 사용한다. 이 메서드는 CriteriaBuilder.In 타입 객체를 리턴한다.

1
2
3
4
5
6
CriteriaBuilder.In<Object> nameIn = cb.in(root.get("name"));
// value() 메서드를 이용해서 in 연산자로 비교할 값 목록을 지정한다.
nameIn.value("고길동").value("홍길동");
cq.where(nameIn);

// where root.name in ('고길동', '홍길동');

In을 Object로 받으면 String 타입뿐 아니라 Integer, Double과 같이 모든 타입의 값을 전달받을 수 있다. 자바는 타입 추론을 지원하므로 In<String> 타입 변수를 사용해서 String 타입으로 제한할 수 있다.

1
2
3
4
//get() 메서드에 타입 파라미터를 지정할 수도 있다.
CriteriaBuilder<Object> nameIn = cb.in(root.<String>get("name"))
      .value("고길동")
      .value(0); //error

7-3. 콜렉션 비교

JPQL의 member of에 해당하는 Predicate를 생성할 때는 isMember() 메서드를 사용한다.

  • isMember(E elem, Expression<C> collection)
  • isMember(Expression<E> elem, Expression<C> collection)
1
2
3
4
5
6
7
8
9
Player player = em.find(Player.class, "P1");

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Team> cq = cb.createQuery(Team.class);
Root<Team> t = cq.from(Team.class);
cq.where(
  cb.isMember(player, t.get("players"))
);
cq.select(t)
1
select t from Team t where :player member of t.players

특정 콜렉션에 속해 있지 않은지 비교할 때에는 isNotMember() 메서드를 사용한다. 콜렉션이 비어있는지 확인할 때는 isEmpty()를 그와 반대는 isNotEmpty()를 사용한다.

7-4. exists, all, any

exists, all, any 쿼리는 서브쿼리를 사용하기 때문에 다소 복잡하다.

JPQL은 exists를 위해 서브쿼리를 사용하고있다. 크리테리아도 동일하게 서브쿼리를 사용해서 exists 비교 조건을 생성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CriteriaQuery<Hotel> cq = cb.createQuery(Hotel.class);

//select h from Hotel h
Root<Hotel> h = cq.from(Hotel.class);
cq.select(h);

//exists()에 사용할 서브쿼리 생성
Subquery<Review> subquery = cq.subquery(Review.class);
Root<Review> r = subquery.from(Review.class);
subquery.select(r).where(cb.equal(r.get("hotel"), h);

//exists(서브쿼리) 조건
Predicate existsPredicate = cb.exists(subquery);

//not(existsPredicate), 즉 not exists(서브쿼리)
cq.where(cb.not(existsPredicate));

7-5. and와 or로 조건 조합

JPQL처럼 and와 or처럼 크리테리아도 여러 조건을 조합할 수 있다.

1
2
3
4
Predicate emailPredicate = cb.equal(ubs.get("email"), "aaa@aaa.com");
Predicate titlePredicate = cb.equal(ubs.get("title"), "%랜드");
Predicate andPredicate = cb.and(emailPredicate, titlePredicate);
cq.where(andPredicate);

크리테리아를 사용해서 쿼리를 구성하게 되면 문자열로 작성할 때 발생할 수 있는 쿼리 오류 문제가 거의 발생하지 않는다.

또 conjunction() 메서드를 사용하면 if문도 줄일 수 있다. 이 메서드는 조건이 존재하지 않으면 무조건 true인 조건을 생성한다. 예) 조건이 null이라면 전체 목록을 조회하는 쿼리인 1=1 쿼리를 생성한다.

복잡한 if-else 블록을 줄이는 또 다른 방법으로는 Predicate 리스트를 만들어 AND로 연결할 조건을 추가하는 방법이다.

1
2
3
4
List<Predicate> predicates = new ArrayList<>();
if(email != null) predicates.add(cb.equal(ubs.get("email"), email);
if(keyword != null) predicates.add(cb.like(ubs.get("title"), "%"+keyword));
cq.where(predicates.toArray(new Predicate[predicates.size()]));

위 코드처럼 Predicate 배열을 where() 메서드에 전달하면 각 조건을 AND로 조합한다.

08. 정렬 순서 지정하기

정렬순서는 CriteriaBuilder의 asc() 또는 desc()로 Order를 생성한뒤 CriteriaQuery orderBy()에 Order를 전달해서 정렬 순서를 지정한다.

1
2
3
4
5
6
CriteriaQuery<Player> cq = cb.createQuery(Player.class);
Root<Player> p = cq.from(Player.class);
cq.select(p);
Order teamIdOrder = cb.asc(p.get("team").get("id"));
Order nameOrder = cb.asc(p.get("name"));
cq.orderBy(teamIdOrder, nameOrder);

09. 지정 칼럼 조회

9-1. 한 개 속성 조회하기

크리테리아에 엔티티가 아닌 특정 컬럼을 조회하고 싶다면 select()에 해당 경로를 전달하면 된다.

1
2
3
4
5
6
CriteriaQuery<String> cq = cb.createQuery(String.class);
Root<Player> p = cq.from(Player.class);
cq.select(p.get("id")); // select 메서드로 단일 속성 선택

TypedQuery<String> query = em.createQuery(cq);
List<String> rows = query.getResultList();

9-2. 배열로 조회하기

엔티티가 아닌 두 개 이상의 속성을 배열로 조회하는 방법은 select() 메서드 대신 multiselect() 메서드를 사용하고 선택 타입으로 Object[]을 사용한다는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
Root<Player> p = cq.from(Player.class);
cq.multiselect(p.get("id"), p.get("name"), p.get("salary"));

TypedQuery<Object[]> query = em.createQuery(cq);
List<Object[]> rows = query.getResultList();
for(Object[] row: rows) {
    String id = (String) row[0];
    String name = (String) row[1];
    int salary = (int)row[2];
    //...
}

9-3. 특정 객체로 조회하기

select new jpastart.common.IdName(p.id, p.name) from Player p 처럼 특정 객체로 조회하고 싶다면 construct() 메서드를 사용해서 클래스 생성자와 전달할 대상을 지정한다.

1
2
3
4
5
6
7
8
9
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<IdName> cq = cb.createQuery(IdName.class);
Root<Player> p = cq.from(Player.class);

//클래스 타입과 생성자에 전달할 대상 목록을 지정한다.
cq.select(cb.construct(IdName.class, p.get("id"), p.get("name")));

TypedQuery<IdName> query = em.createQuery(cq);
List<IdName> rows = query.getResultList();
1
2
3
4
5
6
7
8
9
10
public class IdName{
    private String id;
    private String name;

    public IdName(String id, String name) {
        this.id = id;
        this.name = name;
    }
    //...
}

10. 조인

크리테리아도 JPQL과 마찬가지로 다음과 같은 방식을 이용해서 조인을 처리할 수 있다.

  • 자동 조인 - 연관된 엔티티의 속성에 접근할 때 발생,
1
2
3
4
5
6
CriteriaQuery<Player> cq = cb.createQuery(Player.class);
Root<Player> p = cq.from(Player.class);
cq.select(p);
Predicate teamNamePredicate = cb.equal(p.get("team").get("name"), "팀");
cq.where(teamNamePredicate);
cq.orderBy(p.get("name")));
  • 명시적 조인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Player> cq = cb.createQuery(Player.class);
Root<Player> p = cq.from(Player.class);
cq.select(p);
Join<Player, Team> t = p.join("team");  //Player의 "team" 속성에 대한 조인을 표현하는 Join 객체를 리턴한다.
cq.where(cb.equal(t.get("name"), "팀"));
cq.orderBy(cb.asc(p.get("name")));

//외부조인
Team team = em.find(Team.class, "T1");

//....

Join<Player, Team> t = p.join("team", JoinType.LEFT);
t.on(cb.equal(t, team));
  • where 절에서 조인
1
2
3
4
5
6
7
8
9
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
//조인할 Root 객체를 두개 생성
Root<User> u = cq.from(User.class);
Root<UserBestSight> s = cq.from(UserBestSight.class);
//조인할 속성끼리 값이 같은지 비교
cq.where(cb.equal(u.get("email"), s.get("email")));
cq.multiselect(u, s);
cq.orderBy(cb.asc(u.get("name")));

11. 집합 함수

CriteriaBulider는 집합 함수에 대응하는 메서드를 제공한다.

Expression<N>의 N은 Number 또는 그 하위 타입이다.

1
2
3
4
5
6
7
8
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
Root<Player> p = cq.from(Player.class);
cq.multiselect(
    cb.count(p), cb.sum(p.get("salary")).
    cb.avg(p.get("salary")), cb.max(p.get("salary")), cb.min(p.get("salary")));

TypedQuery<Object[]> query = em.createQuery(cq);
Object[] aggvalues = query.getSingleReuslt();

12. group by와 having

1
2
3
4
5
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
Root<Player> p = cq.from(Player.class);
cq.groupBy(p.get("team").get("id"));
cq.multiselct(p.get("team").get("id"), cb.count(p), cb.avg(p.get("salary")), cb.max(p.get("salary")), cb.min(p.get("salary")));
TypedQuery<Object[]> query = em.createQuery(cq);
1
2
3
4
5
6
7
8
// select t count(p), avg(p.salary) from Player p left join p.team t group by t
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
Root<Player> p = cq.from(Player.class);
Join<Player, Team> t = p.join("team", joinType.LEFT);

cq.groupBy(t);
cq.multiselct(cb.count(p), cb.avg(p.get("salary")));
TypedQuery<Object[]> query = em.createQuery(cq);
1
2
3
4
5
6
7
8
// select t, count(p), avg(p.salary) from Team t left join t.player p group by t having count(p) > 1
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
Root<Team> t = cq.from(Team.class);
Join<Team, Player> p = t.join("players", joinType.LEFT);

cq.groupBy(t);
cq.having(cb.gt(cb.count(p), 1));  //count(p) > 1
cq.multiselct(cb.count(p), cb.avg(p.get("salary")));

groupBy() 메서드는 두 개가 있다. 1)가변인자를 사용하는 메서드, 2)List를 사용하는 메서드이다. 따라서 두 개 이상의 기준을 사용해서 그룹을 나누고 싶다면 groupBy() 메서드에 인자를 두 개 이상 전달하거나 그룹 대상을 담고있는 List를 인자로 주면 된다.

13. 함수와 연산자

13-1. 문자열 함수

  • concat() - 두 개 문자열을 연결한다.
  • substring() - 문자열의 시작위치에서 지정한 길이에 해당하는 문자열을 구한다.
  • trim() - 문자열의 공백 문자를 제거한다.
  • lower() - 문자열을 소문자로 변경한다.
  • upper() - 문자열을 대문자로 변경한다.
  • length() - 문자열의 길이를 구한다.
  • locate() - 문자열이 포함된 위치를 구한다.

13-2. 수학 함수

  • abs() - 절대 값을 구한다.
  • sqr() - 제곱근을 구한다.
  • mod() - x를 y로 나눈 나머지를 구한다.
  • neg() - 수식의 부호를 바꾼다.
  • sum() - x에 y를 더한다.
  • diff() - x에서 y를 뺀다.
  • prod() - x와 y를 곱한다.
  • quot() - x를 y로 나눈다.

13-3. 날짜 함수

  • currentDate() - 현재 시간을 Date 타입으로 구한다.
  • currentTimestamp() - 현재 시간을 Timestamp 타입으로 구한다.
  • currentTime() - 현재 시간을 Time 타입으로 구한다.

13-4. 콜렉션 관련 함수

  • size() - 콜렉션의 크기를 구한다.

14. fetch 조인

1:1이나 N+1 연관에 대해 fetch 조인을 사용하고 싶다면 join() 메서드 대신 fetch() 메서드를 사용하면 된다.

1
2
3
4
5
6
7
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<MembershipCard> cq = cb.createQuery(MembershipCard.class);
Root<MembershipCard> mc = cq.from(MembershipCard.class);
Fetch<MembershipCard, User> u = mc.fetch("owner", JoinType.LEFT);
cq.select(mc);

//select mc from MembershipCard mc left join fetch mc.owner u

15. 정적 메타모델

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
@Table(name="hotel_review")
public class Review{
  //...

  @Temporal(TemporalType.TIMESTAMP)
  @Column(name="rating_data")
  private Date ratingDate;
}


//특정 기간에 작성한 Review를 검색하고자 할 때,
CriteriaQuery<Review> cq = cb.createQuery(Review.class);
Root<Review> r = cq.from(Review.class);
cq.select(r);
Date fromDate = ...
Date roDate = ....

Predicate betweenPredicate = cb.between(r.get("ratingDate"), fromDate, toDate);
//파라미터로 Date 타입만 들어올 수 있게 다음과 같이 할 수 있다.
//Predicate betweenPredicate = cb.between(r.<Date>get("ratingDate"), fromDate, toDate);
cq.where(betweenPredicate);

타입 파라미터를 통해 값 타입을 제한하면 컴파일러를 통해 타입을 검사할 수 있어 잘못된 값 타입으로 인해 발생하는 오류를 줄일 수 있다. 하지만 다소 코드가 길어지고 보기 어려워진다.

이때 정적 메타모델을 사용하면 타입도 안전하고, 코드도 덜 복잡해진다.

1
2
3
4
5
6
7
@StaticMetamodel(Review.class)
public class Review_{
  public static SingulerAttribute<Review, String> id;
  public static SingulerAttribute<Review, Date> ratingDate;
}

Predicate betweenPredicate = cb.between(r.get(Review_.ratingDate), fromDate, toDate);

15-1. 정적 메타모델 클래스 구성

336~337p

This post is licensed under CC BY 4.0 by the author.