BOOK 3 - 자바 ORM 표준 JPA 프로그래밍(10)-1
객체지향 쿼리 언어 - JPQL
- JPA는 복잡한 검색 조건을 사용해서 엔티티 객체를 조회할 수 있는 다양한 쿼리 기술을 지원한다.
객체지향 쿼리 소개
- 데이터베이스에서 SQL을 사용해 필요한 데이터를 최대한 걸러서 조회해야 한다.
- ORM을 사용하면 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하므로 검색도 테이블이 아닌 엔티티 객체를 대상으로 하는 방법이 필요하다.
- 이런 문제를 해결하기 위해 JPQL이 만들어졌다.
- JPQL은 다음과 같은 특징이 있다.
- 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리이다.
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
- JPA는 JPQL을 분석한 다음 적절한 SQL을 만들어 데이터베이스를 조회한다.
- 조회한 결과로 엔티티 객체를 생성해서 반환한다.
JPQL 소개
- JPQL ( Java Persistence Query Language )은 엔티티 객체를 조회하는 객체지향 쿼리다.
- JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않으므로 데이터베이스 dialect만 변경하면 JPQL을 수정하지 않아도 데이터베이스를 변경할 수 있다.
String jpql = "select m from Member as m where m.username= 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList(0);
- 위 코드는 회원 이름이 ‘kim’ 인 엔티티를 조회한다.
- JPQL에서
Member는 엔티티 이름이고,m.username은 테이블 컬럼명이 아니라 엔티티 객체의 필드명이다. em.createQuery()메소드에 실행할 JPQL과 반환할 엔티티의 클래스 타입을 넘겨주고getResultList()메소드를 호출한다.- JPA는 JPQL을 SQL로 변환해서 데이터베이스를 조회하고 결과로
Member엔티티를 생성해서 반환한다.
- JPA는 JPQL을 SQL로 변환해서 데이터베이스를 조회하고 결과로
Criteria 쿼리 소개
- Criteria는 JPQL을 생성하는 빌더 클래스이다.
- Criteria는 문자가 아닌 프로그래밍 코드로 JPQL을 작성할 수 있다는 장점이 있다.
- 문자 기반 쿼리는 오타가 있어도 컴파일은 성공하고 어플리케이션을 배포할 수 있다.
- 해당 쿼리가 런타임 시점에 오류가 발생하여 문제가 된다.
- Criteria는 문자가 아닌 코드로 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).where(cb.equal(m.get("username"), "kim"));
List<Member> resultList = em.createQuery(cq).getResultList();
m.get("username")에서 필드명을 문자로 작성했다.- 이 부분도 문자가 아닌 코드로 작성하고 싶으면 MetaModel을 사용해야 한다.
- 자바가 제공하는 Annotation Processor 기능을 사용하면 어노테이션을 분석해서 클래스를 생성할 수 있다.
- JPA는 이 기능을 사용해서
Member엔티티 클래스로부터 Criteria 전용 클래스를 생성하는데 이것을 메타 모델이라 한다.
- 이 부분도 문자가 아닌 코드로 작성하고 싶으면 MetaModel을 사용해야 한다.
- Criteria는 코드로 쿼리를 작성할 수 있어서 동적 쿼리를 작성할 때 유용하다.
QueryDSL 소개
- QueryDSL도 Criteria처럼 JPQL 빌더 역할을 한다.
- QueryDSL은 코드 기반이면서 단순하고 사용하기 쉽다는 장점이 있다.
JPAQuery query = new JPAQuery(em);
QMember member = QMember.member;
List<Member> members = query.from(member)
.where(member.username.eq("kim"))
.list(member);
- QueryDSL도 어노테이션 프로세서를 사용해서 쿼리 전용 클래스를 만들어야 한다.
QMember는Member엔티티 클래스를 기반으로 생성한 QueryDSL 쿼리 전용 클래스 이다.
네이티브 SQL 소개
- JPA는 SQL을 직접 사용할 수 있는 기능을 지원하는데 이것을 네이티브 SQL 이라 한다.
- 네이티브 SQL은 데이터베이스에 의존하는 SQL을 작성하기 때문에 데이터베이스를 변경하면 함께 수정해야 한다는 단점이 있다.
String sql = "SELECT ID,AGE,TEAM_ID,NAME FROM MEMBER WHERE NAME = 'kim'";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();
- 네이티브 SQL은
em.createNativeQuery()를 사용하면 나머지는 JPQL과 동일하다.
JDBC 직접 사용, MyBatis 같은 SQL Mapper 프레임워크 사용
Session session = entityManager.unwrap(Session.class);
session.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
// work...
}
})
- JDBC 커넥션에 직접 접근하고 싶으면 JPA 구현체가 제공하는
Session을 구해서doWork()를 호출하면 된다. - JDBC나 MyBatis를 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야 한다.
- JDBC나 MyBatis 같은 SQL 매퍼는 JPA를 우회해서 데이터베이스를 접근한다.
- JPA를 우회하는 SQL에 대해서 JPA가 전혀 인식하지 못하기 때문에 영속성 컨텍스트와 데이터베이스를 불일치 상태로 만들어 데이터 무결성을 훼손할 수 있다.
- 이런 이슈를 해결하기 위해 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 데이터베이스와 영속성 컨텍스트를 동기화해야 한다.
JPQL
- 다음 도메인 모델을 가정해보자.


