- JPQL : Java Persistence Query Language → Java 표준
- QuertDSL : JPQL을 좀 더 쉽게 쓸 수 있게 해주는 구현체, 빌더 클래스 모음
✅ JPQL(Java Persistence Query Language)
엔티티 객체를 조회하는 객체지향 쿼리
//쿼리 생성
String jpql = "SELECT m FROM Member as m WHERE m.name = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
- 실행한 JPQL
SELECT m FROM Member as m WHERE m.name = 'kim'
- 실행된 SQL
SELECT member.id as id, member.age as age, member.team_id as team, member.name as name FROM Member member WHERE member.name='kim'
✅ JPQL 기본 문법
SELECT
,UPDATE
,DELETE
문 사용할 수 있다
INSERT
문 x→EntityManager.persist()
사용하여 엔티티 저장한다
🔹JPQL 문법
SELECT :: =
SELECT_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
//벌크 연산
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
✅ SELECT
SELECT m FROM Member AS m WHERE m.name = 'kim'
🔻대소문자 구분
- 엔티티와 속성 → 대소문자 구분 o
ex1)
Member
≠member
ex2)
username
≠Username
- JPQL 키워드 → 대소문자 구분 x
ex1)
SELECT
=select
ex2)
FROM
=from
ex3)
AS
=as
🔻엔티티 이름
Member
→ 클래스 명 x, 엔티티 명 o
- 엔티티 명 지정 :
@Entity(name="XXX")
- 기본값 : 클래스 명 → 권장
🔻 별칭 필수
- JPQL은 별칭 사용 필수
SELECT name FROM Member m //잘못된 문법 SELECT m.name FROM Member m //바른 문법
AS
: 생략 가능→Member As m
=Member m
✅ TypeQuery, Query
- 작성한 JPQL을 실행하기 위한 쿼리 객체
- 차이점 : 언제 사용하나?
TypeQuery
: 반환 타입을 명확하게 지정할 때
Query
: 반환 타입을 명확하게 지정할 수 없을 때
🤖 TypeQuery
TypedQuery<Member> query
= em.createQuery("SELECT m FROM Member m", Member.class);
List<Member> resultList = query.getResultList();
for (Member member : resultList) {
System.out.println("member = " + member);
}
return 값이 Member 엔티티이다. Member.class로 반환되는 타입을 명시했다. 반환되는 타입이 명확할 때는 TypeQuery를 쓰는 것이 형변환을 줄일 수 있어서 간편하다.
🤖 Query
Query query
= em.createQuery("SELECT m.name, m.age FROM Member m");
List<Object> resultList = query.getResultList();
for (Object o : resultList) {
Object[] result = (Object[]) o; //결과가 둘 이상이면 Object[] 반환
System.out.println("name: " + result[0]);
System.out.println("age: " + result[1]);
}
Query
객체SELECT
절에서 여러 엔티티나 컬럼을 선택 시 반환 타입이 명확하지 않을 때 사용한다
- 조회 대상의 갯수에 따라 반환 타입이 달라진다
- 둘 이상 :
Object[]
SELECT m.name, m.age FROM Member m
- 하나 :
Object
"SELECT m.name FROM Member m
- 둘 이상 :
위 코드는 return 값이 명확하지 않다. String 혹은 int일 수 있다. 따라서 Query 객체를 사용하여 Object 객체들로 리턴 받고, List로 만들어 Object[]로 만들었다.
명확하게는 결과가 둘 이상이라 Object[]로 query 객체 내부에 리턴 받는다. 이것을 다시 List로 만든 것이다. List<Object[]>이 되겠다. 따라서 위 코드는 아래와 같이 변경할 수 있다.
Query query = em.createQuery("SELECT m.name, m.age FROM Member m");
List<Object[]> resultList = query.getResultList();
for (Object[] result : resultList) {
System.out.println("name = " + result[0]);
System.out.println("age = " + result[1]);
}
결과를 보면 List안에 Object[]이 있고, Object[] 배열은 Member 엔티티 객체에 속성 값들을 가지고 있게 된다.
🔹파라미터 바인딩
- 이름 기준 파라미터(Named Parameters)
- 파라미터를 이름으로 구분
- 파라미터 앞에
:
사용 ex):username
public static void useParameters(EntityManager em) { System.out.println("=============================== use Parameters ============================"); em.clear(); String memberName = "member1"; TypedQuery<Member> query = em.createQuery("SELECT m From Member m WHERE m.name = :memberName", Member.class); //== Methode 체인 방식으로도 가능 ==// // List<Member> resultList = // em.createQuery("SELECT m From Member m WHERE m.name = :memberName", Member.class) // .setParameter("memberName", memberName) // .getResultList(); query.setParameter("memberName", memberName); List<Member> resultList = query.getResultList(); for (Member member : resultList) { System.out.println("id: " + member.getId()); System.out.println("name: " + member.getName()); } }
👉 위치 기준 파라미터 방식보다 이름 기준 파라미터 바인딩 방식을 사용하는 것이 명확하기 때문에 위치 기준 파라미터 방식을 생략했다
🔻메소드 체인방식
//== Methode 체인 방식으로도 가능 ==//
List<Member> resultList =
em.createQuery("SELECT m From Member m WHERE m.name = :memberName", Member.class)
.setParameter("memberName", memberName)
.getResultList();
✅ 프로젝션(Projection)
🔻프로젝션 : SELECT 절에 조회할 대상을 지정하는 것
- 방법 :
[SELECT {프로젝션 대상} FROM]
🔹엔티티(Entity) 프로젝션
SELECT m FROM Member m //회원
SELECT m.team FROM Member m //팀
엔티티를 직접 조회하는 것과, 엔티티와 연관된 엔티티를 조회할 수도 있다.
- 원하는 객체를 바로 조회
- 조회한 엔티티는 영속성 컨텍스트에서 관리
public static void useEntityProjection(EntityManager em) {
System.out.println("=============================== use EntityProjection ============================");
em.clear();
System.out.println("=============================== result Team =============================");
TypedQuery<Team> queryTeam =
em.createQuery("SELECT m.team From Member m", Team.class);
List<Team> resultTeamList = queryTeam.getResultList();
for (Team team : resultTeamList) {
System.out.println("id: " + team.getId());
System.out.println("name: " + team.getName());
}
}
위 코드를 실행하면 Team 정보가 두 번 나타나는 것을 볼 수 있다. m.team
은 Member 엔티티와 연관된 Team 엔티티를 나타낸 것이지 Table에 Team에 대한 Id 값을 가져온다는 뜻이 아님을 이해해야 한다. 따라서 Member의 Team을 조회했다면 member1, member2가 가지고 있는 각각의 Team 객체를 조회하게 되는 것이다.
🔹임베디드(Embedded) 타입 프로젝션
String query = "SELECT a FROM Address a";
String query = "SELECT o.adress FROM Order o";
List<Address> addresses = em.createQuery(query, Address.class)
.getResultList();
- Embedded 타입은 조회의 시작점이 될 수 없다. 엔티티 타입이 아니라 값 타입이다. 따라서 첫 번째 JPQL로 조회할 수 없다.
- 엔티티 타입(x), 값 타입(o)→ 영속성 컨텍스트에서 관리되지 않는다.
public static void useEmbedded(EntityManager em) {
em.clear();
System.out.println("=============================== use Embedded ============================");
List<Address> addresses =
em.createQuery("SELECT o.address FROM Order o", Address.class)
.getResultList();
for (Address address : addresses) {
System.out.println("City: " + address.getCity());
System.out.println("Street: " + address.getStreet());
System.out.println("Zipcode: " + address.getZipcode());
}
}
//실행된 SQL
Hibernate:
select
order0_.city as col_0_0_,
order0_.street as col_0_1_,
order0_.zipcode as col_0_2_
from
ORDERS order0_
🔹스칼라(Scalar) 타입 프로젝션
🔻숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다
- ScalarType
public static void useScalarType(EntityManager em) { em.clear(); System.out.println("=============================== use Scalar ============================"); List<String> names = // 중복 제거 : SELECT DISTINCT em.createQuery("SELECT name FROM Member m", String.class).getResultList(); for (String name : names) { System.out.println("Member name: " + name); } List<Object> resultList = em.createQuery("SELECT m.id, m.name FROM Member m") .getResultList(); Iterator iterator = resultList.iterator(); while (iterator.hasNext()) { Object[] row = (Object[]) iterator.next(); System.out.print("id: " + (Long) row[0] + ", "); System.out.println("name: " + (String) row[1]); } List<Object[]> resultObjectList = em.createQuery("SELECT o.id, o.member, o.product FROM Order o") .getResultList(); for (Object[] row : resultObjectList) { System.out.println("id: " + (Long) row[0]); // 스칼라 System.out.println("Member: " + (Member) row[1]); // 엔티티 System.out.println("Product: " + (Product) row[2]); // 엔티티 } }
🤖 전체 회원 이름 조회
public static void useScalarType(EntityManager em) {
em.clear();
System.out.println("=============================== use Scalar ============================");
List<String> names =
// 중복 제거 : SELECT DISTINCT
em.createQuery("SELECT name FROM Member m", String.class).getResultList();
for (String name : names) {
System.out.println("Member name: " + name);
}
🤖 여러 프로젝션 Object[] 조회
List<Object> resultList =
em.createQuery("SELECT m.id, m.name FROM Member m")
.getResultList();
Iterator iterator = resultList.iterator();
while (iterator.hasNext()) {
Object[] row = (Object[]) iterator.next();
System.out.println("id: " + (Long) row[0]);
System.out.println("name: " + (String) row[1]);
}
🤖 여러 프로젝션 엔티티 & 스칼라 타입 조회
List<Object[]> resultObjectList =
em.createQuery("SELECT o.id, o.member, o.product FROM Order o")
.getResultList();
for (Object[] row : resultObjectList) {
System.out.println("id: " + (Long) row[0]); // 스칼라
System.out.println("Member: " + (Member) row[1]); // 엔티티
System.out.println("Product: " + (Product) row[2]); // 엔티티
}
✅ New 명령어로 객체변환
- UserDto
public class UserDto { private String name; private int age; public UserDto(String name, int age) { this.name = name; this.age = age; } //.. getter, setter ..//
🤖사용 전 후
public static void useUserDto(EntityManager em) {
em.clear();
System.out.println("=============================== use new UserDto ============================");
//== new 사용 전 ==//
List<Object[]> resultList =
em.createQuery("SELECT m.name, m.age FROM Member m")
.getResultList();
//객체 변환 작업
List<UserDto> userDTOs = new ArrayList<>();
for (Object[] row : resultList) {
UserDto userDto = new UserDto((String)row[0], (Integer)row[1]);
userDTOs.add(userDto);
}
//== new 사용 후 ==//
TypedQuery<UserDto> query =
em.createQuery("SELECT new jpabook.model.entity.UserDto(m.name, m.age) FROM Member m", UserDto.class);
List<UserDto> userDtoList = query.getResultList();
for (UserDto userDto : userDtoList) {
System.out.println("UserDto name: " + userDto.getName());
System.out.println("UserDto age: " + userDto.getAge());
}
}
SELECT
다음NEW
명령어 사용하여 반환받을 클래스 지정→ 클래스의 생성자에 JPQL 조회 결과 넘겨줄 수 있다
TypeQuery
사용 가능 → 지루한 객체 변환 작업 감소
❗주의 사항
- 패키지 명을 포함한 전체 클래스명 입력
- 순서와 타입이 일치하는 생성자 필요
✅ Paging API
setFirstResult(int startPostion)
: 조회 시작 위치(0부터 시작)
setMaxResults(int maxResult)
: 조회할 데이터 수
public class MainPagingTest {
public static void main(String[] args) {
//엔티티 매니저 팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
EntityManager em = emf.createEntityManager(); //엔티티 매니저 생성
EntityTransaction tx = em.getTransaction(); //트랜잭션 기능 획득
try {
tx.begin(); //트랜잭션 시작
//== with use Paging API ==//
testPagingApi(em);
tx.commit();//트랜잭션 커밋
em.clear();
//== useP Paging API ==//
usePaging(em);
} catch (Exception e) {
e.printStackTrace();
tx.rollback(); //트랜잭션 롤백
} finally {
em.close(); //엔티티 매니저 종료
}
emf.close(); //엔티티 매니저 팩토리 종료
}
public static void testPagingApi(EntityManager em) {
System.out.println("============================== save testPagingApi ==========================");
Team team = new Team();
team.setName("team");
em.persist(team);
Product productA = new Product();
productA.setName("productA");
productA.setPrice(2000);
em.persist(productA);
Address address1 = new Address();
address1.setCity("Daegu");
address1.setStreet("Sangin");
address1.setZipcode("41281");
//em.persist(address1); // Entity가 아니므로 Embedded를 사용하는 쪽에서 저장해야함.
Order order = new Order();
order.setProduct(productA);
order.setAddress(address1);
for (int i = 0; i < 100; i++) {
Member member = new Member();
member.setName("member" + i);
member.setAge(i);
member.setTeam(team);
order.setMember(member);
em.persist(member);
}
em.persist(order);
}
public static void usePaging(EntityManager em) {
TypedQuery<Member> query =
em.createQuery("SELECT m FROM Member m ORDER BY m.id DESC", Member.class);
query.setFirstResult(10);
query.setMaxResults(20);
List<Member> members = query.getResultList();
// Team, Order, Product에 ID 시퀀스가 1씩 할당되어 이름과 3의 차이가 발생했음
System.out.println("Paging Size: " + members.size());
for (Member member : members) {
System.out.print("Member id: " + member.getId() + ", ");
System.out.println("Member name: " + member.getName());
}
}
}
✅ GROUP BY, HAVING
🔹GROUP BY
- 통계 데이터를 구할 때 특정 그룹끼리 묶어준
- 문법 : groupby_절 ::= GROUP BY {단일값 경로 | 별칭}
SELCET 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
GROUP BY
와 함께 사용
GROUP BY
로 그룹화 한 통계 데이터를 기준으로 필터링
- 문법 : having_절 ::= HAVING 조건식
SELCET 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)
- 결과를 정렬할 때 사용
- 문법 : orderby_절 ::= ORDER BY {상태필드 경로 | 결과 변수 [ASC | DESC]}
- 상태필드 : 객체의 상태를 나타내는 필드
- 결과 변수 :
SELECT
절에 나타나는 값(집계함수사용0)
ASC
: 오름차순(기본값)
DESC
: 내림차순SELCET t.name, COUNT(m.age) as cnt //cnt : 결과 변수 FROM Member m LEFT JOIN m.team t GROUP BY t.name //t.name : 상태필드 ORDER BY cnt
✅ JPQL 조인
🔹내부 조인
INNER JOIN
사용 → INNER 생략 가능String teamName = "팀A"; 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();
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
) 사용FROM Member m
: 회원을 선택하고 별칭m
설정
Member m JOIN m.team t
- 회원이 가지고 있는 연관 필드로 팀과 조인
- 조인한 팀에 별칭
t
설정
❗주의할 점
JPQL은
JOIN
명령어 다음에 조인할 객체의 연관 필드 사용→SQL
조인처럼 사용하면 문법 오류 발생한다.SELECT m FROM Member m JOIN Team t //잘못된 JPQL 조인, 오류 발생
- 연관 필드(
- 조인한 두 개의 엔티티 조회
public static void innerJoin(EntityManager em) { em.clear(); System.out.println("=============================== use Inner Join ============================"); String query = "SELECT m, t FROM Member m JOIN m.team t"; List<Object[]> result = em.createQuery(query).getResultList(); for (Object[] row : result) { Member member = (Member) row[0]; Team team = (Team) row[1]; System.out.println("Member: " + member); System.out.println("Team: " + team); } }
🔹외부 조인
OUTER
생략 가능 → 보통LEFT JOIN
으로 사용SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
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=?
- 조인한 두 개의 엔티티 조회
public static void outerJoin(EntityManager em) { em.clear(); System.out.println("=============================== use Outer Join ============================"); String query = "SELECT m FROM Member m LEFT JOIN m.team t"; List<Member> result = em.createQuery(query).getResultList(); for (Member member : result) { System.out.println("Member: " + member); System.out.println("Team: " + member.getTeam()); }
Fetch.Lazy 설정이 되어있어서, 실제 Team 객체를 사용될 때 한 번 더 조회되었다.
🔹컬렉션 조인
🔻컬렉션을 사용하는 곳에 조인하는 것 ex) 일대다 관계, 다대다 관계
- [회원 → 팀]
- 다대일 조인
- 단일 값 연관 필드(m.team) 사용
- [팀 → 회원]
- 일대다 조인
- 컬렉션 값 연관 필드(m.members) 사용
- 컬렉션 조인
Team과 Member는 일대다 관계이다. 반대로 Member과 Team은 다대일 관계이다.
public static void collectionJoin(EntityManager em) { em.clear(); System.out.println("=============================== use Collection Join ============================"); String query = " SELECT t, m FROM Team t LEFT JOIN t.members m"; List<Object[]> result = em.createQuery(query).getResultList(); for (Object[] o : result) { Team team = (Team) o[0]; Member member = (Member) o[1]; System.out.print("Team: " + team + ", "); System.out.println("Member1: " + member + ", "); } }
팀과 팀이 보유한 회원목록을 컬렉션 값 연관 필드로 외부 조인했다.
🔹JOIN ON절(JPA 2.1)
- JPA 2.1부터 조인 시 지원한다
- 조인 대상 필터링 및 조인 가능하다
- 내부 조인의
ON
절은WHERE
절을 사용할 때와 결과가 같다 → 보통 외부 조인에서만 사용한다
//JPQL
SELECT m, t from Member m //모든 회원과 연관된 팀을 조회
LEFT join m.team t on t.name = 'team1' //조건 : 팀 이름이 team1인 팀만 조회
//SQL
SELECT m.*, t.* FROM Member m
LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='team1' //조인 시점에 조인 대상 필터링
✅ 페치 조인
Fetch Join의 목적은 관계 있는 Entity를 한 방에 가져오는데 목적이 있다. 즉, Fetch Join은 지연로딩을 하지 않고 즉시 로딩(EAGER)로 가져오는 쿼리이다. N명의 멤버가 모두 각기 다른 팀을 가진다고 가정하면 1명의 멤버마다 그 멤버가 소속된 팀의 쿼리가 나아갈 것이고 N + 1 문제가 발생되는 원인이 된다.
🔹엔티티 페치 조인(ManyToOne)
- JPQL에서 성능 최적화를 위해 제공하는 기능(SQL에서의 페치 조인과 다른 개념)이다.
- 연관된 엔티티나 컬렉션을 한 번에 같이 조회한다. 보통 N+1 문제를 해결할 때 쓰인다.
- 명령어 :
join fetch
- 문법 :
페치 조인 ::= [ LEFT [OUTER] or INNER ] JOIN FETCH 조인경로
👉 Fetch 조인 시에는 조인 컬럼에 별칭을 사용하지 않는다(하이버네이트는 허용한다) 그래도 사용하지 말자.
🤖 Fetch 조인으로 연관된 엔티티 객체 조회
public static void fetchJoin(EntityManager em) {
em.clear();
System.out.println("=============================== use Fetch Join ============================");
//String query = "SELECT m FROM Member m";
String fetchQuery = "SELECT m FROM Member m JOIN FETCH m.team";
List<Member> result = em.createQuery(fetchQuery).getResultList();
for (Member member : result) {
System.out.println("Member name: " + member.getName());
System.out.println("Member team: " + member.getTeam());
}
}
- 일반 쿼리
String query = "SELECT m FROM Member m"
일반적으로 Member 엔티티를 조회하면 Member와 연관관계가 있는 Team을 조회하기 위해 쿼리가 2번 더 발생되는 것을 볼 수 있다. 2 + 1의 쿼리가 나타난 것이다(N + 1).
- 페치 조인 쿼리
String fetchQuery = "SELECT m FROM Member m JOIN FETCH m.team";
Fetch 조인을 사용하면 Inner 조인으로 한 번에 연관된 객체를 조회한다. 연관된 팀을 사용해도 지연 로딩 발생하지 않았다. 즉 Eager로 조회된다.
🔹컬렉션 페치 조인(OneToMany)
🤖컬렉션 페치 조인 사용
public static void collectionFetchJoin(EntityManager em) {
em.clear();
System.out.println("=============================== use Collection Fetch Join ============================");
//String query = "SELECT t FROM Team t WHERE t.name = :name";
String fetchQuery = "SELECT t FROM Team t JOIN FETCH t.members WHERE t.name = :name";
List<Team> result = em.createQuery(fetchQuery)
.setParameter("name", "team1")
.getResultList();
for (Team team : result) {
System.out.println("Team name: " + team.getName());
System.out.println("Team members: " + team.getMembers());
}
}
팀(t
)을 조회하면서 연관된 회원 컬렉션(t.members
)도 함께 조회했다.
팀 A입장에서는 하나이지만, 멤버가 2명이어서 2 ROW가 된다. 그래서 결과가 2개가 나온다.
- JPQL에서 EAGER FETCH를 사용했기 때문에 모든 연관된 멤버가 함께 로딩된다.
- SQL 결과에는 "Team" 테이블의 각 행과 연결된 "Member" 테이블의 각 행이 나타난다.
🔻DISTINCT
- SQL에서 중복 결과 제거 + 애플리케이션에서 중복 결과 제거
- SQL에
SELECT DISTINCT
추가됨→ 각 로우의 데이터가 달라 효과 x
- 애플리케이션에서
distinct
명령어를 보고 중복 데이터 거름→ 중복된 엔티티 제거로 결과는 하나만 조회됨
String fetchQuery = "SELECT DISTINCT t FROM Team t JOIN FETCH t.members WHERE t.name = :name";
✅ 페치 조인의 특징과 한계
🔹페치 조인 특징
- 성능 최적화
- SQL 한 번으로 연관된 엔티티 함께 조회 → SQL 호출 횟수 감소
- SQL 한 번으로 연관된 엔티티 함께 조회 → SQL 호출 횟수 감소
- 글로벌 로딩 전략보다 우선된다
- 될 수 있으면 지연 로딩을 사용하고 최적화가 필요한 경우 페치 조인 적용 권장한다
- 객체 그래프를 유지할 때 효과적이다
- 연관된 엔티티를 쿼리 시점에 조회 → 지연 로딩 발생 x
👉준영속 상태에서도 객체 그래프 탐색 가능
- 연관된 엔티티를 쿼리 시점에 조회 → 지연 로딩 발생 x
🔹 페치 조인 한계
- 페치 조인 대상에는 별칭을 줄 수 없다
SELECT
절,WHERE
절, 서브 쿼리에 페치 조인 대상 사용 x
- JPA 표준에서는 지원하지 않음
- 예외) 하이버네이트를 포함한 몇몇 구현체들은 지원
- ❗안 쓰는게 좋다.
- 별칭을 잘못 사용할 경우, 데이터 무결성이 깨질 수 있음
- 특히 2차 캐시와 함께 사용 시 조심해야 함
ex) 연관된 데이터 수가 달라진 상태에서 2차 캐시에 저장 → 다른 곳에서 조회할 때도 연관된 데이터 수가 달라짐
- 둘 이상의 컬렉션을 페치할 수 없음
- 구현체에 따라 가능한 경우도 존재→ 컬렉션 * 컬렉션의 카테시안 곱이 만들어지므로 주의
- 하이버네이트 사용 시 예외 발생
"javax.persistence.PersistenceException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags"
- 컬렉션을 페치 조인하면 페이징 API(
setFirstResult
,setMaxResults
) 사용 불가- 단일 값 연관 필드(일대일, 다대일)들은 페치 조인을 사용해도 페이징 API 사용 가능
- 하이버네이트
- 경고 로그 남기면서 메모리에서 페이징 처리
- 데이터가 많으면 성능 이슈와 메모리 초과 예외가 발생할 수 있어 위험
✅ 경로 표현식
🔻 .
(점)을 찍어 객체 그래프를 탐색하는 방법
ex) m.username
, m.team
- 상태 필드(state field) : 단순히 값을 저장하기 위한 필드
- 연관 필드(association field) : 연관관계를 위한 필드
- 임베디드 타입 포함
- 종류
- 단일 값 연관 필드
@ManyToOne
,@OneToOne
- 대상 : 엔티티
- 컬렉션 값 연관 필드
@OneToMany
,@ManyToMany
- 대상 : 컬렉션
- 단일 값 연관 필드
종류 | 설명 | 탐색 |
---|---|---|
상태 필드 경로 | 경로 탐색의 끝 | X |
단일 값 연관 경로 | 묵시적 내부 조인 | O |
컬렉션 값 연관 경로 | 묵시적 내부 조인 | X (단, FROM 절에서 조인을 통해별칭을 얻으면 별칭으로 탐색 가능) |
🤖예제 코드
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "name")
private String username; //상태 필드
private Integer age; //상태 필드
@ManyToOne()
@JoinColumn(name = "TEAM_ID")
private Team team; //연관 필드 - 단일 값 연관 필드
@OneToMany(mappedBy = "member")
private List<Order> orders; //연관 필드 - 컬렉션 값 연관 필드
}
🔹상태 값 연관 필드 경로 검색
//JPQL
SLELCT m.username, m.age FROM Member m
//SQL
SLELCT m.name, m.age
FROM Member m
🔹단일 값 연관 필드 연관 경로 탐색
//JPQL
SLELCT o.member FROM Order o
//SQL
SLELCT m.*
FROM Orders o INNER JOIN Member m ON o.member_id=m.id
o.member
→ 주문에서 회원으로 단일 값 연관 필드로 경로 탐색
🔹컬렉션 값 연관 필드 경로 탐색
SLELCT t.members FROM Team t //성공
SLELCT t.members.username FROM Team t //실패
//조인을 사용해서 새로운 별칭 획득 → 컬렉션 경로 탐색 가능
SLELCT m.username FROM Team t join t.members m
//size : 컬렉션의 크기를 구할 수 있는 기능
//COUNT 함수를 사용하는 SQL로 변환
SLELCT t.members.size FROM Team t
🤖경로 탐색 코드
public static void searchLoad(EntityManager em) {
em.clear();
System.out.println("=============================== use SearchLoad ============================");
System.out.println("============================== 상태 필드 경로 =============================");
String statusField = "SELECT m.name, m.age FROM Member m";
List<Object[]> statusFieldResult = em.createQuery(statusField).getResultList();
for (Object[] o : statusFieldResult) {
System.out.print("Member name: " + (String) o[0] + ", ");
System.out.println("Member age: " + (Integer) o[1]);
}
System.out.println("============================== 단일 필드 경로 =============================");
String oneRelationshipField = "SELECT o.member FROM Order o";
List<Member> oneResult = em.createQuery(oneRelationshipField, Member.class).getResultList();
for (Member member : oneResult) {
System.out.print("Member name: " + member.getName() + ", ");
System.out.println("Member age: " + member.getAge());
}
System.out.println("============================== 컬렉션 필드 경로 =============================");
String collectionRelationshipField = "SELECT t.members FROM Team t";
List<Member> collectionResult = em.createQuery(collectionRelationshipField).getResultList();
for (Member member : collectionResult) {
System.out.print("Member name: " + member.getName() + ", ");
System.out.println("Member age: " + member.getAge());
}
}
✅ 서브 쿼리
🔻제약 사항
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
🔹서브 쿼리 함수
🔻EXISTS
- 문법 :
[NOT] EXISTS (subquery)
- 설명 : 서브쿼리에 결과가 존재하면 참
NOT
→ 반대
- ex) 팀A 소속인 회원
SELECT m FROM Member m WHERE EXISTS (SELECT t FROM m.team t WHERE t.name = '팀A')
🔻[ ALL | ANY | SOME ]
- 문법 :
{ALL | ANY | SOME} (subquery)
- 설명 : 비교 연산자와 같이 사용( =, >, >=, <, <=, <>)
ALL
: 서브쿼리의 모든 결과가 만족해야 조건이 참
ANY
혹은SOME
: 서브쿼리의 조건을 하나라도 만족하면 참
- ex1) 전체 상품 각각의 재고보다 주문량이 많은 주문들
SELECT o FROM Order o WHERE o.orderAmount > ALL (SELECT p.stockAmount FROM Product p)
ALL 연산자는 서브쿼리의 결과로 생성된 모든 결과를 각각 개별적으로 검사하고 난 뒤, 모든 결과가 참이여야지 해당 엔티티를 반환한다. 다음을 보자.
🔸가정:
- 주문 1: 주문 금액 = 100
- 주문 2: 주문 금액 = 70
- 주문 3: 주문 금액 = 110
🔸JPQL:
SELECT o FROM Order o WHERE 80 > ALL (SELECT o2.orderAmount FROM Order o2)
🔸연산
ALL
연산자는 서브쿼리의 결과 집합을 가져와 개별적으로 주어진 조건 "주문 금액이 80보다 작은 경우"와 비교한다.
- 첫 번째 결과 (주문 1: 주문 금액 = 100)을 가져와서 검사한다. 이 조건은 거짓이다.
- 두 번째 결과 (주문 2: 주문 금액 = 70)을 가져와서 검사한다. 이 조건은 참이다.
- 세 번째 결과 (주문 3: 주문 금액 = 110)을 가져와서 검사한다. 이 조건은 거짓입니다.
ALL
연산자는 모든 결과가 조건을 만족하는지 확인하고, 하나라도 거짓이면 최종적으로 조건이 거짓으로 판단한다. 따라서 위 JPQL의 결과는 거짓이다. 어떠한 Order 엔티티도 반환하지 않는다.
🔻IN
- 문법 :
[NOT] IN (subquery)
- 설명 : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
- 서브쿼리가 아닌 곳에서도 사용
- ex) 20세 이상을 보유한 팀
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 타입 지정) | 10L10D10F |
날짜 | ◾ DATE {d 'yyyy-mm-dd'} ◾ TIME {t 'hh-mm-ss'} ◾ DATETIME {ts 'yyyy-mm-dd hh:mm:ss.f'} | {d '2012-03-24'}{t '10-11-11'} {ts '2012-03-24 10:11:11.123'} m.createDate = {d '2012-03-24'} |
Boolean | TRUE, FALSE | |
Enum | 패키지명을 포함한 전체 이름 사용 | jpabook.MemberType.Admin |
엔티티 타입 | ◾ 엔티티 타입 표현 ◾ 주로 상속 관련하여 사용 | TYPE(m) = Member |
🔻빈 컬렉션 비교 식
- 문법 :
{컬렉션 값 연관 경로} IS [NOT] EMPTY
- 설명 : 컬렉션에 값이 비어있으면 참
//JPQL : 주문이 하나라도 있는 회원 조회
SELECT m FROM Member m
WHERE m.orders IS NOT EMPTY
// 오류발생
SELECT m FROM Member m
WHERE m.orders IS NULL
🔻컬렉션의 멤버 식
- 문법 : `{엔티티나 값} [NOT] MEMBER [OF] {컬렉션 값 연관 참조}
- 설명 : 엔티티나 값이 컬렉션에 포함되어 있으면 참
SELECT t FROM Team t
WHERE :memberParam member OF t.members
✅ 스칼라 식
🔹문자함수
함수 | 설명 | 예제 |
---|---|---|
CONCAT(문자1, 문자2, ...) | ◾ 문자를 합함 ◾ HQL에서는 ‖로 대체 가능 | CONCAT('A','B') = AB |
SUBSTRING(문자, 위치, [길이]) | ◾ 위치부터 시작해 길이만큼 문자를 구함 ◾ 길이 값 x → 나머지 전체 길이 | SUBSTRING('ABCDEF', 2, 3) = BCD |
TRIM([ LEADING │ TRAILING │ BOTH] [트림문자] FROM] 문자) | ◾ 트림 문자 제거 - LEADING : 왼쪽만 - TRAILING : 오른쪽만 - BOTH : 양쪽 다(기본값) ◾ 트림 문자의 기본값 → 공백(SPACE) | TRIM(' ABC ') = 'ABC' |
LOWER(문자) | 소문자로 변경 | LOWER('ABC') = 'abc' |
UPPER(문자) | 대문자로 변경 | UPPER('abc') = 'ABC' |
LENGTH(문자) | 문자 길이 | LENGTH('ABC') = 3 |
LOCATE(찾을 문자,원본 문자, [검색시작위치]) | 검색위치부터 문자를 검색 ◾ 1부터 시작 ◾ 못 찾으면 0 반환 | LOCATE('DE', 'ABCDEFG') = 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 | 현재 날짜 시간 |
✅ CASE 식
🔹기본 CASE
🔻문법
CASE
{WHEN <조건식> THEN <스칼라식>}+
ELSE <스칼라식>
END
- 예제
select case when m.age <= 10 then '학생요금' when m.age >= 60 then '경로요금' else '일반요금' end from Member m
🔹심플 CASE
- 조건식 사용 불가능
- 단순한 문법
- 자바의
switch case
문과 비슷
🔻문법
CASE <조건대상>
{WHEN <스칼라식1> THEN <스칼라식2>}+
ELSE <스칼라식>
END
- 예제
select case t.name when '팀A' then '인센티브110%' when '팀B' then '인센티브120%' else '인센티브105%' end from Team t
✅ 다형성 쿼리
🔻JPQL로 부모 엔티티를 조회하면 자식 엔티티도 함께 조회 된다
- 엔티티
@Entity @Inheritance(starategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "DTYPE") public abstract class Item {...} @Entity @DiscriminatorValue("B") public class Book extends Item { ... private String author; } //Album, Movie...
List resultList = em.createQuery("select i from Item i").getResultList();
- SINGLE_TABLE
- JOINED
🔹TYPE
🔻엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 사용
//Item 중에 Book, Movie 조회
//JPQL
select i from Item i
where type(i) IN (Book, Movie)
//SQL
SELECT i FROM Item i
WHERE i.DTYPE in ('B', 'M')
🔹TREAT
🔻상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
public static void useTREAT(EntityManager em) {
List<Item> items =
em.createQuery("SELECT i FROM Item i WHERE TREAT(i as Book).author = 'me' ", Item.class)
.getResultList();
for (Item i : items) {
System.out.println("Item name: " + i.getName());
}
}
✅ 엔티티 직접 사용
🔹기본 키 값
👉JPQL는 엔티티 사용한 부분을 SQL로 변환할 때 엔티티의 기본 키 값을 사용하는 것으로 변환한다.
🤖 엔티티를 파라미터로 직접 받는 코드
String qlString = "select m from Member m where m = :member";
List resultList = em.createQuery(qlString)
.setParameter("member", member)
.getResultList();
🔹외래 키 값
🤖 외래 키 대신 엔티티를 직접 사용하는 코드
Team team = em.find(Team.class, 1L);
String qlString = "select m from Member m where m.team = :team";
List resultList = em.createQuery(qlString)
.setParameter("team", team) // 엔티티 세팅
.getResultList();
🤖 외래 키 직접 사용하는 코드
String qlString = "select m from Member m where m.team.id = :teamId";
List resultList = em.createQuery(qlString)
.setParameter("teamId", 1L) // 외래키 세팅
.getResultList();
✅ NamedQuery
- JPQL 쿼리
- 동적 쿼리
- JPQL을 문자로 완성해서 직접 넘기는 것ex)
em.createQuery("select ..")
- 런타임에 특정 조건에 따라 JPQL을 동적으로 구성 가능
- JPQL을 문자로 완성해서 직접 넘기는 것ex)
- 정적 쿼리(= Named 쿼리)
- 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는 것
- 한 번 정의하면 변경할 수 없음
- 애플리케이션 로딩 시점에 JPQL 문법 체크 및 미리 파싱
- 빠른 오류 확인
- 사용 시점에 파싱된 결과 재사용 → 성능 향상
- 정적 SQL 생성 → 데이터베이스의 조회 성능 최적화에 도움
- 동적 쿼리
🔹@NamedQueries, @NamedQuery
@Entity
@NamedQueries({
@NamedQuery(name = "Member.findByName", query = "SELECT m FROM Member m WHERE m.name = :name"),
@NamedQuery(name = "Member.count", query = "SELECT COUNT(m) FROM Member m")
})
public class Member {
- name : 쿼리 이름 부여
- query : 사용할 JQPL 쿼리
- 사용
public static void namedQuery(EntityManager em) { List<Member> members = em.createNamedQuery("Member.findByName", Member.class) .setParameter("name", "member1") .getResultList(); for (Member member : members) { System.out.print("Member Name: " + member.getName()); } }
✅ 벌크 연산
🔻한 번에 여러 데이터를 수정, 삭제하는 연산이다.
public static void bulk(EntityManager em) {
//String이므로 띄어쓰기에 유의
String jpqlString = "UPDATE Product p " +
"SET p.price = p.price * 2.0 " +
"WHERE p.stockAmount < :stockAmount";
int resultCount = em.createQuery(jpqlString)
.setParameter("stockAmount", 20)
.executeUpdate();
System.out.println(resultCount);
}
Uploaded by N2T