- 참조: 자바 ORM 표준 JPA 프로그래밍
- 집합
Criteria 에서 집합(group by) 과 집합의 조건(having) 을 어떻게 사용하는지 예제를 통해 알아보자. group by 를 사용해보기 위해 기존 Member 엔티티에 age 속성을 더해주고, Address 를 함께 사용한다. Embedded 타입에 equals 와 hasCode 재정의 해 주는것도 잊지말자.
@Entity
public class Member extends DateMarkable{
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private int age;
private String name;
@Embedded
private Address address;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
...............
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public Address() {
super();
}
public Address(String city, String street, String zipcode) {
super();
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
위의 엔티티 정보로 group by 를 사용해서 만들어볼 JPQL 은 각 도시(city) 마다 최고 나이와 최소 나이를 산출해보는것이다. JPQL 로는 "select m.address.city, min(m.age), max(m.age) from Member m group by m.address.city" 와 같은 형태가 되겠다.
이를 Criteria 로 만들어보기 위해 예제 데이터부터 영속화시켜보자.
Member member1 = new Member();
member1.setName("Name#1");
member1.setAge(10);
member1.setAddress(
new Address("City#1", "Street#1", "Zipcode#1"));
em.persist(member1);
Member member2 = new Member();
member2.setName("Name#2");
member2.setAge(13);
member2.setAddress(
new Address("City#1", "Street#2", "Zipcode#2"));
em.persist(member2);
Member member3 = new Member();
member3.setName("Name#3");
member3.setAge(32);
member3.setAddress(
new Address("City#2", "Street#3", "Zipcode#3"));
em.persist(member3);
Member member4 = new Member();
member4.setName("Name#4");
member4.setAge(22);
member4.setAddress(
new Address("City#2", "Street#4", "Zipcode#4"));
em.persist(member4);
group by 사용을 위해 member 1과 2의 City를 City#1 으로 member 3과 4의 City를 City#2 로 설정했다. 나이는 4 명의 Member 모두 다르게 할당하였다.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<Member> m = cq.from(Member.class);
Expression<Integer> maxAge = cb.max(m.<Integer>get("age"));
Expression<Integer> minAge = cb.min(m.<Integer>get("age"));
cq.multiselect(m.get("address").get("city").alias("city"),
maxAge.alias("maxAge"),
minAge.alias("minAge"));
cq.groupBy(m.get("address").get("city"));
List<Tuple> statCities = em.createQuery(cq)
.getResultList();
statCities.forEach(statCity -> {
System.out.println("City name: " + statCity.get("city") +
", maxAge: " + statCity.get("maxAge") +
", minAge: " + statCity.get("minAge"));
});
위의 예제에서 Root<Member> 까지는 우리가 앞에서 살편 코드들이다. min 과 max 표현식은 CriteriaBuilder 로 부터 만든다. age 속성은 Integer 형으로 반환받을 수 있다.
multiselect 에서 도시마다 최대나이 최소나이를 조회해야 하므로 m.address.city 와 min(m.age), max(m.age) 표현식을 인자로 넘겨준다. 여기까지 하면 JPQL 로 따졌을 때 select 절과 from 절까지 완성했다고 볼 수 있다.
group by 절은 select 절에서 city 를 표현한것과 같은 표현을 해주면 된다. 이때에는 grouping 정보이지 Tuple 조회시 alias 를 사용할 필요는 없으므로 alias 를 제외한 경로표현식을 작성해준다.
Hibernate:
/* select
generatedAlias0.address.city,
max(generatedAlias0.age),
min(generatedAlias0.age)
from
Member as generatedAlias0
group by
generatedAlias0.address.city */ select
member0_.city as col_0_0_,
max(member0_.age) as col_1_0_,
min(member0_.age) as col_2_0_
from
member member0_
group by
member0_.city
City name: City#1, maxAge: 13, minAge: 10
City name: City#2, maxAge: 32, minAge: 22
위의 SQL 로그는 실행결과이다. 주석처리된 부분을 보면 우리가 의도하려고 했던 group by SQL 로 잘 변환되었고, 결과도 올바르게 출력된것을 확인할 수 있다.
각 도시의 최소 나이가 20 이상인 건만 조회하려고 한다면 having 절을 사용해야 한다.
..............
cq.multiselect(m.get("address").get("city").alias("city"),
maxAge.alias("maxAge"),
minAge.alias("minAge"));
cq.groupBy(m.get("address").get("city"));
cq.having(cb.ge(minAge, 20));
..............
having 절은 위와 같이 CriteriaBuilder 에서 비교연산 표현식을 사용한다.
Hibernate:
/* select
generatedAlias0.address.city,
max(generatedAlias0.age),
min(generatedAlias0.age)
from
Member as generatedAlias0
group by
generatedAlias0.address.city
having
min(generatedAlias0.age)>=20 */ select
member0_.city as col_0_0_,
max(member0_.age) as col_1_0_,
min(member0_.age) as col_2_0_
from
member member0_
group by
member0_.city
having
min(member0_.age)>=20
City name: City#2, maxAge: 32, minAge: 22
having 절이 적절하게 추가되어 최소 나이가 10 살이던 도시의 데이터가 조회되지 않는다.
Criteria 는 직관적이지 않기 때문에 헷갈릴 수 있다. JPQL의 예약어들에 해당하는 select, from, where, order by, group by, having 절은 CriteriaQuery 로 설정한다. 대소비교 및 표현식들은 CriteriaBuilder 로 만든다.
위의 원칙을 가지고 아래 단계로 천천히 생각해보면서 사용하도록 하자.
- 엔티티 매니저에서 CriteriaBuilder 와 CriteriaBuilder 로 부터 CriteriaQuery 를 얻어온다.
- 얻어올 엔티티 정보를 설정하는 Root 를 선언한다. 이때에는 CriteriaQuery 로 부터 얻어온다.
- select, group by, having 절을 CriteriaQuery 로 부터 얻어오고 해당 절 안의 경로 표현식은 Root 변수로 부터 참조한다.
- min 또는 max 에 대한 표현식은 CriteriaBuilder 로 부터 얻어온다.
- 정렬
order by 도 위에서 설명한 원칙을 가지고 작성하면 된다.
cq.multiselect(m.get("address").get("city").alias("city"),
maxAge.alias("maxAge"),
minAge.alias("minAge"));
cq.where(cb.between(m.get("age"), 10, 50));
cq.groupBy(m.get("address").get("city"));
cq.having(cb.ge(minAge, 10));
cq.orderBy(cb.desc(m.get("address").get("city")));
select 부터 where, group by, having, order by 를 모두 작성해보았다. 예약어에 해당하는 order by 절도 CriteriaQuery 로 부터 얻어오며, asc 나 desc 에 해당하는 표현식은 CriteriaBuilder 로 설정한다.
Hibernate:
/* select
generatedAlias0.address.city,
max(generatedAlias0.age),
min(generatedAlias0.age)
from
Member as generatedAlias0
where
generatedAlias0.age between 10 and 50
group by
generatedAlias0.address.city
having
min(generatedAlias0.age)>=10
order by
generatedAlias0.address.city desc */ select
member0_.city as col_0_0_,
max(member0_.age) as col_1_0_,
min(member0_.age) as col_2_0_
from
member member0_
where
member0_.age between 10 and 50
group by
member0_.city
having
min(member0_.age)>=10
order by
member0_.city desc
City name: City#2, maxAge: 32, minAge: 22
City name: City#1, maxAge: 13, minAge: 10
order by 절이 의도한대로 도시의 내림차순으로 잘 변환되었다.
- 조인
Criteria 에 정의된 JoinType enum 클래스 정의는 아래와 같다.
public enum JoinType {
/** Inner join. */
INNER,
/** Left outer join. */
LEFT,
/** Right outer join. */
RIGHT
}
위의 예제를 약간 수정해서 inner join 을 사용해보자. Member 엔티티와 연관 맵핑된 Order 엔티티를 조인한다. 각 도시의 최소 최대 나이를 구하는것과 더불어 주문건 수를 구한다.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<Member> m = cq.from(Member.class);
Join<Member, Order> o = m.join("orders", JoinType.INNER);
Expression<Long> sumOrders = cb.count(o.get("id"));
Expression<Integer> maxAge = cb.max(m.<Integer>get("age"));
Expression<Integer> minAge = cb.min(m.<Integer>get("age"));
cq.multiselect(m.get("address").get("city").alias("city"),
sumOrders.alias("sumOrders"),
maxAge.alias("maxAge"),
minAge.alias("minAge"));
cq.where(cb.between(m.get("age"), 10, 50));
cq.groupBy(m.get("address").get("city"));
cq.having(cb.ge(minAge, 10));
cq.orderBy(cb.desc(m.get("address").get("city")));
List<Tuple> statCities = em.createQuery(cq)
.getResultList();
statCities.forEach(statCity -> {
System.out.println("City name: " + statCity.get("city") +
", maxAge: " + statCity.get("maxAge") +
", minAge: " + statCity.get("minAge"));
});
m.join("orders", ...) 메소드를 이용하여 Member 에서 Order 를 join 한다. 2번째 인자로 JoinType 을 지정하는데 위의 예제에서는 INNER join 으로 설정하였다.
위의 쿼리는 실행하면 결과가 나오지 않는다. 예제 데이터에서 Member 에 주문을 아무것도 맵핑해주지 않았기 때문이다.
Hibernate:
/* select
generatedAlias0.address.city,
count(generatedAlias1.id),
max(generatedAlias0.age),
min(generatedAlias0.age)
from
Member as generatedAlias0
inner join
generatedAlias0.orders as generatedAlias1
where
generatedAlias0.age between 10 and 50
group by
generatedAlias0.address.city
having
min(generatedAlias0.age)>=10
order by
generatedAlias0.address.city desc */ select
member0_.city as col_0_0_,
count(orders1_.order_id) as col_1_0_,
max(member0_.age) as col_2_0_,
min(member0_.age) as col_3_0_
from
member member0_
inner join
orders orders1_
on member0_.member_id=orders1_.member_id
where
member0_.age between 10 and 50
group by
member0_.city
having
min(member0_.age)>=10
order by
member0_.city desc
비록 결과는 나오지 않지만 변환된 SQL을 보면 member 가 orders 테이블과 inner join 을 수행하는것을 알 수 있다. LEFT 로 바꿔서 한번 수행해보자.
..............
Root<Member> m = cq.from(Member.class);
Join<Member, Order> o = m.join("orders", JoinType.LEFT);
..............
LEFT join 을 하면 Member 는 존재하기 때문에 연관 맵핑된 Order 엔티티가 존재하지 않아도 정보가 조회된다.
Hibernate:
...........................
from
member member0_
left outer join
orders orders1_
on member0_.member_id=orders1_.member_id
...........................
City name: City#2, sumOrders: 0, maxAge: 32, minAge: 22
City name: City#1, sumOrders: 0, maxAge: 13, minAge: 10
m.fetch 를 사용하면 Fetch Join 도 사용할 수 있다. 주의사항은 JPQL 에서 말한것과 동일하다.
'Framework and Tool > JPA' 카테고리의 다른 글
JPA - 객체지향 쿼리 언어 - Criteria 파라미터, 네이티브 함수 (0) | 2021.08.12 |
---|---|
JPA - 객체지향 쿼리 언어 - Criteria 서브 쿼리 (0) | 2021.08.11 |
JPA - 객체지향 쿼리 언어 - Criteria 조회 (0) | 2021.08.08 |
JPA - 객체지향 쿼리 언어 - Criteria 쿼리 생성 (0) | 2021.08.05 |
JPA - 객체지향 쿼리 언어 - Criteria (0) | 2021.08.04 |
댓글