기본 문법과 쿼리 API
- JPQL도 SQL과 비슷하게
SELECT,UPDATE,DELETE문을 사용할 수 있다.
SELECT 문
SELECT m FROM Member AS m where m.username = 'Hello'
- 대소문자 구분
- 엔티티와 속성은 대소문자를 구분한다.
- 반면에
SELECT,FROM,AS같은 JPQL 키워드는 대소문자를 구분하지 않는다.
- 엔티티 이름
- JPQL에서 사용한
Member는 클래스 명이 아니라 엔티티 명이다. - 엔티티 명은
@Entity(name="XXX")로 지정할 수 있다.- 따로 지정하지 않으면 클래스명을 기본 값으로 사용한다.
- JPQL에서 사용한
- alias는 필수
Member AS m식으로Member엔티티에m이라는 별칭을 주었다.as는 생략 가능하다.
- JPQL은 별칭을 필수로 사용해야 한다.
TypedQuery, Query
- 작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 한다.
- 쿼리 객체는
TypedQuery와Query가 있다.
- 쿼리 객체는
- 반환할 타입을 명확하게 지정할 수 있으면
TypedQuery객체를 사용하고, 반환 타입을 명확하게 지정할 수 없으면Query객체를 사용하면 된다.
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
List<Member> resultList = query.getResultList();
em.createQuery()의 두번째 파라미터에 반환할 타입을 지정하면TypedQuery를 반환한다.
Query query = em.createQuery("SELECT m.username, m.age from Member m");
List resultList = query.getResultList();
for(Object o : resultList) {
Object[] result = (Obejct[]) o;
System.out.println("username = " + result[0]);
System.out.println("age = " + result[0]);
}
- 위 코드처럼 여러 엔티티나 컬럼을 선택할 때는 조회 대상의 타입이 명확하지 않으므로
Query객체를 사용한다. Query객체는 SELECT 절의 조회 대상이 둘 이상이면Object[]를 반환하고, 하나면Object를 반환한다.
결과 조회
- 다음 메소드들을 호출하면 실제 쿼리를 실행해서 데이터베이스를 조회한다.
query.getResultList(): 결과를 반환한다. 결과가 없으면 빈 컬렉션을 반환한다.query.getSingleResult(): 결과가 정확히 하나일 때 사용한다.- 결과가 없으면
NoResultException예외가 발생한다. - 결과가 1개보다 많으면
NonUniqueResultException예외가 발생한다.
- 결과가 없으면
파라미터 바인딩
- JPQL은 이름 기준 파라미터 바인딩도 지원한다.
이름 기준 파라미터
- Named Parameters는 파라미터를 이름으로 구분하는 방법이다.
- 파라미터 앞에
:를 사용한다.
- 파라미터 앞에
String usernameParam = "User1";
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class);
query.setParameter("username", usernameParam);
List<Member> resultList = query.getResultList();
-
JPQL API는 대부분 메소드 체인 방식으로 설계되어 있어서 다음과 같이 연속해서 작성할 수 있다.
List<Member> members = em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class) .setParameter("username", usernameParam) .getResultList();
위치 기준 파라미터
- Positional paramters를 사용하려면
?다음에 위치 값을 지정해준다.- 위치 값은 1부터 시작한다.
List<Member> members = em.createQuery("SELCT m FROM Member m where m.username = ?1", Member.class)
.setParameter(1, usernameParam)
.getResultList();
파라미터 바인딩 방식을 사용하면 파라미터의 값이 달라도 같은 쿼리로 인식해서 JPA는 JPQL을 SQL로 파싱한 결과를 재사용할 수 있어 성능이 개선된다.
데이터베이스 내부에서도 실행한 SQL을 파싱해서 사용하는데 같은 쿼리는 파싱한 결과를 재사용할 수 있기 때문에 어플리케이션과 데이터베이스 모두 해당 쿼리의 파싱 결과를 재사용할 수 있어서 전체 성능이 향상된다.
프로젝션
- SELECT 절에 조회할 대상을 지정하는 것을 projection 이라고 한다.
- 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입이 있다.
엔티티 프로젝션
SELECT m FROM Member m
SELECT m.team FROM Member m
- 회원과 회원과 연관된 팀을 조회했다.
- 둘 다 엔티티를 프로젝션 대상으로 사용했다.
- SQL 처럼 컬럼을 하나하나 나열해서 조회할 필요가 없다.
- 조회된 엔티티는 영속성 컨텍스트에서 관리된다.
임베디드 타입 프로젝션
-
JPQL에서 임베디드 타입은 엔티티와 거의 비슷하게 사용된다.
-
임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다.
-
즉, 다음과 같이 쿼리를 작성할 수 없다.
String query = "SELECT a FROM Address a";
-
-
Address값 타입이 속해있는Order엔티티가 시작점이 되어야 한다.
String query = "SELECT o.address FROM Order o";
List<Address> addresses = em.createQuery(query, Address.class)
.getResultList();
- 위 코드를 실행했을 때 실행되는 SQL은 다음과 같다.
SELECT order.city, order.street, order.zipcode
FROM Orders order
- 임베디드 타입은 엔티티 타입이 아닌 값 타입이므로 위와 같이 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.
스칼라 타입 프로젝션
- 숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다.
List<String> usernames = em.createQuery("SELECT username FROM Member m", String.class).getResultList();
-
중복 데이터를 제거하려면
DISTINCT를 사용한다.SELECT DISTINCT username FROM Member m
여러 값 조회
- 엔티티 단위가 아니라 필요한 데이터들만 선택해서 조회해야 할 때도 있다.
Query객체를 사용하여 조회할 수 있다.
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList = query.getResultList();
Iterator iterator = resultList.iterator();
while(iterator.hasNext()) {
Object[] row = (Object[]) iterator.next();
String username = (String) row[0];
Integer age = (Integer) row[1];
}
-
generic에
Object[]를 사용하면 좀 더 간결하게 작성할 수 있다. -
스칼라 타입뿐만 아니라 엔티티 타입도 여러 값을 함께 조회할 수 있다.
List<Object[]> resultList = em.createQuery("SELECT o.member, o.product, o.orderAmount FROM Order o").getResultList(); for(Object[] row : resultList) { Member member = (Member) row[0]; Product product = (Product) row[1]; int orderAmount = (Integer) row[2]; }
NEW 명령어
username과age두 필드만 프로젝션하는 경우에UserDTO라는 유의미한 객체로 결과를 반환받고 싶다면new명령어를 사용한다.
TypedQuery<UserDTO> query = em.CreateQuery("SELECT new com.jpql.UserDTO(m.username, m.age) FROM Member m", UserDTO.class);
List<UserDTO> resultList = query.getResultList();
SELECT다음에new명령어를 사용하면 반환받을 클래스를 지정할 수 있는데, 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다.- 해당 클래스에 순서와 타입이 일치하는 생성자가 선언되어 있어야 한다.
- 클래스를 지정할 때 패키지명을 포함한 전체 클래스명을 입력해야 한다.
페이징 API
- JPA는 페이징을 다음 두 API로 추상화했다.
setFirstResult(int startPosition): 조회 시작 위치(0부터 시작)setMaxResults(int maxResult): 조회할 데이터 수
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class);
query.setFirstResult(10);
query.setMaxResults(20);
query.getResultList();
-
위 코드를 실행하면 11번째 데이터부터 20건의 데이터를 조회한다.
-
MySQL 데이터베이스에서 실행되는 쿼리는 다음과 같다. (데이터베이스마다 페이징을 처리하는 SQL 문법이 다르다.)
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 ?, ?
집합과 정렬
- 집합은 집합함수와 함께 통계 정보를 구할 때 사용한다.
집합 함수
| 함수 | 설명 | 반환 타입 |
|---|---|---|
| COUNT | 결과 수를 구한다. | Long |
| MAX , MIN | 최대, 최소 값을 구한다. 문자, 숫자, 날짜 등에 사용한다. | - |
| AVG | 평균값을 구한다. 숫자 타입만 사용할 수 있다. | Double |
| SUM | 합을 구한다. 숫자 타입만 사용할 수 있다. | Long , Double , BigInteger , BigDecimal |
집합 함수 사용시 참고사항
NULL값은 무시하므로 통계에 잡히지 않는다.DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있다.DISTINCT를COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.
GROUP BY, HAVING
GROUP BY는 통계 데이터를 구할 때 특정 그룹끼리 묶어준다.HAVING은GROUP BY와 함께 사용하는데GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링한다.
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
정렬(ORDER BY)
ORDER BY는 결과를 정렬할 때 사용한다.ASC: 오름차순DESC: 내림차순
select m from Member m order by m.age DESC, m.username ASC
JPQL 조인
-
JPQL 조인은 SQL 조인과 기능은 같고 문법만 약간 다르다.
-
JPQL은
JOIN명령어 다음에 조인할 객체의 연관 필드를 사용한다.
내부 조인
- 내부 조인은
INNER JOIN을 사용한다.
String teamName = "teamA";
String query = "SELECT m FROM Member m INNER JOIN m.team t"
+ "WHERE t.name = :teamName";
List<Member> members = em.createQuery(query, Member.class)
.setParameter("teamName", teamName)
.getResultList();
-
위 코드를 실행했을 때 생성되는 내부 조인 SQL은 다음과 같다.
SELECT M.ID AS ID, M.AGE AS AGE, M.TEAM_ID AS TEAM_ID, M.NAME AS NAME FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID WHERE T.NAME = ? -
JPQL 조인은 연관 필드를 사용한다.
- 연관 필드는 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 의미한다.
- 여기서는
m.team이 연관 필드이다.
-
JPQL 조인을 SQL 조인처럼 사용하면 문법 오류가 발생한다.
FROM Member m JOIN Team t // 잘못된 JPQL 조인 => 오류 발생
외부 조인
SELECT m
FROM Member m LEFT [OUTER] JOIN m.team t
-
위 JPQL 쿼리를 실행하면 데이터베이스에서 다음 SQL이 실행된다.
SELECT M.ID AS ID, M.AGE AS AGE, M.TEAM_ID AS TEAM_ID, M.NAME AS NAME FROM MEMBER M LEFT OUTER JOIN TEAM T ON M.TEAM_ID = T.ID WHERE T.NAME = ?
컬렉션 조인
- 일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라 한다.
- 예를 들어
팀 -> 회원의 경우 일대다 조인으로 컬렉션 값 연관 필드를 사용한다.
- 예를 들어
SELECT t, m FROM Team t LEFT JOIN t.members m
- 팀과 팀이 보유한 회원 목록을 컬렉션 값 연관필드(
t.members)로 외부 조인했다.
세타 조인
WHERE절을 사용해서 세타 조인을 할 수 있다.- 세타 조인을 사용하면 전혀 관계없는 엔티티도 조회할 수 있다.
// JPQL
select count(m) from Member m, Team t
where m.username = t.name
// SQL
SELECT COUNT(M.ID)
FROM MEMBER M CROSS JOIN TEAM T
WHERE M.USERNAME = T.NAME
JOIN ON 절
ON절을 사용하면 조인 대상을 필터링하고 조인할 수 있다.- 내부조인의
ON절은WHERE절을 사용할 때와 결과가 동일하므로 보통 외부조인에서만 사용한다.
// 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'
페치 조인
- 페치 조인은 SQL에서 다루는 조인의 종류는 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다.
- 연관된 엔티티나 컬렉션을 한번에 같이 조회하는 기능으로,
JOIN FETCH명령어로 사용할 수 있다.
엔티티 페치 조인
- 회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회하는 경우를 생각해보자.
select m from Member m join fetch m.team
-
회원(
m)과 연관된 팀(m.team) 을 함께 조회한다. -
페치 조인은 alias를 사용할 수 없다.
-
실행되는 SQL은 다음과 같다.
SELECT M.*, T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID

