JPQL은 테이블을 대상으로 쿼리하는것이 아니라 엔티티 객체를 대상으로 쿼리한다. JPQL은 결국 SQL로 변환되서 실행된다.
select m from Member as m where m.age > 18
TypedQuery<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()
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
em.persist(member);
TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
List<Member> resultList1 = query1.getResultList();
Member member1 = resultList1.get(0);
String username1 = member1.getUsername();
int age1 = member1.getAge();
Member member2 = query1.getSingleResult();
String username2 = member2.getUsername();
int age2 = member2.getAge();
System.out.println("================================");
Query query2= em.createQuery("select m from Member m");
List resultList2 = query2.getResultList();
Object o2 = resultList2.get(0);
Member member3 = (Member) o2; // Member 타입으로 변환해서 사용 가능
String username3 = member3.getUsername();
int age3 = member3.getAge();
Object result = query2.getSingleResult();
Member member4 = (Member) result; // Member 타입으로 변환해서 사용 가능
String username4 = member4.getUsername();
int age4 = member4.getAge();
List<Member> resultListEx = query2.getResultList(); // TypedQuery 처럼도 할 수 있다.
Member memberEx = resultListEx.get(0);
String usernameEx = memberEx.getUsername();
int ageEx = memberEx.getAge();
반환 타입이 명확하지 않아서 Query를 사용한다면, Object 타입으로 값을 반환하므로 적절히 타입 변환해서 사용해야한다.
반환 타입이 명확하지 않아서 Query를 사용했지만 TypedQuery처럼 getResultList()를 사용할 때 List의 제네릭으로 타입을 지정할 수 도 있다.
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class);
query.setParameter("username", "memberName");
List<Member> result1 = em.createQuery("select m from Member m", Member.class).getResultList(); // 엔티티 프로젝션
List<Team> result2 = em.createQuery("select m.team from Member m", Team.class).getResultList(); // 엔티티 프로젝션
List<Address> result3 = em.createQuery("select o.address from Order o", Address.class).getResultList(); // 임베디드 타입 프로젝션
List<String> result4 = em.createQuery("select m.username from Member m", String.class).getResultList(); // 스칼라 타입 프로젝션
이 중에서 String, int와 같이 여러값을 조회 하는 스칼라 타입에 대해 좀 더 알아보자.
em.createQuery("select m.username, m.age from Member m").getResultList();
이때는 createQuery할떄 타입을 지정하지않는다. 쉽게 생각하면, String과 age중에 어떤 타입으로 지정해야할지 모르기 때문이다.
이때 값을 조회하는 방법에는 다음 3가지 방법이 있다.
Query 타입으로 조회
Object[] 타입으로 조회
Query query3 = em.createQuery("select m.username, m.age from Member m"); // Query 타입으로 조회
List resultList3 = query3.getResultList();
Object o3 = resultList3.get(0);
Object[] resultArray3 = (Object[]) o3;
String username5 = (String) resultArray3[0];
Integer age5 = (Integer) resultArray3[1];
Object o4 = query3.getSingleResult();
Object[] resultArray4 = (Object[]) o4;
String username6 = (String) resultArray4[0];
Integer age6 = (Integer) resultArray4[1];
List<Object[]> resultList = query3.getResultList(); // TypedQuery 처럼도 할 수 있다. Object[] 타입으로 조회
Object[] resultArray5 = resultList.get(0);
String username7 = (String) resultArray5[0];
Integer age7 = (Integer) resultArray5[1];
new 명령어로 조회
public class MemberDto {
private String username;
private int age;
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
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.getUsername());
System.out.println(memberDto.getAge());
setFirstResult(int startPosition) // 조회 시작 위치
setMaxResults(int maxResult) // 조회할 데이터 수
==> 몇번째부터 몇개를 가져올지 지정
==> 조회된 데이터 기준으로 몇번째부터 몇개를 가져올지 지정
==> setFirstResult(0) setMaxResult(2)는 조회된 데이터 기준으로 맨 첫번째부터 두개를 가져온다는것이다.
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();
이렇게 실행하게되면, persistence.xml에서 지정한 hibernate.dialect 즉 db방언에 맞게 페이징쿼리 JPQL이 SQL로 변환되어 실행된다.
ex)
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 ?, ?
Team team = new Team();
team.setName("A");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
member.setType(MemberType.USER);
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
String jpql = "select m from Member m left join m.team t on t.name = 'A'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
아래와 같은 SQL로 변환된다.
select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as team_id5_0_,
member0_.type as type3_0_,
member0_.username as username4_0_
from
Member member0_
left outer join
Team team1_
on
member0_.TEAM_ID=team1_.id
and (
team1_.name='A'
)
Team team = new Team();
team.setName("A");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
member.setType(MemberType.USER);
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
String jpql = "select m from Member m left join Team t on m.username = t.name;
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
아래와 같은 SQL로 변환된다.
select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as team_id5_0_,
member0_.type as type3_0_,
member0_.username as username4_0_
from
Member member0_
left outer join
Team team1_
on (
member0_.username=team1_.name
)
SQL과 동일하게 JPQL에서도 서브쿼리를 사용할 수 있다.
ex1) select m from Member m where m.age > (select avg(m2.age) from Member m2)
ex2)
select m from Member m where (select count(o) from Order o where m = o.member) > 0
exists
select m from Member m where exists (select t from m.team t where t.name = '팀A')
=> m과 연관된 t의 name이 팀A인 데이터가 있으면 해당 데이터(m)(=그걸 만족하는 m)(=t의 name이 팀A인 데이터)을 조회
ALL
select o from Order o where o.orderAmount > ALL(select p.stockAmount from Product p)
=> 서브쿼리 결과의 모든값보다 orderAmount의 값이 큰 데이터가 있으면 해당 데이터(=그걸 만족하는 o)(=서브쿼리 결과 각각의 모든값보다 orderAmount의 값이 큰 데이터)를 조회
ANY
select m from Member m where m.team = ANY (select t from Team t)
=> 서브쿼리 결과에 m과 연관된 team이 하나라도 있으면 해당 데이터(m)(=그걸 만족하는 m)을 조회 => IN과 의미가 같다.
public enum MemberType { // enum(클래스가 아님)
ADMIN, USER // enum상수
}
MemberTYpe d = MemberType.ADMIN; // 이렇게 사용
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
private MemberType type; // enum 타입
}
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
member.setType(MemberType.USER);
em.persist(member);
em.flush();
em.clear();
String query = "select m.username, 'HELLO', true from Member m where m.type = jpql.MemberType.USER";
List<Object[]> result = em.createQuery(query).getResultList();
참고로 위 코드를 실행하면,
select
member0_.username as col_0_0_,
'HELLO' as col_1_0_,
true as col_2_0_
from
Member member0_
where
member0_.type=1
이와 같은 sql로 변환되어 실행되는데, member0_.type=1에서 1로 되어있다.
이는 자바에서 enum타입의 값을 저장할때, USER가 아니라 순서를 저장하기 때문이다. 그래서 db에 보면 1이라고 저장되어있다.
이게 아니라 USER 자체를 저장하기 위해서는,
@Enumerated(EnumType.STRING)
private MemberType type;
이렇게 해주어야한다.
그럼 sql도
select
member0_.username as col_0_0_,
'HELLO' as col_1_0_,
true as col_2_0_
from
Member member0_
where
member0_.type='USER'
이와 같이 실행되고, db에도 USER라고 저장이 된다.
JPQL을 작성할 때, select t.members.username from Team t 형태로 하면 안된다.
대신에 select m.username from Team join t.members m 형태로 해야한다.
select m.username from Team join t.members m를 실행하면 SELECT m.username FROM team t JOIN member m ON t.id = m.team_id 쿼리로 변환되서 실행된다.
여기서,
t.members가 member로 변환되는 이유는 컬렉션 필드 members의 제네릭 타입이 Member이기 때문이다. t.members는 Team 객체가 갖는 List 컬렉션이다. 이 컬렉션은 결국 Member 엔티티에 해당하는 테이블, 즉 member 테이블의 여러 행들을 의미한다. 따라서 JPA는 join t.members를 Member 엔티티 대상으로 join하는것으로 해석하고, SQL에서는 member 테이블로 변환된다.
ON t.id = m.team_id가 추가되는 이유는 Team의 members와 Member의 team이 연관관계인 상태이기 때문이다.
from 뒤에는 엔티티객체만 적을수 있는데, join 뒤에는 엔티티객체뿐만 아니라 엔티티객체의 연관필드도 적을수 있다. 하지만 이때는 반드시 경로표현식 형태인 .을 통해 나타내야한다.
COALESCE : 하나씩 조회해서 null이 아니면 반환
SELECT COALESCE(m.username, '이름 없는 회원') FROM Member m;
=> m.username이 NULL이 아니면 → m.username 반환
m.username이 NULL이면 → '이름 없는 회원' 반환
SELECT COALESCE(m.username, m.nickname, '이름 없는 회원') FROM Member m;
=> m.username이 NULL이 아니면 → m.username 반환
m.username이 NULL이고, m.nickname이 NULL이 아니면 → m.nickname 반환
m.username, m.nickname 모두 NULL이면 → '이름 없는 회원' 반환
NULLIF : 두 값이 같으면 null 반환, 다르면 첫번째값 반환
SELECT NULLIF(m.username, '관리자') from Member m
연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회하는 기능이다.
public class Member {
...
@ManyToOne(fetch = FetchType.EAGER)
//@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private Team team;
... getter and setter
}
em.find
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setName("hello1");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember = " + findMember.getTeam().getName());
1. 즉시로딩
하나의 쿼리로 연관된 데이터도 조회한다. Member와 Team의 데이터를 한번에 조회한다. 연관관계의 데이터도 함께 조회한다.
2. 지연로딩
Member의 데이터만 조회한다. 연관관계의 데이터는 조회하지않는다. 이후에 findMember.getTeam().getName();을 호출하면 추가적인 쿼리가 발생하고, 연관된 데이터가 조회된다.(N+1)
jpql
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setName("hello1");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
String jpql = "select m from Member m";
//String jpql = "select m from member m join fetch m.team";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
for (Member member1 : resultList) {
System.out.println("member1 = " + member1);
//member1.getTeam().getName();
}
1. 즉시로딩
2. 지연로딩
3. fetch join(연관된 데이터를 SQL한번에 함께 조회하는 기능)
즉, em.find()를 사용하면 지연로딩일때 N+1문제가 발생하고, jpql을 사용하면 즉시로딩 및 지연로딩일때 N+1문제가 발생한다.
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().name());
}
// 페치조인으로 회원과 팀을 함께 조회해서 지연로딩X
연관된 엔티티도 함께 조회할 수 있다.
[JPQL]
select m from Member m join fetch m.team
[SQL]
select M.*, T.* MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID
참고로, 영속성 컨텍스트의 1차캐시에는 3개의 member객체와 2개의 team객체가 들어가 있으며,
JPQL 결과 리스트에는 3개의 Member객체가 들어가 있다.
jpql을 실행해서 sql쿼리가 실행되고 난 결과 row 수 만큼 데이터가 리턴된다.

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());
for(Member member : team.getMembers()) {
System.out.println("username = " + member.getUsername());
// 페치조인으로 회원과 팀을 함께 조회해서 지연로딩X
}
}
1대다로 연관된 컬렉션을 함께 조회할 수 있다.
[JPQL]
select t from Team 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'
컬렉션 페치 조인을 사용할 때에는 중복된 데이터가 결과 리스트에 포함될 수 있다.
👆 참고로, 관계형 데이터베이스에서, orders 테이블의 결과는 하나이고 order_item 테이블의 결과는 2개일때, order_id로 조인을 하게되면, order_item 테이블의 결과의 개수만큼 결과가 나온다.
select * from A JOIN B ON A.id = B.id; 에서 JOIN 조건에 따라 A의 한 행과 B의 N행이 매칭되면 결과는 N행이 된다. JOIN 조건에 따라 A의 N행과 B의 한 행이 매칭되면 결과는 N행이 된다. => 데이터가 뻥튀기 된다.
참고로 이 내용은, JOIN이든 LEFT JOIN이든 상관없이 적용되고, A와 B가 일대다 관계일때 A가 일이든 B가 다이든, A가 다이든 B가 일이든 상관없이 적용된다.
이때 distinct를 사용하면,
3-1. sql에 distinct를 추가해서 db에 쿼리를 날린다.
3-2. 그 결과가 애플리케이션에 넘어올때, 동일 식별자의 중복 데이터가 있으면 중복을 제거한다.
참고로, distinct를 사용하든 사용하지않든 영속성 컨텍스트의 1차캐시에는 하나의 team객체와 두개의 member객체가 들어가 있으며,
distinct를 사용하면, JPQL 결과 리스트에 들어 있는 하나의 Team 객체는 1차 캐시의 Team 객체를 참조하고 있는것이고,
distinct를 사용하지 않으면 JPQL 결과 리스트에 들어 있는 Team 객체들은 모두 동일한 1차 캐시의 Team 객체를 참조하고 있는것이다.

