JPA는 다양한 쿼리 방법을 지원한다.
기존 방식대로 EntityManager.find()
, 객체 그래프 탐색(ex: a.getB().getC()
) 방식으로 가장 단순하게 조회할 수 있다.
하지만, 나이가 18살 이상인 회원을 모두 검색하고자 하는 등의 경우 좀 더 현실적이고 복잡한 검색 방법이 필요하다.
이런 문제를 해결하기 위해 JPQL이 만들어졌다.
// 검색
String jpql = "select m From Member m where m.age > 18";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
// 실행된 SQL
select
m.id as id,
m.age as age,
m.USERNAME as USERNAME,
m.TEAM_ID as TEAM_ID
from
Member m
where
m.age>18
//Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
// 루트 클래스(조회를 시작할 클래스)
Root<Member> m = query.from(Member.class);
// 쿼리 생성
CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "kim"));
List<Member> resultList = em.createQuery(cq).getResultList();
하지만 실무에선 거의 사용되지 않는다. 쿼리를 동적으로 생성할 수는 있지만, 구현이 너무 복잡하고 실용성이 없다. 이 대신 QueryDSL 사용을 권장한다.
// JPQL + QueryDSL 조합이 실무에선 거의 95% 정도라고 함
QueryDSL은 JPA 표준은 아니고 오픈소스 프로젝트이다. JPA 뿐만 아니라 MongoDB, Java Collection, Lucene, Hibernate Search, JDO도 거의 같은 문법으로 지원하며 현재 스프링 데이터 프로젝트가 지원할 정도로 많이 기대되는 프로젝트라고 한다.
// JPQL
// select m from Member m where m.age > 18
JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member;
List<Member> list = query.selectFrom(m)
.where(m.age.gt(18))
.orderBy(m.name.desc())
.fetch();
String sql = "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();
Member member = new Member();
member.setName("Sergio Ramos");
conn.createQuery("select * from Member where username = 'Sergio Ramos'");
member는 Jdbc가 쿼리를 수행하는 시점에서 영속성 컨텍스트에만 있고 DB에 아직 저장되지 않았기 때문에 조회 결과가 없다. 따라서 쿼리 수행 전 수동으로 플러시를 해줘야 한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// 연관관계 편의 메소드
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
// getter, setter
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany
private List<Member> members = new ArrayList<>();
// getter, setter
}
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
private Long id;
private int orderAmount;
@Embedded
private Address address;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
// getter, setter
}
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
}
@Entity
public class Product {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
private int stockAmount;
// getter, setter
}
select_문 ::=
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
update_문 ::= update_절 [where_절]
delete_문 ::= delete_절 [where_절]
select m from Member as m where m.age > 18
select
COUNT(m), // 회원 수
SUM(m.age), // 나이 합
AVG(m.age), // 평균 나이
MAX(m.age), // 최대 나이
MIN(m.age) // 최소 나이
from Member m
select t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
from Member m LEFT JOIN m.team t
GROUP BY t.name
HAVING AVG(m.age) >= 10
select m from Member m order by m.age DESC, m.username ASC
select t.name, COUNT(m.age) as cnt
from Member m LEFT JOIN m.team t
GROUP BY t.name
ORDER BY cnt
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
query.getResultList()
: 결과가 하나 이상일 때, 리스트 반환query.getSingleResult()
: 결과가 정확히 하나, 단일 객체 반환javax.persistence.NoResultException
예외 발생javax.persistence.NonUniqueResultException
예외 발생참고로 Spring Data JPA에선 결과가 없으면 try-catch로 optional 또는 null 을 반환 해준다.
// 이름 기준 파라미터
String sql = "select m from Member m where m.username = :username";
TypedQuery<Member> query = em.createQuery(sql, Member.class);
query.setParameter("username", usernameParam);
List<Member> result = query.getResultList();
// 메소드 체이닝 지원
String sql = "select m from Member m where m.username = :username";
List<Member> result = em.createQuery(sql, Member.class)
.setParameter("username", usernameParam)
.getResultList();
// 위치 기준 파라미터
String sql = "select m from Member m where m.username = ?1";
List<Member> result = em.createQuery(sql, Member.class)
.setParameter("1", usernameParam)
.getResultList();
파라미터 바인딩 방식은 선택이 아닌 필수다. SQL 인젝션 공격 위험성도 있고, 성능 이슈도 있기 때문이다. 다만, 이름 기준 파라미터 바인딩 방식을 사용하는 것을 좀 더 권장한다. (버그 문제 때문)
SELECT m FROM Member m
: 엔티티 프로젝션SELECT m.team FROM Member m
: 엔티티 프로젝션SELECT m.address FROM Member m
: 임베디드 타입 프로젝션SELECT m.username, m.age FROM Member m
: 스칼라 타입 프로젝션SELECT m FROM Member m // 회원
SELECT m.team FROM Member m // 팀
JPQL에서 임베디드 타입은 엔티티와 거의 비슷하게 사용된다.
다만, 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다.
// 임베디드 타입인 Address를 조회의 시작점으로 사용한 잘못된 예시
String query = "SELECT a FROM Address a";
// Order 엔티티가 시작점이다. 엔티티를 통해 임베디드 타입을 조회해야 한다.
String query = "SELECT o.address FROM Order o";
List<Address> addresses = em.createQuery(query, Address.class).getResultList();
임베디드 타입은 엔티티 타입이 아닌 값 타입이다. 따라서 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.
숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다.
em.createQuery("SELECT DISTINCT username FROM Member m").getSingleResult();
엔티티를 대상으로 조회하면 편리하겠지만, 꼭 필요한 데이터들만 선택해서 조회해야 할 때도 있다.
프로젝션에 여러 값을 선택하면 TypeQuery
를 사용할 수 없고 Query
를 사용해야 한다.
Query
타입으로 조회Member member = new Member();
member.setUsername("member1");
member.setAge(10);
em.persist(member);
em.flush();
em.clear();
List resultList = em.createQuery("select m.username, m.age from Member m")
.getResultList();
Object o = resultList.get(0);
Object[] result = (Object[]) o;
System.out.println("result = " + result[0]);
System.out.println("result = " + result[1]);
Object[]
타입으로 조회List<Object[]> resultList = em.createQuery("select m.username, m.age from Member m")
.getResultList();
Object[] result = resultList.get(0);
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
// MemberDTO.java
public class MemberDTO {
private String username;
private int age;
public MemberDTO(String username, int age) {
this.username = username;
this.age = age;
}
// getter, setter
}
// Main.java
List<MemberDTO> resultList = em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
.getResultList();
MemberDTO memberDTO = resultList.get(0);
System.out.println("memberDTO = " + memberDTO.getUsername());
System.out.println("memberDTO = " + memberDTO.getAge());
페이징 처리용 SQL을 작성하는 일은 지루하고 반복적이다. 더 큰 문제는 데이터베이스마다 페이징을 처리하는 SQL 문법이 다르다는 점이다.
JPA의 페이징은 다음과 같은 특징이 있다.
setFirstResult(int startPosition)
: 조회 시작 위치(0부터 시작)setMaxResults(int maxResult)
: 조회할 데이터 수예시는 다음과 같다.
for (int i = 0; i < 100; i++) {
Member member = new Member();
member.setUsername("member" + i);
member.setAge(i);
em.persist(member);
}
em.flush();
em.clear();
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(1)
.setMaxResults(10)
.getResultList();
System.out.println("result.size = " + result.size());
for (Member member1 : result) {
System.out.println("member1 = " + member1);
}
결과는 다음과 같이 확인할 수 있다.
데이터베이스 별로 페이징 API에 대한 방언은 다음과 같다.
// MySQL
SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM
MEMBER M
ORDER BY
M.NAME DESC LIMIT ?, ?
// Oracle
SELECT * FROM
( SELECT ROW_.*, ROWNUM ROWNUM_
FROM
( SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM MEMBER M
ORDER BY M.NAME
) ROW
WHERE ROWNUM <= ?
)
WHERE ROWNUM_ > ?
기존에는 Diarect별로 방언을 맞춰서 쿼리를 하나하나 구현해야 했는데, 이제는 두 개의 함수로 해결할 수 있어서 몹시 편리하다.
만약, 페이징 SQL을 더 최적화하고 싶다면 JPA가 제공하는 페이징 API가 아닌 네이티브 SQL을 직접 사용해야 한다.
// @ManyToOne(fetch = FetchType.LAZY)가 되어있어야 함.
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
String query = "select m from Member m inner join m.team t";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
tx.commit();
String query = "select m from Member m left outer join m.team t";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
String query = "select m from Member m, Team t where m.username = t.name";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
ex) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
// JPQL
SELECT m, t FROM Member m LEFT JOIN m.team t on t.name='A'
// SQL
SELECT m.*, t.* FROM
Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='A'
ex) 회원의 이름과 팀의 이름이 같은 대상 외부조인
// JPQL
SELECT m, t FROM Member m LEFT JOIN Team t on m.username=t.name
// SQL
SELECT m.*, t.* FROM
Member m LEFT JOIN Team t ON m.username=t.name
ex) 나이가 평균보다 많은 회원
select m from Member m
where m.age > (select avg(m2.age) from Member m2)
ex) 한 건이라도 주문한 고객
select m from Member m
where (select count(o) from Order o where m = o.member) > 0
select m from Member m
where exists (select t from m.team t where t.name = '팀A')
select o from Order o
where o.orderAmount > ALL (select p.stockAmount from Product p)
select m from Member m
where m.team = ANY (select t from Team t)
select m.username, 'HELLO', true from Member m
where m.type = jpql.MemberType.ADMIN
em.createQuery("select i from Item i where type(i) = Book", Item.class);
String query =
"select " +
"case when m.age <= 10 then '학생요금' " +
" when m.age >= 60 then '경로요금' " +
" else '일반요금'"+
" end " +
"from Member m";
List<String> result = em.createQuery(query, String.class)
.getResultList();
for (String s : result) {
System.out.println("s = " + s);
}
String query =
"select " +
"case t.name when 'teamA' then '인센티브110%' " +
" when 'teamB' then '인센티브120%' " +
" else '인센티브105%'"+
" end " +
"from Team t";
List<String> result = em.createQuery(query, String.class)
.getResultList();
하나씩 조회해서 null이 아니면 반환
select coalesce(m.username, '이름 없는 회원') from Member m
두 값이 같으면 null 반환, 다르면 첫 번째 값 반환
select NULLIF(m.username, '관리자') from Member m
// CONCAT
select concat('a', 'b') // ab
// SUBSTRING: firstParam의 값을 secondParam 위치부터 thirdParam 갯수만큼 잘라서 반환
select substring('abcd', 2, 3) // bc
// TRIM
select trim(' sergio ramos ') // sergio ramos
// LOWER, UPPER
select LOWER()
select UPPER()
// LENGTH
select LENGTH('sergioramos') // 11
// LOCATE
select LOCATE('r', 'ramos') // 1
// ABS, SQRT, MOD
select ABS(-30) // 30
select SQRT(4) // 2
select MOD(4, 2) // 0
// SIZE, INDEX(JPA 용도)
select SIZE(t.members) from Team t // 0
하이버네이트는 사용전 방언에 추가해야 한다.
→ 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.
(실제 소스 코드 내부에 정의되어 있는 함수들을 참고해서 작성해주면 된다.)
// group_concat이라는 함수를 만들어서 등록한다 가정
public class MyH2Dialect extends H2Dialect {
public MyH2Dialect() {
registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
}
...
}
// 설정 파일 등록
<property name="hibernate.dialect" value="hello.MyH2Dialect"/>
// 하이버네이트 구현체 사용
select function('group_concat', i.name) from Item i