[JPQL] select t fromTeam t **join fetch t.members** where t.name = ‘팀A' [SQL] SELECT T.*, **M.*** FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'
컬렉션 Fetch Join 사용 코드
1 2 3 4 5 6 7 8 9
String jpql = "select t from Team t join fetch t.members where t.name = '팀A'" 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()) { //Fetch Join으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함 System.out.println(“-> username = " + **member.getUsername()**+ ", member = " + member); }
결과
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200 -> username = 회원2, member = Member@0x300 teamname = 팀A, team = Team@0x100 -> username = 회원1, member = Member@0x200 -> username = 회원2, member = Member@0x300
Fetch Join과 DISTINCT
SQL의 DISTINCT는 중복된 결과를 제거하는 명령
JPQL의 DISTINCT 2가지 기능 제공
SQL에 DISTINCT를 추가
애플리케이션에서 엔티티 중복 제거
Fetch Join과 DISTINCT
1 2 3
**select distinct t fromTeam t join fetch t.members where [t.name](http://t.name/) = ‘팀A’**
SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과에서 중복제거 실패
Fetch Join과 DISTINCT
DISTINCT가 추가로 애플리케이션에서 중복 제거시도
같은 식별자를 가진 Team 엔티티 제거
[DISTINCT 추가시 결과]
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200 -> username = 회원2, member = Member@0x300
하이버네이트6 변경 사항
DISTINCT가 추가로 애플리케이션에서 중복 제거 시도
-> 하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아 도 애플리케이션에서 중복 제거가 자동으로 적용됩니다.
Fetch Join과 일반 조인의 차이
일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않음
1 2 3 4 5 6 7 8 9 10
[JPQL] select t fromTeam t join t.members m where t.name = ‘팀A' [SQL] SELECT **T.*** FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'
Fetch Join과 일반 조인의 차이
JPQL은 결과를 반환할 때 연관 관계 고려X
단지 SELECT 절에 지정한 엔티티만 조회할 뿐
여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회X
Fetch Join과 일반 조인의 차이
Fetch Join을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)
Fetch Join은 객체 그래프를 SQL 한번에 조회하는 개념
Fetch Join 실행 예시
Fetch Join은 연관된 엔티티를 함께 조회함
1 2 3 4 5 6 7 8 9 10 11 12
[JPQL] select t fromTeam t **join fetch** t.members where t.name = ‘팀A' [SQL] SELECT **T.*, M.*** FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'
Fetch Join의 특징과 한계
Fetch Join 대상에는 별칭을 줄 수 없다.
하이버네이트는 가능, 가급적 사용X
둘 이상의 컬렉션은 Fetch Join 할 수 없다.
컬렉션을 Fetch Join하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
일대일, 다대일 같은 단일 값 연관 필드들은 Fetch Join해도 페이징 가능
하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
Fetch Join의 특징과 한계
연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
//글로벌 로딩 전략
@OneToMany(fetch = FetchType.LAZY)
실무에서 글로벌 로딩 전략은 모두 지연 로딩
최적화가 필요한 곳은 Fetch Join 적용
m.username 하는 식으로 사용 X
기본적으로 Fetch Join은 결과 전부를 가져오는 것이기에 사용 불가
일부만 끌어오려면 다른 Join을 사용하는 것이 좋다
Fetch Join - 정리
모든 것을 Fetch Join으로 해결할 수 는 없음
Fetch Join은 객체 그래프를 유지할 때 사용하면 효과적
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, Fetch Join 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적
JPQL - 다형성 쿼리
중요도 - 하
TYPE
조회 대상을 특정 자식으로 한정
예) Item 중에 Book, Movie를 조회해라
1 2 3 4 5 6 7
[JPQL] select i fromItem i where type(i) IN (Book, Movie)
[SQL] select i from i where i.DTYPEin (‘B’, ‘M’)
TREAT(JPA 2.1)
자바의 타입 캐스팅과 유사
상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
FROM, WHERE, SELECT(하이버네이트 지원) 사용
TREAT(JPA 2.1) 추가 설명
예) 부모인 Item과 자식 Book이 있다.
1 2 3 4 5 6 7
[JPQL] select i fromItem i where treat(i asBook).author = ‘kim’
[SQL] select i.* fromItem i where i.DTYPE = ‘B’ and i.author = ‘kim’
JPQL - 엔티티 직접 사용
JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용
1 2 3 4 5 6
[JPQL] select **count([m.id](http://m.id/))** from Member m //엔티티의 아이디를 사용 select **count(m)** fromMember m //엔티티를 직접 사용
[SQL](JPQL 둘다 같은 다음 SQL 실행) select count([m.id](http://m.id/)) as cnt from Member m
엔티티 직접 사용 - 기본 키 값
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// 엔티티를 파라미터로 전달 String jpql = “select m fromMember m where m = :member”; List resultList = em.createQuery(jpql) .setParameter("member", member) .getResultList();
// 식별자를 직접 전달 String jpql = “select m fromMember m where m.id = :memberId”; List resultList = em.createQuery(jpql) .setParameter("memberId", memberId) .getResultList();
//실행된 SQL select m.* fromMember m where m.id=?
엔티티 직접 사용 - 외래 키 값
1 2 3 4 5 6 7 8 9 10 11 12 13
Team team = em.find(Team.class, 1L); String qlString = “select m fromMember m where m.team = :team”; List resultList = em.createQuery(qlString) .setParameter("team", team) .getResultList();
String qlString = “select m fromMember m where m.team.id = :teamId”; List resultList = em.createQuery(qlString) .setParameter("teamId", teamId) .getResultList();
// 실행된 SQL select m.* fromMember m where m.team_id=?
JPQL - Named 쿼리
Named 쿼리 - 정적 쿼리
미리 정의해서 이름을 부여해두고 사용하는 JPQL
정적 쿼리
어노테이션, XML에 정의
애플리케이션 로딩 시점에 초기화 후 재사용
**애플리케이션 로딩 시점에 쿼리를 검증 *****
Named 쿼리 - 어노테이션
1 2 3 4 5 6 7 8 9 10 11 12
@Entity @NamedQuery( name = "Member.findByUsername", query="select m from Member m where m.username = :username") public classMember { ... }
<?xml version="1.0" encoding="UTF-8"?> <entity-mappingsxmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"version="2.1"> <named-queryname="Member.findByUsername"> <query><![CDATA[ select m from Member m where m.username = :username ]]></query> </named-query> <named-queryname="Member.count"> <query>select count(m) from Member m</query> </named-query> </entity-mappings>
//검색 String jpql = "**select m From Member m where m.name like ‘%hello%'**"; List<Member> result = em.createQuery(jpql, Member.class) .getResultList();
JPQL 추가 설명
테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리
SQL을 추상화해서 특정 데이터베이스 SQL에 의존X
JPQL을 한마디로 정의하면 객체 지향 SQL
JPQL과 실행된 SQL
JQPL
1 2 3
//검색 String jpql = "select m from Member m where m.age > 18"; List<Member> result = em.createQuery(jpql, Member.class).getResultList();
실행된 SQL
1 2 3 4 5 6 7 8 9
select m.idas id, m.ageas age, m.USERNAMEasUSERNAME, m.TEAM_IDasTEAM_ID from Member m where m.age>18
Criteria 소개
문자가 아닌 JAVA 코드로 JPQL을 작성할 수 있음
JPQL 빌더 역할
JPA 공식 기능
단점
너무 복잡하고 실용성이 없다.
Criteria 대신에 QueryDSL 사용 권장
예시
1 2 3 4 5 6 7 8 9 10 11
//Criteria 사용 준비 CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Member> query = cb.createQuery(Member.class);
//루트 클래스 (조회를 시작할 클래스) Root<Member> m = query.from(Member.class);
JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, 마이바티스등을 함께 사용 가능
단 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요
예) JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 플러시
JPQL(Java Persistence Query Language)
JPQL - 기본 문법과 기능
JPQL 소개
JPQL은 객체 지향 쿼리 언어다.
따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
JPQL은 결국 SQL로 변환된다.
JPQL 문법
JPQL 문법
select m from Member as m where m.age > 18
엔티티와 속성은 대소문자 구분O (Member, age)
JPQL 키워드는 대소문자 구분X (SELECT, FROM, where)
엔티티 이름 사용, 테이블 이름이 아님(Member)
별칭은 필수(m) (as는 생략가능)
집합과 정렬
GROUP BY, HAVING
ORDER BY
1 2 3 4 5 6 7 8
select COUNT(m), //회원수 SUM(m.age), //나이 합 AVG(m.age), //평균 나이 MAX(m.age), //최대 나이 MIN(m.age) //최소 나이 from Member m
TypeQuery, Query
TypeQuery
반환 타입이 명확할 때 사용’
1 2
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
Query
반환 타입이 명확하지 않을 때 사용
1 2
Query query = em.createQuery("SELECT m.username, m.age from Member m");
결과 조회 API
query.getResultList() : 결과가 하나 이상일 때, 리스트 반환
결과가 없으면 빈 리스트 반환
query.getSingleResult() : 결과가 정확히 하나, 단일 객체 반환
결과가 없으면: javax.persistence.NoResultException
둘 이상이면: javax.persistence.NonUniqueResultException
파라미터 바인딩 - 이름 기준, 위치 기준
이름 기준
1 2
SELECT m FROMMember m where m.username=:username query.setParameter("username", usernameParam);
위치 기준
사용 X
위치일 경우 순서 밀릴 가능성 있음
1 2
SELECT m FROMMember m where m.username=?1 query.setParameter(1, usernameParam);
프로젝션
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로 중복 제거
프로젝션 - 여러 값 조회
SELECT m.username, m.age FROM Member m
Query 타입으로 조회
Object[] 타입으로 조회
new 명령어로 조회
단순 값을 DTO로 바로 조회 SELECT
**`new** jpabook.jpql.UserDTO(m.username, m.age)`
FROM
`Member m`
패키지 명을 포함한 전체 클래스 명 입력
순서와 타입이 일치하는 생성자 필요
페이징 API
JPA는 페이징을 다음 두 API로 추상화
setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
setMaxResults(int maxResult) : 조회할 데이터 수
페이징 API 예시
1 2 3 4 5 6
//페이징 쿼리 String jpql = "select m from Member m order by m.name desc"; List<Member> resultList = em.createQuery(jpql, Member.class) .setFirstResult(10) .setMaxResults(20) .getResultList();
페이징 API - MySQL 방언
1 2 3 4 5 6 7 8 9
SELECT M.IDASID, M.AGEASAGE, M.TEAM_IDASTEAM_ID, M.NAMEASNAME FROM MEMBER M ORDERBY M.NAMEDESCLIMIT ?, ?
페이징 API - Oracle 방언
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
SELECT * FROM ( SELECTROW_.*, ROWNUMROWNUM_ FROM ( SELECT M.IDASID, M.AGEASAGE, M.TEAM_IDASTEAM_ID, M.NAMEASNAME FROM MEMBER M ORDERBY M.NAME ) ROW_ WHEREROWNUM <= ? ) WHEREROWNUM_ > ?
조인
내부 조인: SELECT m FROM Member m [INNER] JOIN m.team t
외부 조인: SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
세타 조인: select count(m) from Member m, Team t where m.username= [t.name](http://t.name/)
조인 - ON 절
ON절을 활용한 조인(JPA 2.1부터 지원)
조인 대상 필터링
연관관계 없는 엔티티 외부 조인(하이버네이트 5.1부터)
1. 조인 대상 필터링
예) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
1 2 3 4 5 6 7 8 9 10 11 12
**// JPQL SELECT m, t FROMMember m LEFTJOIN m.team t on t.name = 'A'
// SQL SELECT m.*, t.* FROMMember m LEFTJOINTeam t ON m.TEAM_ID=t.id and t.name='A'**
2. 연관 관계 없는 엔티티 외부 조인
예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
1 2 3 4 5 6 7 8 9 10 11
// JPQL SELECT m, t FROM Member m LEFTJOINTeam t **on** m.username = t.name
// SQL SELECT m.*, t.* FROM Member m LEFTJOINTeam t **ON** m.username = t.name
서브 쿼리
1 2 3 4 5 6 7
// 나이가 평균보다 많은 회원 select m fromMember m where m.age > **(select avg(m2.age) fromMember m2)**
// 한 건이라도 주문한 고객 select m fromMember m where **(select count(o) fromOrder o where m = o.member)** > 0
서브 쿼리 지원 함수
[NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
{ALL | ANY | SOME} (subquery)
ALL 모두 만족하면 참
ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참
[NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
서브 쿼리 - 예제
1 2 3 4 5 6 7 8 9 10 11
// 팀A 소속인 회원 select m fromMember 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)
public voidprintUserAndTeam(String memberId) { Member member = em.find(Member.class, memberId); Team team = member.getTeam(); System.out.println("회원 이름: " + member.getUsername()); System.out.println("소속팀: " + team.getName()); }
회원만 출력
1 2 3 4 5
public voidprintUser(String memberId) { Member member = em.find(Member.class, memberId); Team team = member.getTeam(); System.out.println("회원 이름: " + member.getUsername()); }
프록시 기초
em.find()vsem.getReference()
em.find()
데이터베이스를 통해서 실제 엔티티 객체 조회
em.getReference()
데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
Proxy가 생성한 가짜 객체
프록시 특징
프록시 특징
프록시 객체는 실제 객체의 참조(target)를 보관
프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
프록시 초기화
1 2
Member member = em.**getReference**(Member.class, “id1”); member.getName();
프록시 특징
프록시 객체는 처음 사용할 때 한 번만 초기화
프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님
초기화되면 프록시 객체를 통해서 실제 엔티티에 접근만 가능
프록시 객체는 원본 엔티티를 상속 받음, 따라서 타입 체크시 주의해야 함
(== 비교 실패, 대신 instance of 사용)
영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference() 호출해도 실제 엔티티 반환
1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공
1 2 3 4
Member a = em.find(Member.class, "**member1**"); Member b = em.find(Member.class, "**member1**");
System.out.println(**a == b**); //동일성 비교 **true**
이점 3 : 엔티티 등록 트랜잭션을 지원하는 쓰기 지연
트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
1 2 3 4 5 6 7 8 9 10 11
EntityManager em = emf.createEntityManager(); EntityTransaction transaction = em.getTransaction(); //엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작 em.persist(memberA); em.persist(memberB); //여기까지 INSERT SQL을 데이터베이스에 보내지 않는다. //커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋
member1, member2 있을 경우, 하나씩 차례로 작업한다
이점 4 : 엔티티 수정 변경 감지 (Dirty Checking)
1 2 3 4 5 6 7 8 9 10 11 12 13
EntityManager em = emf.createEntityManager(); EntityTransaction transaction = em.getTransaction(); transaction.begin(); // [트랜잭션] 시작
// 영속 엔티티 조회 Member memberA = em.find(Member.class, "memberA");
// 영속 엔티티 데이터 수정 memberA.setUsername("hi"); memberA.setAge(10); //em.update(member) 이런 코드가 있어야 하지 않을까?
transaction.commit(); // [트랜잭션] 커밋
엔티티 vs 스냅샷
값을 비교하여 수정되었는지 확인한다
이점 5 : 플러시
영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것
플러시 발생
변경 감지
수정된 엔티티 쓰기 지연 SQL 저장소에 등록
쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송 (등록, 수정, 삭제 쿼리)
영속성 컨텍스트를 플러시하는 방법
1 2 3 4 5
em.flush() - 직접 호출
트랜잭션 커밋 - 플러시 자동 호출
JPQL 쿼리 실행 - 플러시 자동 호출
JPQL 쿼리 실행 시 플러시가 자동으로 호출되는 이유
-
1 2 3 4 5 6
em.persist(memberA); em.persist(memberB); em.persist(memberC); //중간에 JPQL 실행 query = em.createQuery("select m from Member m", Member.class); List<Member> members= query.getResultList();