- JPQL은 객체지향 쿼리 언어다. 따라서 테이블을 대상으로 쿼리 하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
- JPQL은 SQL을 추상화 해서 특정 데이터베이스 SQL에 의존하지 않는다.
- JPQL은 결국 SQL로 변환된다.
- 엔티티와 속성은 대소문자 구분(Member,age)
- JPQL 키워드는 대소문자 구분 X (select, from, where)
- 엔티티 이름을 사용함(select m from Member m)
- 별칭은 필수(as는 생략가능)
select
COUNT(m),
SUM(m.age),
AVG(m.age),
MAX(m.age),
MIN(m.age)
from Member m
TypeQuery<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
이름 기준
select m from Member m where m.usernmae = :username
query.setParameter("username", usernameParam);
위치 기준
select m from Member m where m.username = ?1
query.setParameter(1, usernameParam);
select m.username, m.age from Member m
List<MemberDTO> result = em.createQuery("select new jpqlbook.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());
JPA는 페이징을 다음 두 API로 추상화했다.
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> resultList = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(1)
.setMaxResults(10)
.getResultList();
System.out.println("resultListSize = " + resultList.size());
for (Member member : resultList) {
System.out.println("memberUserName " + member.getUsername());
}
내부 조인
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
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
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
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
// String query = "select m.username, 'HELLO', TRUE from Member m " +
// "where m.type = jpqlbook.MemberType.ADMIN";
// 파라미터로 전달
String query = "select m.username, 'HELLO', TRUE from Member m " +
"where m.type =:userType";
List<Object[]> resultList = em.createQuery(query)
.setParameter("userType", MemberType.ADMIN)
.getResultList();
System.out.println("resulListSize = " + resultList.size());
for(Object[] objects: resultList){
System.out.println("Object[] " + objects[0]);
System.out.println("Object[] " + objects[1]);
System.out.println("Object[] " + objects[2]);
}
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> resultList = em.createQuery(query, String.class)
.getResultList();
for (String s : resultList) {
System.out.println(s);
}
ex) 사용자 이름이 없으면 "이름 없음" 반환
select coalesce(m.username , "이름 없음") from Member m
ex) 사용자 이름이 '관리자'면 null을 반환하고 나머지는 본인의 이름을 반환
select NULLIF(m.username, '관리자') from Member m
- 상태 필드(state field) : 단순히 값을 저장하기 위한 필드 (ex: m.username)
- 연관 필드(association field) : 연관관계를 위한 필드
- 단일 값 연관 필드 : @ManyToOne(다대일), @OneToOne(일대일), 대상이 엔티티(m.team)
- 컬렉션 값 연관 필드 : @OneToMany(일대다), @ManyToMany(다대다), 대상이 컬렉션(m.orders)
- 상태 필드: 경로 탐색의 끝 부분, 탐색 불가능
- 단일 값 연관 경로 : 묵시적 내부 조인(inner join) 발생, 탐색 가능
- 컬렉션 값 연관 경로 : 묵시적 내부 조인 발생, 탐색 불가능
- 컬렉션 값 연관 경로는 FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색이 가능하다.
항상 내부 조인을 통해 일어난다.
컬렉션은 경로 탐색의 끝이므로 명시적 조인을 통해 별칭을 얻어서 경로를 탐색해야한다.
경로 탐색은 주로 select, where 절에서 사용하지만, 묵시적 조인으로 인해 sql의 from(join)절에 영향을 준다.
가급적 묵시적 조인 대신 명시적 조인을 사용하자 실무에서는 수 많은 쿼리로 묵시적 조인이면 조인이 일어나는 상황을 파악하기가 힘들다.
- SQL 조인 종류가 아니다
- JPQL에서 성능 최적화를 위해 제공하는 기능이다.
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능(즉시 로딩과 비슷)
회원 1,2는 팀A에 소속되어있고, 회원 3은 팀B에 소속되어있다.
Team teamA = new Team();
teamA.setName("teamA");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("member1");
member1.changeTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.changeTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("member3");
member3.changeTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
String query = "select m from Member m";
List<Member> resultList = em.createQuery(query, Member.class)
.getResultList();
for (Member m : resultList) {
System.out.println("Member = " + m.getUsername() + " , Team = "
+ m.getTeam().getName());
}
위 코드는 팀 엔티티는 프록시로 들어오게 되고(지연로딩),
회원1 팀 조회(SQL)
회원2 팀 조회(1차캐시)
회원3 팀 조회(SQL)
결과상 쿼리가 3번이 나가게 된다.
이로 인해 N + 1의 문제 상황이 발생한다.
(1은 첫 번째 날린 쿼리 첫 번째 날린 결과 쿼리를 N)
Team teamA = new Team();
teamA.setName("teamA");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("member1");
member1.changeTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.changeTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("member3");
member3.changeTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
String query = "select m from Member m join fetch m.team";
List<Member> resultList = em.createQuery(query, Member.class)
.getResultList();
for (Member m : resultList) {
System.out.println("Member = " + m.getUsername() + " , Team = "
+ m.getTeam().getName());
}
tx.commit();
fetch join을 이용하게 되면 위에서 팀 엔티티가 프록시로 들어오는 것이 아닌, 실제 엔티티로 조회가 된다.
데이터가 기존에 디비에서 가져와서 1차캐시에 저장되어있기 때문에, 글로벌 전략이 지연로딩이어도 쿼리를 한 번에 보내서 데이터를 다 끌어오게 된다.
참고로 fetch join은 글로벌 지연로딩 전략보다 우선한다.
String query = "select t from Team t join fetch t.members";
List<Team> resultList = em.createQuery(query, Team.class)
.getResultList();
for (Team t : resultList) {
System.out.println("Team = " + t.getName() + " , membersSize = "
+ t.getMembers().size());
for (Member m : t.getMembers()) {
System.out.println("-> Member " + m.getUsername());
}
}
tx.commit();
DB에서는 1:N 관계에서는 데이터가 늘어나게 된다.
JPA는 DB에서 주는 데이터를 가져다 쓸 수 밖에 없으므로, 팀A의 회원이 2명이 있다는 것을 중복으로 보여주게 된다.
String query = "select distinct t from Team t join fetch t.members";
원래 DB에서의 distinct는 모든 값이 다 중복이 되어야지 distinct로 중복 데이터를 없애주는데,
JPA에서는 distinct가 애플리케이션에서 중복되는 엔티티를 제거해주는 역할을 한다.
참고 : 하이버네이트6 부터는 distinct 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용된다.
일반 조인 실행시 연관된 엔티티를 함께 조회하지 않는다(지연 로딩)
단지 select 절에 지정한 엔티티만 조회한다.
그러나 페치 조인을 사용할 때는 연관된 엔티티도 함께 조회한다(즉시 로딩)
페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념이기 때문이다.
- 페치 조인 대상에는 별칭을 줄 수 없다
- 둘 이상의 컬렉션은 페치 조인 할 수 없다.
- 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
- @OneToOne, @ManyToOne 같은 단일 값 연관 필드들은 패치 조인해도 페이징 가능
- 하이버네이트는 경고 로그를 남기고 메모리에서 페이징한다.
- 연관된 엔티티들을 SQL 한 번으로 조회 -> 성능 최적화
- 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선한다
- 최적화가 필요한 곳은 페치 조인 적용을 하자
JPA에서 객체 그래프를 탐색한다는 것은 모든 데이터를 조회되게 설계를 했는데, 그런데 만약에 데이터를 몇개씩만 들고오면 혹시나 cascade나 orphanRemoval같은 것이 적용되어있으면 데이터 변형이 일어날 수 있다.
조회 대상을 특정 자식으로 한정하는 것
ex) Item 중에 Book, Movie를 조회한다
[JPQL]
select i from Item i where type(i) IN (Book, Movie)
[SQL]
select i from i where i.DTYPE in ('Book', 'Movie')
ex) 부모인 Item과 자식 Book이 있다.
Book에서 저자가 김씨인 사람인 book item을 가져와라
단일 테이블일 경우
[JPQL]
select i from Item i where treat(i as Book).author = 'kim'
[SQL]
select i.* from Item i where i.DTYPE = 'Book' and i.author = 'kim'
JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.
[JPQL]
select count(m.id) from Member m
select count(m) from Member m
==
[SQL]
select count(m.id) as mid from Member m
em.createQuery("select m from Member m where m = :member").setParamter("member", member).getResultList(); // 엔티티로 전달
em.createQuery("select m from Member m where m.d = :memberId").setParameter("memberId" , memberId).getResultList(); // 식별자로 전달
[SQL]
select m.* from Member m where m.id = ?
em.createQuery("select m from Member m where m.team = :team").setParameter("team", team).getResultList();
em.createQuery("select m fro Member m where m.team.id = :teamId").setParameter("teamId" , teamId).getResultList();
[SQL]
select m.* from Member m where m.team_id = ?
미리 쿼리를 정의해서 이름을 부여해두고 사용하는 JPQL이다. 정적 쿼리에 적합하고, 어노테이션이나 XML에 정의를 한다.
애플리케이션 로딩 시점에 초기화 후 재사용을 하고, 로딩 시점에 쿼리를 검증한다.
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username")
public class Member {
.....
}
XML이 항상 우선권을 가지고, 운영 환경에 따라 다른 XML을 배포할 수 있다.
JPA는 변경 감지 기능으로 엔티티의 변화를 감지한다.
근데 만약 100개의 데이터가 변경되었다면, 이를 업데이트 하기 위해서 100번의 업데이트 sql을 실행하게된다.
그러나 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리로 100번의 업데이트 sql을 피할 수 있다.
- 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
- excuteUpdate()의 결과는 영향받은 엔티티의 수를 반환한다.
- UPDATE, DELETE 지원
기존 회원의 나이를 모두 20살로 바꾸자
Team teamA = new Team();
teamA.setName("teamA");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("member1");
member1.changeTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.changeTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("member3");
member3.changeTeam(teamB);
em.persist(member3);
// 일대다
int i = em.createQuery("update Member m set m.age = '20'")
.executeUpdate();
System.out.println("i = " + i);
tx.commit();
벌크 연산으로 기존의 회원이 모두 20살로 변경되었고, 데이터베이스에는 20살로 적용되어있지만, 영속성 컨텍스트에는 적용이 되어있지 않은 상태이다.
flush가 일어나는 조건은 JPQL 쿼리가 수행될 때, 강제로 flush를 했을 때, commit 시점에 flush가 발생하게 된다.
그리고 flush가 발생한다고 해서 영속성 컨텍스트를 비워주지는 않는다.
그로 인해 em.find(Member.class, member1.getId()), em.find(Member.class, member2.getId()), em.find(Member.class, member3.getId())...을 통해 엔티티를 찾고 age를 찍어봐도 20살이 안나오고 기본값인 0이 나올 것이다.
벌크 연산 코드
.
.
.
em.clear();
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember.getAge() = " + findMember.getAge());
그래서 벌크 연산 이후 영속성 컨텍스트를 비워주고 member1의 아이디로 나이를 찍어봤을 때는 데이터베이스에서 member1의 엔티티를 가져와 조회하기에 나이가 20살이 출력이된다.