- 엔티티 페치 조인 JPQL에서
select m으로 회원 엔티티만 프로젝션했지만 실행된 SQL을 보면M.*, T.*로 회원과 연관된 팀도 함께 조회된 것을 확인할 수 있다.
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();
for(Member member : members) {
System.out.println("username = " + member.getUsername() + ", teamname = " + member.getTeam().getName());
}
- 위 코드에서 회원 엔티티와 함께 팀 엔티티도 페치 조인했기 때문에
member.getTeam().getName()코드를 실행할 때 지연 로딩이 발생하지 않고 실제 엔티티에서 값을 조회한다.Team엔티티에 지연 로딩 설정을 했어도 페치 조인을 실행하면 쿼리 실행시에 연관된 팀 엔티티도 함께 데이터를 조회한다.
컬렉션 페치 조인
- 일대다 관계에서 컬렉션을 페치 조인할 수도 있다.
select t from Team t join fetch t.members where t.name = 'teamA'
- 팀(
t)을 조회하면서 연관된 회원 컬렉션(t.members)도 함께 조회한다. - 실행되는 SQL은 다음과 같다.
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
WHERE T.NAME = 'teamA'

- 컬렉션을 페치 조인한 JPQL에서
select t로 팀 엔티티만 프로젝션했지만 실행된 SQL을 보면T.*, M.*로 회원 엔티티도 함께 조회된 것을 확인할 수 있다. - 일대다 관계의 페치 조인의 경우 위와 같이 연관된 테이블과 INNER JOIN을 하면서 하나의 PK가 중복되어 결과 테이블에 조회될 수 있다.
- 즉,
팀A라는 이름의 팀은TEAM테이블에 하나 존재하지만, 연관된 회원 데이터가 여러개인 경우 여러개의 결과가 나오게 된다. - 영속성 컨텍스트는 식별자로 엔티티를 구분하기 때문에 팀 엔티티는 하나의 인스턴스로 생성되지만, 결과 리스트에는 같은 팀 인스턴스가 두개 포함되는 것이다.
- 즉,
String jpql = "select t from Team t join fetch t.members where t.name = 'teamA'";
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();
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);
}
}
-
위 출력 결과는 다음과 같다.
teamname = teamA, team = Team@0x100 -> username = member1, member = Member@0x200 -> username = member2, member = Member@0x300 teamname = teamA, team = Team@0x100 -> username = member1, member = Member@0x200 -> username = member2, member = Member@0x300- 같은
teamA가 2건 조회된 것을 확인할 수 있다.
- 같은
페치 조인과 DISTINCT
- JPQL의
DISTINCT명령어는 SQL에DISTINCT를 추가하는 것은 물론이고 어플리케이션에서 한 번 더 중복을 제거한다. - 바로 직전에 컬렉션 페치 조인에서
teamA가 중복으로 조회되는 문제를 해결하기 위해DISTINCT를 추가할 수 있다.
select distinct t from Team t join fetch t.members where t.name = 'teamA'
DISTINCT를 사용하면 SQL에SELECT DISTINCT가 추가되지만 위와 같은 경우는 각 로우의 데이터가 다르므로 효과가 없다.- 다음으로 어플리케이션에서
distinct명령어를 보고 중복된 데이터를 걸러낸다.select distinct t의 의미는 팀 엔티티의 중복을 제거하라는 의미이다.- 따라서 결과 리스트에서 중복된
teamA결과는 지우고 하나의 결과만 나오게 된다.
페치 조인과 일반 조인의 차이
- 페치 조인을 사용하지 않고 일반 조인을 사용하면 어떻게 될까?
select t from Team t join t.members m where t.name = 'teamA'
- 실행되는 SQL은 다음과 같다.
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
WHERE T.NAME = 'teamA'
- 실행된 SQL의 SELECT 절을 보면 팀만 조회하고 회원은 조회하지 않는다.
- JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다.
- 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다.
- 만약 회원 엔티티에 지연 로딩을 설정했다면 프록시나 컬렉션 래퍼를 반환하고, 즉시 로딩을 설정했다면 회원 엔티티를 조회하기 위해 쿼리를 한번 더 실행한다.
- 반면에 페치 조인을 사용하면 연관된 엔티티도 함께 조회하기 때문에 쿼리를 한번 더 실행하는 일이 없다.
페치 조인의 특징과 한계
-
페치 조인을 사용하면 SQL 한번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다.
-
페치 조인은 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선하기 때문에 엔티티를 지연 로딩으로 설정해도 페치 조인을 사용하면 함께 조회한다.
@OneToMany(fetch=FetchType.LAZY) // 글로벌 로딩 전략 -
글로벌 로딩 전략을 즉시 로딩으로 설정하면 어플리케이션 전체에서 항상 즉시 로딩이 일어난다.
- 사용하지 않는 연관 엔티티까지 자주 로딩하므로 성능에 악영향을 미칠 수 있다.
-
글로벌 로딩 전략은 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이다.
한계
- 페치 조인 대상에는 alias를 줄 수 없다.
- 따라서
SELECT,WHERE, 서브 쿼리에 페치 조인 대상을 사용할 수 없다.
- 따라서
- 둘 이상의 컬렉션을 페치할 수 없다.
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
- 컬렉션(일대다)이 아닌 단일 값 연관 필드(일대일, 다대일)들은 페치 조인을 사용해도 페이징 API를 사용할 수 있다.
경로 표현식
- 경로 표현식은
.을 찍어 객체 그래프를 탐색하는 것이다. m.username,m.team,t.name등이 경로 표현식을 사용한 예이다.
경로 표현식의 용어 정리
- 상태 필드 : 단순히 값을 저장하기 위한 필드
- 연관 필드 : 연관관계를 위한 필드, 임베디드 타입 포함
- 단일 값 연관 필드 :
@ManyToOne,@OneToOne, 대상이 엔티티 - 컬렉션 값 연관 필드 :
@OneToMany,@ManyToMany, 대상이 컬렉션
- 단일 값 연관 필드 :
경로 표현식과 특징
- 상태 필드 경로 : 경로 탐색의 끝이다. 더이상 탐색할 수 없다.
- 단일 값 연관 필드 경로 : 묵시적으로 내부 조인이 일어난다. 계속 탐색할 수 있다.
- 컬렉션 값 연관 필드 경로 : 묵시적으로 내부 조인이 일어난다. 더이상 탐색할 수 없다.
- 단,
FROM절에서 조인을 통해 alias를 얻으면 별칭으로 탐색할 수 있다.
- 단,
단일 값 연관 필드 경로 탐색
select o.member from Order o
- 이 JPQL을 실행한 SQL은 다음과 같다.
SELECT m.*
FROM Orders o
INNER JOIN Member m ON o.MEMBER_ID = m.ID
- 단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는데 이것을 묵시적 조인이라 한다.
- 묵시적 조인은 모두 내부 조인이다.
- 좀 더 복잡한 경우를 생각해보자.
select o.member.team
from Order o
where o.product.name = 'productA' and o.address.city = 'JINJU'
- 실행되는 SQL은 다음과 같다.
SELECT t.*
FROM Orders o
INNER JOIN Member m on o.MEMBER_ID = m.ID
INNER JOIN Team t on m.TEAM_ID = t.ID
INNER JOIN Product p on o.PRODUCT_ID = p.ID
WHERE p.name = 'productA' AND o.city = 'JINJU'
- 실행된 SQL을 보면 총 3번의 묵시적 조인이 발생했다.
o.address같이 임베디드 타입에 접근하는 것도 단일 값 연관 경로 탐색이지만ORDERS테이블에 이미 포함되어 있으므로 조인은 일어나지 않는다.
컬렉션 값 연관 경로 탐색
-
t.members.username처럼 컬렉션에서 경로 탐색을 시작하는 것은 허용되지 않는다. -
컬렉션에서 경로 탐색을 하고 싶으면 조인을 사용해서 새로운 별칭을 획득해야 한다.
select m.username from Team t join t.members m -
컬렉션은 컬렉션의 크기를 구할 수 있는
size라는 특별한 기능을 사용할 수 있다.- SQL에서
COUNT라는 함수로 적절히 변환된다.
select t.members.size from Team t - SQL에서
경로 탐색을 사용한 묵시적 조인 시 주의사항
- 항상 내부 조인이다.
- 컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야 한다.
서브 쿼리
- JPQL도 SQL처럼 서브 쿼리를 지원한다.
- 서브 쿼리는
WHERE,HAVING절에만 사용할 수 있고,SELECT,FROM절에서는 사용할 수 없다.
서브 쿼리 사용 예제
-
나이가 평균보다 많은 회원을 찾는다.
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-
이 쿼리는 컬렉션 값 연관 필드의
size기능을 사용해도 같은 결과를 얻을 수 있다.select m from Member m where m.orders.size > 0
-
서브 쿼리 함수
-
[NOT] EXISTS (subquery)
- 서브 쿼리에 결과가 존재하면 참 (NOT은 반대)
select m from Member m where exists (select t from m.team t where t.name = 'teamA') -
{ALL ANY SOME} (subquery) - 비교 연산자와 같이 사용한다.
ALL: 조건을 모두 만족하면 참이다.ANY/SOME: 조건을 하나라도 만족하면 참이다.
select o from Oerder 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) -
[NOT] IN (subquery)
- 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참이다.
select t from Team t where t IN (select t2 from Team t2 JOIN t2.members m2 where m2.age >= 20)
조건식
타입 표현
| 종류 | 설명 | 예제 |
|---|---|---|
| 문자 | 작은 따옴표 사이에 표현 작은 따옴표를 표현하고 싶으면 작은 따옴표 연속 두개(‘’) 사용 |
‘HELLO’ ‘She’’s’ |
| 숫자 | L (Long 타입) D (Double 타입) F (Float 타입) |
10L 10D 10F |
| 날짜 | DATE {d ‘yyyy-mm-dd’} TIME {t ‘hh-mm-ss’} DATETIME {ts ‘yyyy-mm-dd hh:mm:ss.f’} |
{d ‘2020-10-31’} {t ‘14-31-20’} {ts ‘2020-10-31 14:31:20.123’} |
| Boolean | TRUE / FALSE | |
| Enum | 패키지명을 포함한 전체 클래스명을 사용 | com.jpa.MemberEnum.Admin |
| 엔티티 타입 | 엔티티 타입을 표현한다. 주로 상속과 관련해서 사용 | TYPE(m) = Member |
연산자 우선 순위
- 경로 탐색 연산(
.) - 수학 연산 :
+,-,*,/ - 비교 연산 :
=,>,>=,<,<=,[NOT] BETWEEN,[NOT] LIKE,[NOT] IN,IS [NOT] NULL,IS [NOT] EMPTY,[NOT] MEMBER [OF],[NOT] EXISTS - 논리 연산 :
NOT,AND,ORAND: 둘 다 만족하면 참NOT: 조건식의 결과 반대OR: 둘 중 하나만 만족해도 참
BETWEEN , IN , LIKE , NULL 비교
-
BETWEEN
- X [NOT] BETWEEN A AND B
- X는 A~B 사이의 값이면 참 (A, B 값 포함)
select m from Member m where m.age between 10 and 20 -
IN
- X [NOT] IN
- X와 같은 값이 하나라도 있으면 참. 서브 쿼리를 사용할 수 있다.
select m from Member m where m.username in ('회원1', '회원2') -
LIKE
- 문자표현식 [NOT] LIKE 패턴값 [ESCAPE 이스케이프문자]
-
문자표현식과 패턴식을 비교한다.
%: 아무 값들이 입력되어도 된다.(공백 포함)_: 아무 값이 입력되어도 되지만 값이 있어야 한다.(한 글자)
// 중간에 '원'이라는 단어가 들어간 회원 select m from Member m where m.username like '%원%' // 처음에 회원이라는 단어가 포함 where m.username like '회원%' // 회원3 where m.username like '__3' -
NULL 비교
-
{단일값 경로 입력 파라미터} IS [NOT] NULL - NULL인지 비교한다.
- NULL은
=으로 비교하면 안되고IS NULL을 사용해야 한다.
- NULL은
-
컬렉션 식
-
컬렉션은 컬렉션 식만 사용할 수 있다.
-
EMPTY
- {컬렉션 값 연관 필드 경로} IS [NOT] EMPTY
- 컬렉션에 값이 비었으면 참
select m from Member m where m.orders is not empty-
실행되는 SQL
SELECT m.* FROM MEMBER m WHERE EXISTS ( SELECT o.ID FROM ORDERS o WHERE m.ID = o.MEMBER_ID )
-
MEMBER
- {엔티티나 값} [NOT] MEMBER [OF] {컬렉션 값 연관 필드 경로}
- 엔티티나 값이 컬렉션에 포함되어 있으면 참
select t from Team t where :memberParam member of t.members
스칼라 식
-
스칼라는 숫자, 문자, 날짜, case, 엔티티 타입(타입 정보) 같은 가장 기본적인 타입들을 말한다.
-
수학 식 :
+,-,*,/ -
문자함수
함수 설명 예제 CONCAT(문자1, 문자2) 문자를 합한다. CONCAT(‘A’, ‘B’) = ‘AB’ SUBSTRING(문자, 위치, [길이]) 위치부터 시작해 길이만큼 문자를 자른다.
길이 값이 없으면 나머지 전체 길이를 뜻한다.SUBSTRING(‘ABCDEF’, 2, 3) = ‘BCD’ TRIM([ [ LEADING | TRAILING | BOTH ] [트림 문자] FROM ] 문자) LEADING: 왼쪽만TRAILING: 오른쪽만BOTH: 양쪽 다
트림 문자를 제거한다.
트림 문자의 기본값은 공백이다.TRIM(‘ ABC ‘) = ‘ABC’ LOWER(문자) 소문자로 변경 LOWER(‘ABC’) = abc UPPER(문자) 대문자로 변경 UPPER(‘abc’) = ABC LENGTH(문자) 문자 길이 LENGTH(‘ABC’) = 3 LOCATE(찾을 문자, 원본 문자, [검색 시작 위치]) 검색위치부터 문자를 검색하여 위치를 반환
1부터 시작, 못 찾으면 0 반환LOCATE(‘DE’, ‘ABCDEF’) = 4 -
수학 함수
함수 설명 예제 ABS(수학식) 절대값을 구한다. ABS(-10) = 10 SQRT(수학식) 제곱근을 구한다. SQRT(4) = 2.0 MOD(수학식, 나눌 수) 나머지를 구한다. MOD(4, 3) = 1 SIZE(컬렉션 값 연관 필드 경로식) 컬렉션의 크기를 구한다. SIZE(t.members) INDEX(별칭) LIST 타입 컬렉션의 위치값을 구한다.
(@OrderColumn을 사용하는 LIST 타입만 가능)t.members m where INDEX(m) > 3 -
날짜 함수
CURRENT_DATE: 현재 날짜CURRENT_TIME: 현재 시간CURRENT_TIMESTAMP: 현재 날짜 시간
select e from Event e where e.endDate < CURRENT_DATE-
하이버네이트는 날짜 타입에서 년, 월, 일, 시간, 분, 초 값을 구하는 기능을 지원한다.
YEAR,MONTH,DAY,HOUR,MINUTE,SECOND
select year(CURRENT_TIMESTAMP), month(CURRENT_TIMESTAMP), day(CURRENT_TIMESTAMP) from Member
CASE 식
-
특정 조건에 따라 분기할 때 CASE 식을 사용한다.
-
기본 CASE
select case when m.age <= 10 then '학생요금' when m.age >= 60 then '경로요금' else '일반요금' end from Member m -
심플 CASE
select case t.name when 'teamA' then '인센티브 110%' when 'teamB' then '인센티브 120%' else '인센티브 105%' end from Team t -
COALESCE
- 스칼라식을 차례대로 조회해서 null이 아니면 반환한다.
select coalesce(m.username, '이름 없는 회원') from Member m -
NULLIF
- 두 값이 같으면
null을 반환하고, 다르면 첫번째 값을 반환한다.
select NULLIF(m.username, '관리자') from Member m - 두 값이 같으면
다형성 쿼리
- JPQL로 부모 엔티티를 조회하면 그 자식 엔티티도 함께 조회한다.
@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="DTYPE")
public abstract class Item { ... }
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
...
private String author;
}
// Album, Movie
-
다음과 같이 조회하면
Item의 자식 엔티티도 함께 조회한다.List resultList = em.createQuery("select i from Item i").getResultList();-
SINGLE_TABLE전략을 사용할 때 실행되는 SQL은 다음과 같다.SELECT * FROM ITEM -
JOINED전략을 사용할 때 실행되는 SQL은 다음과 같다.SELECT i.ITEM_ID, i.DTYPE, i.name, i.price, i.stockQuantity, b.author, b.isbn, a.artist, a.etc, m.actor, m.director FROM ITEM i LEFT OUTER JOIN BOOK b ON i.ITEM_ID = b.ITEM_ID LEFT OUTER JOIN ALBUM a ON i.ITEM_ID = a.ITEM_ID LEFT OUTER JOIN MOVIE m ON i.ITEM_ID = m.ITEM_ID
-
TYPE
TYPE은 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용한다.
select i from Item i where type(i) IN (Book, Movie)
- 데이터베이스에서 실행되는 SQL은 다음과 같다.
SELECT i.* FROM ITEM i WHERE i.DTYPE in ('B', 'M')
TREAT
TREAT는 자바의 타입 캐스팅과 비슷하다.- 상속 구조에서 부모 타입을 특정 자식 타입으로 변환하여 다룰 때 사용한다.
select i from Item i where treat(i as Book).author = 'kim'
- 데이터베이스에서 실행되는 SQL은 다음과 같다.
SELECT i.* FROM ITEM i
WHERE i.DTYPE = 'B' AND i.author = 'kim'
기타 정리
enum은=비교 연산만 지원한다.- 임베디드 타입은 비교를 지원하지 않는다.
NULL 정의
- 조건을 만족하는 데이터가 하나도 없으면 NULL이다.
- NULL은 알수 없는 값이다.
NULL == NULL은 알 수 없는 값이다.NULL is NULL은 참이다.
엔티티 직접 사용
기본 키 값
- Entity instance는 참조 값으로 식별하고, Table row는 기본 키 값으로 식별한다.
- JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값을 사용한다.
select count(m.id) from Member m // 엔티티의 아이디를 사용
select count(m) from Member m // 엔티티를 직접 사용
- 위 두개의 JPQL을 실행한 SQL은 동일하다.
SELECT COUNT(m.ID) AS cnt FROM MEMBER m
-
엔티티를 파라미터로 직접 받아도 역시 SQL에서 기본 키 값으로 변환된다.
String jpql = "select m from Member m where m = :member"; List resultList = em.createQuery(jpql).setParameter("member", member) .getResultList();SELECT m.* FROM MEMBER m WHERE m.ID = ?
외래 키 값
Team team = em.find(Team.class, 1L);
String jpql = "select m from Member m where m.team = :team";
List resultList = em.createQuery(jpql).setParameter("team", team).getResultList();
m.team은 현재TEAM_ID라는 외래 키와 매핑되어 있기 때문에 다음과 같은 SQL이 실행된다.
SELECT m.* FROM MEMBER m WHERE m.TEAM_ID = ?
Named 쿼리 : 정적 쿼리
- JPQL 쿼리는 크게 동적 쿼리와 정적 쿼리로 나눌 수 있다.
- 동적 쿼리
em.createQuery("select ...")처럼 JPQL을 문자로 완성해서 직접 넘기는 것을 동적 쿼리라 한다.- 런타임에 특정 조건에 따라 JPQL을 동적으로 구성한다.
- 정적 쿼리
- 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용하는 것을 Named 쿼리라고 한다.
- Named 쿼리는 한 번 정의하면 변경할 수 없는 정적 쿼리다.
@NamedQuery를 사용해서 작성할 수 있다.
- Named 쿼리는 어플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해둔다.
- 오류를 빨리 확인할 수 있고, 사용하는 시점에는 파싱된 결과를 재사용하므로 성능상 이점도 있다.
- Named 쿼리는 정적 SQL이 생성되므로 데이터베이스 조회 성능 최적화도 된다.
어노테이션 사용하여 정의
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username")
public class Member {
...
}
name: 쿼리 이름 지정query: 사용할 정적 쿼리 지정
List<member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "member1")
.getResultList();
- Named 쿼리를 사용할 때는
em.createNamedQuery()메서드에 Named 쿼리 이름을 파라미터로 넘겨주면 된다. - 2개 이상의 Named 쿼리를 지정할 때는
@NamedQueries를 사용하면 된다.