목차
- 객체지향 쿼리 언어 소개
- JPQL
- 기본 문법과 기능
- 페치 조인
객체지향 쿼리 언어 소개
JPA는 다양한 쿼리 방법을 지원한다.
- JPQL
- JPA Criteria
- QueryDSL
- 네이티브 SQL
- JDBC 직접 사용 (Mybatis, SpringJdbcTemplate 등)
JPQL 소개
JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공해준다.
SQL과 문법이 유사하지만 특정 데이터베이스에 의존적이지 않으며, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 등을 지원해주며, 엔티티 객체를 대상으로 쿼리를 사용한다.
가장 단순한 조회 방법
- EntityManager.find()
- 객체 그래프 탐색 (a.getB().getC())
검색 예시
String jpql = "select m from Member m where m.id > 18";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
- Member 객체에서 id 값이 18을 넘는 값들을 리스트로 저장
Hibernate:
/* select
m
from
Member m
where
m.id > 18 */ select
member0_.MEMBER_ID as member_i1_6_,
member0_.city as city2_6_,
member0_.street as street3_6_,
member0_.zipcode as zipcode4_6_,
member0_.USERNAME as username5_6_
from
Member member0_
where
member0_.MEMBER_ID>18
- 실제 쿼리문은 다음과 같이 나간다.
Criteria 소개
자바코드로 JPQL을 작성할 수 있도록 도와주는 방식이며 JPQL의 빌더 역할이라고 보면된다. 유지보수가 어려워 실무에서 사용하지 않으니 참고만해두자.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
Root<Member> m = query.from(Member.class);
CriteriaQuery<Member> cq = query.select(m);
String username = "da";
if (username != null) {
cq = cq.where(cb.equal(m.get("name"), "kim"));
}
List<Member> result = em.createQuery(cq).getResultList();
- 동적 쿼리를 적용하기에 문자를 사용하면 불편한 점(컴파일 시점에서 오류, 복잡한 쿼리문)을 해결해준다. 하지만 사용하기에 너무 복잡하고 실용성이 없어 Criteria 대신에 QueryDSL 사용을 권장한다.
QueryDSL 소개
Criteria와 같이 자바코드로 작성하여 빌더 역할을 하며 단순하고 쉽다. 실무에서는 해당 쿼리언어를 사용하므로 꼭 알아두자.
//JPQL
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();
- 컴파일 시점에서도 오류를 잡을 수 있고 간단해서 동적 쿼리를 작성하기에 매우 적합하다.
네이티브 SQL 소개
JPA가 제공하는 SQL을 직접 사용하는 기능으로 특정 데이터베이스에 의존적이다.
List<Member> resultList = em.createNativeQuery("select MEMBER_ID, city, street, zipcode, USERNAME, from MEMBER", Member.class).getResultList();
- 다음과 같이 순수 SQL을 넣어서 사용할 수 있다.
JDBC 직접사용
JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, Mybatis 등 함께 사용 가능하다.
단, 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요하다. 즉, JPA에서 지원하는 기본 쿼리 문들은 모두 flush()처리를 해주지만, JDBC를 직접사용하는 경우 직접 flush()가 필요하다.
JPQL
JPQL 이란 Java Persistence Query Language이란 의미를 갖고 있다.
예시 비교 모델
Member 엔티티
package jpql;
import javax.persistence.*;
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String userName;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
Team 엔티티
package jpql;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
Order 엔티티
package jpql;
import javax.persistence.*;
@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;
}
ORDER
예약어로 인해 오류가 발생할 수 있으므로ORDERS
로 테이블명을 변경하자.
Address 엔티티
package jpql;
import javax.persistence.Embeddable;
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
}
Product 엔티티
package jpql;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Product {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
private int stockAmount;
}
JPQL 문법
- 엔티티와 속성은 대소문자를 구분한다. (Member 와 age 등)
- JPQL 키워드는 대소문자를 구분하지 않는다.
- 테이블 이름이 아닌 엔티티 이름이며, 별칭(m, as는 생략 가능)는 필수로 적용해야한다.
::
은 벌크연산이라고 불리며, 이후에 자세히 정리해보자.
집합과 정렬
select
COUNT(m),
SUM(m.age),
AVG(m.age),
MAX(m.age),
MIN(m.age)
from Member m
- (GROUP BY, HAVING), ORDER BY 등 지원한다.
TypeQuery, Query
TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
TypedQuery<String> query2= em.createQuery("select m.userName from Member m", String.class);
Query query = em.createQuery("select m.userName, m.age from Member m");
- TypeQuery : 반환 타입이 명확할 때 사용
- Query : 반환 타입이 명확하지 않을 때 사용
결과 조회 API
query.getResultList()
: 결과가 하나 이상일 때는 리스트를 반환하고, 없으면 빈 리스트를 반환한다.query.getSingleResult()
: 결과가 정확히 하나일 때 단일 객체를 반환한다.- 결과가 없으면 :
NoResultException
예외 발생 - 결과가 둘 이상이면 :
NonUniqueResultException
예외 발생
- 결과가 없으면 :
파라미터 바인딩
이름 기준
Member member = new Member();
member.setUserName("member1");
em.persist(member);
List<Member> result = em.createQuery("select m from Member m where m.userName = :userName", Member.class)
.setParameter("userName", "member1")
.getResultList();
for (Member mem : result) {
System.out.println("mem = " + mem.getUserName());
}
위치 기준
List<Member> result = em.createQuery("select m from Member m where m.userName = ?1", Member.class)
.setParameter(1, "member1")
.getResultList();
- 위치 기준은 수정할 때 인덱싱 바꾸기가 불편하므로 이름 기준으로 사용하도록 하자.
프로젝션
SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라고 한다. 프로젝션 대상으로는 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)이 있다.
- 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 : 스칼라 타입 프로젝션
- DISTINCT로 중복 제거
프로젝션 예제
package jpql;
import javax.persistence.*;
import java.util.List;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setUserName("member1");
em.persist(member);
em.flush();
em.clear();
List<Member> result = em.createQuery("select m from Member m", Member.class).getResultList();
Member findMember = result.get(0);
findMember.setAge(20);
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
- age가 변경되는 것을 볼 수 있다. 즉, 엔티티 프로젝션은 모두 영속성으로 관리해준다.
List<Team> result = em.createQuery("select t from Member m join m.team t", Team.class).getResultList();
- Member 엔티티에서 Team 엔티티 정보를 꺼내올 때 사용하는 방식이다.
List<Address> result = em.createQuery("select o.address from Order o", Address.class).getResultList();
- 임베디드 타입을 처리해주는 쿼리문이다.
여러 값 조회
Query 타입으로 조회
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("userName = " + result[0]);
System.out.println("age = " + 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]);
new 명령어로 조회
DTO 생성
package jpql;
public class MemberDTO {
private String userName;
private int age;
public MemberDTO(String userName, int age) {
this.userName = userName;
this.age = age;
}
}
- 값을 받을 DTO 객체를 생성한다.
List<MemberDTO> result = em.createQuery("select new jpql.MemberDTO(m.userName, m.age) from Member m", MemberDTO.class).getResultList();
MemberDTO memberDTO = result.get(0);
System.out.println("memberDTO = " + memberDTO.getUserName());
System.out.println("memberDTO = " + memberDTO.getAge());
- createQuery를 자세히보면 DTO 클래스를 넣기위해
new 디렉토리 위치
를 넣을 것을 알 수 있다. 즉, 패키지 명을 포함한 전체 클래스명을 입력해야하며, 순서와 타입이 일치하는 생성자가 필요하다.
페이징 API
JPA는 페이징을 다음 두 API로 추상화 해준다.
setFirstResult(int startPosition)
: 조회 시작 위치 (0부터 시작)setMaxResults(int maxResult)
: 조회할 데이터 수
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_ > ?
예제
package jpql;
import javax.persistence.*;
import java.util.List;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
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(0)
.setMaxResults(10)
.getResultList();
System.out.println("result.size = " + result.size());
for (Member member1 : result) {
System.out.println("member = " + member1);
}
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
- 표준스펙을 정의해서 페이징을 정말 간단하게 사용할 수 있다.
조인
- 내부 조인 : SELECT m FROM Member m [INNER] JOIN m.team t
- 내부 조인은 한곳에서 없는 데이터는 넣지 않는다.
- 외부 조인 : SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
- 한 곳이 없어도 null로 값을 채워넣는다.
- 세타 조인 : select count(m) from Member m, Team t where m.username=t.name
- 곱하기로 모두 불러온다.
조인 테스트를 위한 연관관계 설정
회원과 팀사이의 연관 관계에서 둘다 값을 넣어주기 위해 값을 넣어주자.
Getter & Setter는 들어가있으나 코드 길이 상 생략했다.
Member 엔티티
@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) { // 양방향 연관 관계에서 Team에도 데이터가 자동으로 들어가게 설정
this.team = team;
team.getMembers().add(this);
}
}
조인 테스트
내부 조인
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUserName("member1");
member.setAge(10);
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();
- inner join에서 inner는 생략할 수 있다.
외부 조인
String query = "select m from Member m left outer join m.team t";
- 외부 조인도 outer를 생략할 수 있다.
세타 조인
String query = "select m from Member m, Team t where m.userName=t.name";
- 크로스 조인 방식으로 데이터가 곱해져서 모든 경우의 값을 출력한다.
ON 절
ON절을 활용한 조인은 (JPA 2.1부터 지원한다.)
- 조인 대상 필터링
- 연관관계 없는 엔티티 외부 조인 (하이버네이트 5.1부터)
조인 대상 필터링
- 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인하는 경우
String query = "select m from Member m left join m.team t on t.name = 'A'";
List<Member> result = em.createQuery(query, Member.class).getResultList();
System.out.println("result.size() = " + result.size());
- 다음과 같이 사용한다.
연관관계가 없는 엔티티 외부 조인
- 회원의 이름과 팀의 이름이 같은 대상 외부 조인
String query = "select m from Member m left join Team t on m.userName = t.name";
List<Member> result = em.createQuery(query, Member.class).getResultList();
System.out.println("result.size() = " + result.size());
- 다음과 같이 사용한다.
서브 쿼리
select m from Member m
where m.age > (select avg(m2.age) from Member m2)
- 나이가 평균보다 많은 회원
select m from Member m
where (select count(o) from Order o where m = o.member) > 0
- 한 건이라도 주문한 고객 조회
서브 쿼리 지원 함수
- [NOT] EXISTS (subquery) : 서브 쿼리에 결과가 존재하면 참
- {ALL | ANY | SOME}
- ALL (모두 만족해야 참), ANY, SOME (하나라도 만족하면 참)
- [NOT] IN (subquery) : 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참
서브 쿼리 예제
select m from Member m
where exists (select t from m.team t where t.name ='팀A')
- 팀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)
- 어떤 팀이든 팀에 소속된 회원 조회
JPQL 서브 쿼리 한계
- JPA는 WHERE, HAVING 절에서만 서브 쿼리를 사용할 수 있다.
- 하이버네이트에선 SELECT 절도 가능하다.
- FROM절의 서브 쿼리는 JPQL에서 불가능하여 조인으로 풀 수 있으면 풀어서 해결하자.
JPQL 타입 표현
- 문자 : 'HELLO', 'She''s'
- 숫자 : 10L, 10D, 10F
- boolean : TRUE, FALSE
- ENUM : jpabook.MemberType.Admin (패키지명 포함)
- 엔티티 타입 : TYPE(m) = Member (상속 관계에서 사용)
Team team = new Team();
team.setName("A");
em.persist(team);
Member member = new Member();
member.setUserName("teamA");
member.setAge(10);
member.setType(MemberType.ADMIN);
em.persist(member);
em.flush();
em.clear();
String query = "select m.userName, 'HELLO', true from Member m " +
"where m.type = jpql.MemberType.ADMIN";
List<Object[]> result = em.createQuery(query).getResultList();
for (Object[] objects : result) {
System.out.println("objects = " + objects[0]);
System.out.println("objects = " + objects[1]);
System.out.println("objects = " + objects[2]);
}
tx.commit();
- ENUM을 조회할 때 패키지경로를 모두 조회해야 한다.
String query = "select m.userName, 'HELLO', true from Member m " +
"where m.type = :userType";
List<Object[]> result = em.createQuery(query).setParameter("userType", MemberType.ADMIN).getResultList();
- 파라미터 타입으로 만들어서 해결할 수 있다.
em.createQuery("select i from Item i where type(i) = Book ", Item.class).getResultList();
- Item을 상속받은 Book 엔티티를 DTYPE으로 찾는다.
이 외에도 EXISTS, IN, AND, OR, NOT, BETWEEN, LIKE, IS NULL 등 SQL과 문법이 같은 여러 식을 지원해준다.
조건식 CASE
Team team = new Team();
team.setName("A");
em.persist(team);
Member member = new Member();
member.setUserName("teamA");
member.setAge(10);
member.setType(MemberType.ADMIN);
em.persist(member);
em.flush();
em.clear();
String query = "select " +
" case when m.age <= 10 then '학생요금' " +
" when m.age >= 60 then '경로요금' " +
" else '일반요금' "+
"end " +
" from Member m";
List<String> resultList = em.createQuery(query, String.class).getResultList();
for (String s : resultList) {
System.out.println("s = " + s);
}
tx.commit();
- 기본 CASE 식
String query = "select " +
" case t.name " +
" when '팀A' then '인센티브110%' " +
" when '팀B' then '인센티브120%' "+
" else '인센티브105%' "+
"end " +
" from Team t";
- 단순 CASE 식
select coalesce(m.username, '이름 없는 회원') from Member m
- COALESCE : 하나 씩 조회해서 null이 아니면 반환
select NULLIF(m.username, '관리자') from Member m
- NULLIF : 두 값이 같으면 null 반환, 다르면 첫 번째 값 반환
JPQL 기본 함수
- CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE, ABS, SQRT, MOD, SIZE, INDEX(JPA 용도) 등 지원을 해준다.
CONCAT
String query = "select 'a'||'b' From Member m"; // 하이버네이트 방식 지원
String query = "select concat('a','b') From Member m";
SUBSTRING
String query = "select substring(m.username,2,3) From Member m";
LOCATE
String query = "select locate('de','abcdefg') From Member m";
- 해당 시작인덱스인 4를 반환
SIZE
String query = "select size(t.members) From Team t";
- 컬렉션의 사이즈 반환
사용자 정의 함수 호출
select function('group_concat', i.name) from Item i
- 하이버네이트는 사용전 방언에 추가해야 한다.
- 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.
본 포스팅은 인프런 - 김영한님의 '자바 ORM 표준 JPA 프로그래밍 - 기본편'을 참고하여 정리했습니다.