페치조인 대상에는 별칭을 줄수없다
select t from Team t join fetch t.members where m.username = "test" 형태로 쓸 수 없다. 왜냐하면 JPA의 페치조인의 개념이 t.members에 대한 데이터를 전부 가져오겠다는것인데, 별칭을 주고 일부 데이터를 가져오거나 할 수 없다. 페치 조인의 목적은 연관된 엔티티를 한 번에 모두 로딩하기 위한 것이다. Team을 기준으로 members를 모두 함께 가져와야 페치조인의 의미가 있다.둘 이상의 컬렉션을 페치조인 할 수없다.
컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
jpql을 사용할때 페이징api를 사용하면 jpql이 sql로 변환되서 실행될때 해당 sql에 페이징쿼리가 추가된다. 컬렉션을 페치 조인한 경우에는 데이터가 뻥튀기되기 때문에, 페이징을 사용하더라도 뻥튀기된 결과에 대해 페이징이 적용된다. 이로 인해 의도한 순서나 범위에 맞지 않는 결과가 반환될 수 있다.
distinct를 사용해봐도 안된다. jpql을 사용할때 페이징api를 사용하면 jpql이 sql로 변환되서 실행될때 해당 sql에 페이징쿼리가 추가된다. 이때 distinct는 sql에 DISTINCT를 추가해서 쿼리를 날리고나서, 그 결과가 애플리케이션에 넘어올때 중복된 데이터가 있으면 중복을 추가적으로 제거하는것인데, 예를들어 setMaxResult(2)를 해서 2개의 데이터를 가져오려는 상황속에서 sql에 DISTINCT를 추가해서 쿼리를 날렸는데 distinct가 되지않고, 이때는 결과가 2개이다. setMaxResult(2)이기 때문에 이 결과가 애플리케이션에 넘어오는데 이때 중복된 데이터가 있어서 제거해버리면 결국 1개의 데이터만 넘어오게된다. sql에서는 페이징쿼리에 의해 2개의 데이터가 넘어갔는데 애플리케이션에 넘어올때 중복된 데이터가 있어서 제거해버려서 결과적으로 1개의 데이터만 넘어오기때문에 문제가 발생한다. 2개의 데이터를 가져오려는 페이징인데 1개의 데이터만 넘어오게되서 문제가 발생한다. 즉 SQL쿼리상으로는 정상적으로 데이터를 2개 보냈는데 애플리케이션에서 중복된 데이터이기때문에 중복 데이터를 제거해서 1개만 나오기 때문에 문제가 있다.
예를들어, Team이 총 10개 있고 각 Team마다 Member가 3명씩 있다고 했을때 sql로 변환되서 실행되면
SELECT DISTINCT ...
FROM team t
JOIN member m ON t.id = m.team_id
LIMIT 2
이런식으로 실행된다.
하지만 의도한바는 Team 2개를 가져오고 싶어서 페이징했는데, 중복된 row로 인해 결국 Team 1개만 반환되는 결과가 나온다. SQL 쿼리 상으론 분명 2개를 보냈는데 애플리케이션에서는 중복 제거로 인해 1개만 남게 되는 것이다. 2개의 데이터를 가져오기 위해 페이징을 했고, SQL 쿼리 상으로는 분명 2개의 결과가 반환되었지만, 애플리케이션에서 중복 제거 과정으로 인해 1개만 남게 되는 문제가 발생한다. 즉, 오류 없이 정상 실행되지만, 원하는 결과가 아니기 때문에 문제가 된다.
컬렉션을 페치 조인하면 페이징 API를 사용할수 없기 때문에 아래와 같은 2가지 방법을 사용하면 된다.
select m from Member m join fetch m.team으로 한다. Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("member3");
member3.setTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
String jpql = "select t from Team t";
List<Team> result = em.createQuery(jpql, Team.class)
.setFirstResult(0)
.setMaxResults(2)
.getResultList();
System.out.println("result = " + result.size());
for (Team team : result) {
System.out.println("team = " + team.getName() + "|members" + team.getMembers());
for (Member member : team.getMembers()) {
System.out.println("-> member = " + member);
}
}
위 코드에서, 단순 지연로딩일때는
Hibernate:
/* select
t
from
Team t */ select
team0_.id as id1_3_,
team0_.name as name2_3_
from
Team team0_ limit ?
result = 2
Hibernate:
select
members0_.TEAM_ID as team_id5_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.age as age2_0_1_,
members0_.TEAM_ID as team_id5_0_1_,
members0_.type as type3_0_1_,
members0_.username as username4_0_1_
from
Member members0_
where
members0_.TEAM_ID=?
team = 팀A|members[Member{id=3, username='member1', age=0, type=null}, Member{id=4, username='member2', age=0, type=null}]
-> member = Member{id=3, username='member1', age=0, type=null}
-> member = Member{id=4, username='member2', age=0, type=null}
Hibernate:
select
members0_.TEAM_ID as team_id5_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.age as age2_0_1_,
members0_.TEAM_ID as team_id5_0_1_,
members0_.type as type3_0_1_,
members0_.username as username4_0_1_
from
Member members0_
where
members0_.TEAM_ID=?
team = 팀B|members[Member{id=5, username='member3', age=0, type=null}]
-> member = Member{id=5, username='member3', age=0, type=null}
이런식으로 2번의 추가 쿼리가 발생한다.
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@BatchSize(size = 10)
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
하지만 지연로딩일때 위와 같이 @BatchSize를 설정하게되면,
Hibernate:
/* load one-to-many jpql.Team.members */ select
members0_.TEAM_ID as team_id5_0_1_,
members0_.id as id1_0_1_,
members0_.id as id1_0_0_,
members0_.age as age2_0_0_,
members0_.TEAM_ID as team_id5_0_0_,
members0_.type as type3_0_0_,
members0_.username as username4_0_0_
from
Member members0_
where
members0_.TEAM_ID in (
?, ?
)
team = 팀A|members[Member{id=3, username='member1', age=0, type=null}, Member{id=4, username='member2', age=0, type=null}]
-> member = Member{id=3, username='member1', age=0, type=null}
-> member = Member{id=4, username='member2', age=0, type=null}
team = 팀B|members[Member{id=5, username='member3', age=0, type=null}]
-> member = Member{id=5, username='member3', age=0, type=null}
이런식으로 1번의 추가 쿼리가 발생한다. @BatchSize 설정에 의해 최적화된 지연 로딩 방식이다.
하나의 쿼리로 팀A와 연관된 멤버뿐만 아니라 팀B와 연관된 멤버도 함께 가져온다.
team.getMembers()를 호출하기 전에 이미 영속성 컨텍스트에는 여러 팀(팀A, 팀B)이 존재한다.
그래서 처음 team.getMembers()를 호출하게되면 팀A와 연관된 멤버만 조회하는 쿼리가 발생할것 같지만, 내부적으로 영속성 컨텍스트에 있는 모든 각각의 팀들과 연관된 멤버를 한번에 가져오는 쿼리(=PK 기반의 IN 조건을 사용하는 쿼리)가 발생한다.
즉 첫번째 팀의 getMembers()를 호출하는 시점에 모든 각각의 팀들과 연관된 데이터를 한번에 가져온다.
참고로 size는 최대 10개의 팀에 대해, 연관된 모든 멤버를 가져오라는 의미이다. 영속성 컨텍스트에 팀이 20개가 있으면 2번의 쿼리로 나누어 실행된다.
JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.
[JPQL]
select count(m) from Member m // 엔티티를 직접 사용
select count(m.id) from Member m // 엔티티의 아이디를 사용
[SQL]
select count(m.id) as cnt from Member m // 둘다 이렇게 SQL이 실행된다.
엔티티를 파라미터로 전달하는 경우
2-1
String jpql = "select m from Member m where m = :member";
List resultList = em.createQuery(jpql).setParameter("member", member).getResultList();
String jpql = "select m from Member m where m.id = :memberId";
List resultList = em.createQuery(jpql).setParameter("memberId", memberId).getResultList();
[SQL]
select m.* from Member m where m.id=? // 둘다 이렇게 SQL이 실행된다.
2-2
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();
String jpql = "select m from Member m where m.team.id = :teamId";
List resultList = em.createQuery(jpql).setParameter("teamId", teamId).getResultList();
[SQL]
select m.* from Member m where m.team_id=? // 둘다 이렇게 SQL이 실행된다.
참고로, select m from Member m 에서 select m 부분의 m은 엔티티 자체를 의미한다. 그래서 SQL로 변환될때에는 해당 엔티티를 구성하는 모든 컬럼을 SELECT에 나열한다. 왜냐하면 JPA가 이 결과를 바탕으로 Member 객체에 매핑해야 하기 때문이다.
[JPQL]
select m from Member m
[SQL]
select id, username, team_id from member
@Entity
@NamedQuery(name = "Member.findByUsername", query="select m from Member m where m.username = :username")
public class Member {
...
}
List<Member> result = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
미리 정의해서 이름을 부여해두고 사용하는 JPQL이다.
애플리케이션 로딩 시점(=애플리케이션 시작 시점=서버가 띄워질때)에 한번 초기화하고나서, 다른곳에서 사용할때는 초기화했던 named쿼리를 재사용하는것이다.
애플리케이션 로딩 시점에 쿼리를 검증하기때문에 문법에러 같은 것을 검증한다.
쉽게 말해 SQL의 UPDATE나 DELETE 라고 생각하면 된다.
예를들어, 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려고 할때,
JPA의 변경 감지(dirty checking) 기능으로 실행하려면 너무 많은 SQL을 실행해야한다.
1-1. 재고가 10개 미만인 상품을 리스트로 조회한다.
1-2. 루프를 돌리면서 상품의 가격을 10% 증가한다.
1-3. 그러면 트랜잭션 커밋 시점에 변경감지가 동작한다.
==> 변경된 데이터가 100건이라면 100번의 UPDATE SQL이 실행된다.
벌크 연산을 사용하면 쿼리 한 번으로 가능하다.
String jpql = "update Member m set m.age = 20";
int resultCount = em.createQuery(jpql).executeUpdate();
<벌크연산 주의사항>
Member member1 = new Member();
member1.setUsername("member1");
member1.setAge(0);
em.persist(member1); // 영속성 컨텍스트에 저장됨
em.createQuery("update Member m set m.age = 20").executeUpdate(); // DB만 갱신
System.out.println(member1.getAge()); // 🔴 여전히 0
벌크 연산을 먼저 실행
em.find() 등 영속성 컨텍스트에 작업을 먼저 하지말고 벌크연산을 먼저 실행한다.
이렇게 하면 영속성 컨텍스트에 아무것도 없으니까 문제가 없다.
벌크 연산 수행 후 영속성 컨텍스트 초기화
영속성 컨텍스트에 값이 이미 있는 상태에서 벌크 연산을 수행하면, 수행 후 영속성 컨텍스트를 초기화 해주어야한다.
Member member1 = new Member();
member1.setUsername("member1");
member1.setAge(0);
em.persist(member1); // 영속성 컨텍스트에 저장됨
em.createQuery("update Member m set m.age = 20").executeUpdate(); // DB만 갱신
em.clear();
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getAge()); // 🔴 20