// 엔티티와 속성은 대소문자 구분 (Member, username)
// JPQL 키워드는 대소문자 구분 안함 (SELECT, FROM, WHERE)
// 테이블 이름이 아닌 엔티티 이름 사용 (Member)
// 별칭은 필수 (m) - AS는 생략 가능
String jpql = "SELECT m FROM Member m WHERE m.username = 'kim'";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
// TypedQuery: 반환 타입이 명확할 때 사용
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
TypedQuery<String> query2 = em.createQuery("SELECT m.username FROM Member m", String.class);
// Query: 반환 타입이 명확하지 않을 때 사용
Query query3 = em.createQuery("SELECT m.username, m.age FROM Member m");
// getResultList(): 결과가 하나 이상일 때, 리스트 반환
// 결과가 없으면 빈 리스트 반환
List<Member> members = em.createQuery("SELECT m FROM Member m", Member.class)
.getResultList();
// getSingleResult(): 결과가 정확히 하나, 단일 객체 반환
// 결과가 없으면: NoResultException
// 둘 이상이면: NonUniqueResultException
Member member = em.createQuery("SELECT m FROM Member m WHERE m.id = 1", Member.class)
.getSingleResult();
// 이름 기준 파라미터 (권장)
String jpql = "SELECT m FROM Member m WHERE m.username = :username";
List<Member> members = em.createQuery(jpql, Member.class)
.setParameter("username", "kim")
.getResultList();
// 위치 기준 파라미터 (권장하지 않음)
String jpql2 = "SELECT m FROM Member m WHERE m.username = ?1";
List<Member> members2 = em.createQuery(jpql2, Member.class)
.setParameter(1, "kim")
.getResultList();
// 엔티티 프로젝션 - 영속성 컨텍스트에서 관리됨
List<Member> result = em.createQuery("SELECT m FROM Member m", Member.class)
.getResultList();
// 연관된 엔티티 프로젝션 - 이렇게 하지 말고
List<Team> result = em.createQuery("SELECT m.team FROM Member m", Team.class)
.getResultList();
// 명시적 조인을 사용하자
List<Team> result = em.createQuery("SELECT t FROM Member m JOIN m.team t", Team.class)
.getResultList();
em.createQuery("SELECT m.address FROM Member m", Address.class)
.getResultList();
// 여러 값 조회 - Object[] 반환
List<Object[]> result = em.createQuery("SELECT m.username, m.age FROM Member m")
.getResultList();
for (Object[] row : result) {
String username = (String) row[0];
Integer age = (Integer) row[1];
}
// DTO로 바로 조회 (new 명령어 사용)
List<MemberDTO> result = em.createQuery(
"SELECT new jpql.MemberDTO(m.username, m.age) FROM Member m", MemberDTO.class)
.getResultList();
// MemberDTO 클래스
public class MemberDTO {
private String username;
private int age;
public MemberDTO(String username, int age) {
this.username = username;
this.age = age;
}
}
// JPA는 페이징을 다음 두 API로 추상화
String jpql = "SELECT m FROM Member m ORDER BY m.age DESC";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setFirstResult(10) // 조회 시작 위치 (0부터 시작)
.setMaxResults(20) // 조회할 데이터 수
.getResultList();
// 각 데이터베이스마다 다른 페이징 처리를 JPA가 처리해줌
// MySQL: LIMIT
// Oracle: ROWNUM
// SQL Server: TOP
String jpql = "SELECT m FROM Member m INNER JOIN m.team t";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
String jpql = "SELECT m FROM Member m LEFT JOIN m.team t";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
String jpql = "SELECT m FROM Member m, Team t WHERE m.username = t.name";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
조인 대상 필터링:
// JPQL: 회원과 팀을 조인하면서, 팀 이름이 'A'인 팀만 조인
String jpql = "SELECT m 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'
연관관계 없는 엔티티 외부 조인:
// JPQL: 회원의 이름과 팀의 이름이 같은 대상 외부 조인
String jpql = "SELECT m 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
// 나이가 평균보다 많은 회원
String jpql = "SELECT m FROM Member m WHERE m.age > (SELECT AVG(m2.age) FROM Member m2)";
// 한 건이라도 주문한 고객
String jpql = "SELECT m FROM Member m WHERE (SELECT COUNT(o) FROM Order o WHERE m = o.member) > 0";
// 팀 A 소속인 회원
String jpql = "SELECT m FROM Member m WHERE EXISTS (SELECT t FROM m.team t WHERE t.name = '팀A')";
// 전체 상품 각각의 재고보다 주문량이 많은 주문들
String jpql = "SELECT o FROM Order o WHERE o.orderAmount > ALL (SELECT p.stockAmount FROM Product p)";
// 어떤 팀이든 팀에 소속된 회원
String jpql = "SELECT m FROM Member m WHERE m.team = ANY (SELECT t FROM Team t)";
// ENUM 사용 예시
String jpql = "SELECT m FROM Member m WHERE m.type = jpql.MemberType.ADMIN";
// 엔티티 타입 사용 예시 (상속)
String jpql = "SELECT i FROM Item i WHERE TYPE(i) = Book";
String jpql =
"SELECT " +
"CASE WHEN m.age <= 10 THEN '학생요금' " +
" WHEN m.age >= 60 THEN '경로요금' " +
" ELSE '일반요금' " +
"END " +
"FROM Member m";
String jpql =
"SELECT " +
"CASE t.name " +
" WHEN '팀A' THEN '인센티브110%' " +
" WHEN '팀B' THEN '인센티브120%' " +
" ELSE '인센티브105%' " +
"END " +
"FROM Team t";
// COALESCE: 하나씩 조회해서 null이 아니면 반환
// 사용자 이름이 없으면 이름 없는 회원을 반환
String jpql = "SELECT COALESCE(m.username, '이름 없는 회원') FROM Member m";
// NULLIF: 두 값이 같으면 null 반환, 다르면 첫 번째 값 반환
// 사용자 이름이 '관리자'면 null을 반환하고 나머지는 본인의 이름을 반환
String jpql = "SELECT NULLIF(m.username, '관리자') FROM Member m";
String jpql = "SELECT CONCAT('a', 'b') FROM Member m"; // ab
String jpql = "SELECT SUBSTRING(m.username, 2, 3) FROM Member m";
String jpql = "SELECT LOCATE('de', 'abcdefg') FROM Member m"; // 4
String jpql = "SELECT SIZE(t.members) FROM Team t"; // 팀의 멤버 수
하이버네이트는 사용자 정의 함수를 호출할 수 있다.
// 1. 사용자 정의 함수 등록
public class MyH2Dialect extends H2Dialect {
public MyH2Dialect() {
registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
}
}
// 2. persistence.xml에 dialect 등록
// <property name="hibernate.dialect" value="dialect.MyH2Dialect"/>
// 3. 사용
String jpql = "SELECT FUNCTION('group_concat', m.username) FROM Member m";
JPQL에서 .(점)을 찍어 객체 그래프를 탐색하는 것
SELECT m.username -> 상태 필드
FROM Member m
JOIN m.team t -> 단일 값 연관 필드
JOIN m.orders o -> 컬렉션 값 연관 필드
WHERE t.name = '팀A'
// 상태 필드 경로 탐색
String jpql = "SELECT m.username, m.age FROM Member m"; // OK
// 단일 값 연관 경로 탐색
String jpql = "SELECT m.team FROM Member m"; // OK
String jpql = "SELECT m.team.name FROM Member m"; // OK, 묵시적 조인 발생
// 컬렉션 값 연관 경로 탐색
String jpql = "SELECT t.members FROM Team t"; // OK
String jpql = "SELECT t.members.username FROM Team t"; // 실패!
// 컬렉션 탐색을 하려면 명시적 조인 사용
String jpql = "SELECT m.username FROM Team t JOIN t.members m"; // OK
// 묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생 (내부 조인만 가능)
String jpql = "SELECT m.team.name FROM Member m";
// 명시적 조인: join 키워드 직접 사용
String jpql = "SELECT t.name FROM Member m JOIN m.team t";
실무 조언: 묵시적 조인은 사용하지 말자
SQL 조인 종류가 아니고, JPQL에서 성능 최적화를 위해 제공하는 기능. 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능.
// JPQL
String jpql = "SELECT m FROM Member m JOIN FETCH m.team";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();
// 실행된 SQL
// SELECT M.*, T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID
for (Member member : members) {
// 페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩X
System.out.println("username = " + member.getUsername() +
", teamName = " + member.getTeam().getName());
}
// JPQL
String jpql = "SELECT t FROM Team t JOIN FETCH t.members WHERE t.name = '팀A'";
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();
// 실행된 SQL
// SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID = M.TEAM_ID WHERE T.NAME = '팀A'
for(Team team : teams) {
System.out.println("teamname = " + team.getName() + ", team = " + team);
for (Member member : team.getMembers()) {
// 페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
System.out.println("-> username = " + member.getUsername()+ ", member = " + member);
}
}
주의: 컬렉션 페치 조인에서 일대다 조인은 결과가 증가할 수 있다
팀A에 회원이 2명 있으면 같은 팀A가 2번 조회됨.
// JPQL의 DISTINCT는 2가지 기능 제공
// 1. SQL에 DISTINCT 추가
// 2. 애플리케이션에서 엔티티 중복 제거
String jpql = "SELECT DISTINCT t FROM Team t JOIN FETCH t.members WHERE t.name = '팀A'";
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();
System.out.println("teams = " + teams.size()); // 1 (중복 제거됨)
하이버네이트6부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용됨.
// 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음
String jpql = "SELECT t FROM Team t JOIN t.members m WHERE t.name = '팀A'";
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();
// 실행된 SQL
// SELECT T.* FROM TEAM T INNER JOIN MEMBER M ON T.ID = M.TEAM_ID WHERE T.NAME = '팀A'
for(Team team : teams) {
System.out.println("teamname = " + team.getName());
for (Member member : team.getMembers()) {
// 회원 컬렉션을 실제 사용할 때 별도의 SQL 실행 (지연 로딩)
System.out.println("-> username = " + member.getUsername());
}
}
// 방법 1: 배치 사이즈로 해결
@BatchSize(size = 100) // 개별 최적화
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
// 또는 글로벌 설정
// hibernate.default_batch_fetch_size: 100
// 방법 2: 방향을 뒤집어서 해결
String jpql = "SELECT m FROM Member m JOIN FETCH m.team t";
List<Member> members = em.createQuery(jpql, Member.class)
.setFirstResult(0)
.setMaxResults(10)
.getResultList();
JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본키값을 사용
// JPQL
String jpql = "SELECT m FROM Member m WHERE m = :member";
List<Member> members = em.createQuery(jpql)
.setParameter("member", member)
.getResultList();
// 실행된 SQL
// SELECT m.* FROM Member m WHERE m.id = ?
// 엔티티의 아이디를 사용
String jpql = "SELECT m FROM Member m WHERE m.id = :memberId";
List<Member> members = em.createQuery(jpql)
.setParameter("memberId", memberId)
.getResultList();
Team team = em.find(Team.class, 1L);
String qlString = "SELECT m FROM Member m WHERE m.team = :team";
List<Member> members = em.createQuery(qlString)
.setParameter("team", team)
.getResultList();
// 실행된 SQL
// SELECT m.* FROM Member m WHERE m.team_id = ?
미리 정의해서 이름을 부여해두고 사용하는 JPQL. 정적 쿼리만 가능.
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "SELECT m FROM Member m WHERE m.username = :username")
public class Member {
// ...
}
// 사용
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
<named-query name="Member.findByUsername">
<query><![CDATA[
select m
from Member m
where m.username = :username
]]>
</query>
</named-query>
재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
String qlString = "update Product p " +
"set p.price = p.price * 1.1 " +
"where p.stockAmount < :stockAmount";
int resultCount = em.createQuery(qlString)
.setParameter("stockAmount", 10)
.executeUpdate();
// 회원1의 나이를 20으로 변경하고 싶다.
Member member1 = em.find(Member.class, 1L);
System.out.println("member1 = " + member1.getAge()); // 0
// 벌크 연산 수행: 모든 회원의 나이를 20으로 변경
em.createQuery("update Member m set m.age = 20")
.executeUpdate();
System.out.println("member1 = " + member1.getAge()); // 0 (영속성 컨텍스트에는 여전히 0)
// 해결책 1: 벌크 연산을 먼저 실행
// 해결책 2: 벌크 연산 수행 후 영속성 컨텍스트 초기화
em.clear();
Member findMember = em.find(Member.class, 1L);
System.out.println("findMember = " + findMember.getAge()); // 20
JPQL은 SQL과 문법이 유사하면서도 객체지향적 특성을 유지하는 강력한 쿼리 언어다. 특히 페치 조인은 성능 최적화에 있어서 핵심적인 기능이므로 반드시 숙지해야 한다. 하지만 복잡한 쿼리나 통계성 쿼리의 경우 네이티브 SQL이나 QueryDSL 같은 대안을 고려해보는 것도 좋다.