이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.
Java Persistence Query Language (JPQL)은 Java Persistence API (JPA)에서 사용하는 쿼리 언어이다. JPQL은 객체 지향 쿼리 언어로, SQL과 유사하지만 엔티티 객체를 대상으로 하여 데이터베이스와 상호작용한다. JPQL을 사용하면 데이터베이스의 특정 데이터를 조회하거나 조작할 수 있다.
- JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공
- SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원
- JPQL은 엔티티 객체를 대상으로 쿼리(테이블과 컬럼이 아닌 엔티티와 엔티티의 속성을 대상)
- SQL은 데이터베이스 테이블을 대상으로 쿼리
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존X
- JPQL을 한마디로 정의하면 객체 지향 SQL
//검색
String jpql = "select m From Member m where m.name like ‘%hello%'";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
--실행된 SQL
select
m.id as id,
m.age as age,
m.USERNAME as USERNAME,
m.TEAM_ID as TEAM_ID
from
Member m
where
m.age>18
QueryDSL
- 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
- JPQL 빌더 역할
- 컴파일 시점에 문법 오류를 찾을 수 있음
- 동적쿼리 작성 편리함
- 단순하고 쉬움
- 실무 사용 권장
//JPQL
//select m from Member m where m.age > 18
JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member;
List<Member> list = query.selectFrom(m).where(m.age.gt(18)).orderBy(m.name.desc()).fetch();
네이티브 SQL
- JPA가 제공하는 SQL을 직접 사용하는 기능
- JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능
- 예) 오라클 CONNECT BY, 특정 DB만 사용하는 SQL 등은 네이티브 SQL을 사용
String sql = “SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = ‘kim’";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();
JDBC 직접 사용, SpringJdbcTemplate 등
- JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, 마이바티스등을 함께 사용 가능
- 단 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요
- 예) JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 플러시
기본 문법과 쿼리 API
- JPQL은 객체지향 쿼리 언어다.따라서 테이블을 대상으로 쿼리 하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
- JPQL은 SQL을 추상화해서 특정 관계형 데이터베이스 SQL에 의존하지 않는다.
- JPQL은 결국 SQL로 변환된다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
@Enumerated(EnumType.STRING)
private MemberType type;
//getter and setter...
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String username;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
//getter and setter...
}
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
//getter and setter...
}
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
private Long id;
private int orderAmount;
@Embedded
private Address address;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
//getter and setter...
}
@Entity
public class Product {
@Id @GeneratedValue
private Long id;
private int price;
private int stockAmount;
//getter and setter...
}
public enum MemberType {
ADMIN, USER
}
JPQL 문법
select_문 :: =
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
//검색
String jpql = "select m from Member m where m.age > 18";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
- 엔티티와 속성은 대소문자 구분O (Member, age)
- JPQL 키워드는 대소문자 구분X (SELECT, FROM, where)
- 엔티티 이름 사용, 테이블 이름이 아님(Member)
- 별칭은 필수(m) (as는 생략가능)
집합과 정렬
select
COUNT(m), // Member 수
SUM(m.age), // Member의 나이 합
AVG(m.age), // Member의 평균 나이
MAX(m.age), // Member의 최대 나이
MIN(m.age) // Member의 최소 나이
from Member m
- GROUP BY, HAVING, ORDER BY 등 모두 사용이 가능하다.
TypeQuery, Query
- TypeQuery: 반환 타입이 명확할 때 사용
- Query: 반환 타입이 명확하지 않을 때 사용
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
TypedQuery<Member> query = em.createQuery("SELECT m.username FROM Member m", String.class);
Query query = em.createQuery("SELECT m.username, m.age from Member m");
결과 조회
- query.getResultList(): 결과가 하나 이상일 때, 리스트 반환
-> 결과가 없으면 빈 리스트 반환 - query.getSingleResult(): 결과가 정확히 하나 일때 사용, 단일 객체 반환
-> 결과가 없으면: javax.persistence.NoResultException
-> 둘 이상이면: javax.persistence.NonUniqueResultException
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
em.persist(member);
TypedQuery<Member> query = em.createQuery("select m from Member m where m.id = 1", Member.class);
List<Member> resultList = query.getResultList();
Member singleResult = query.getSingleResult();
System.out.println("result = " + resultList.get(0).getUsername());
System.out.println("result = " + singleResult.getUsername());
Spring Data JPA 를 사용하면 query.getSingleResult() 를 사용할 때 NoResultException 와 NonUniqueResultException 예외를 잡아서 Optional, 리스트 또는 빈 리스트로 반환하니까 걱정안해도 된다.
파라미터 바인딩 (이름 기준, 위치 기준)
이름 기준
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
em.persist(member);
Member result = em.createQuery("SELECT m FROM Member m where m.username=:username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
위치 기준
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
em.persist(member);
Member result = em.createQuery("SELECT m FROM Member m where m.username= ?1", Member.class)
.setParameter(1, "member1")
.getSingleResult();
프로젝션(여러 값 조회)
SELECT 절에 조회할 대상을 지정하는 것
프로젝션 대상: 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자등 기본 데이터 타입)
- SELECT m FROM Member m -> 엔티티 프로젝션
- SELECT m.team FROM Member m -> 엔티티 프로젝션
- SELECT o.address FROM Order o -> 임베디드 타입 프로젝션
- SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션
DISTINCT로 중복 제거 가능
List<Team> result = em.createQuery("SELECT m.team FROM Member m", Team.class).getResultList();
em.createQuery("SELECT o.address FROM Order o", Address.class).getResultList();
em.createQuery("SELECT m.username, m.age FROM Member m").getResultList();
이와 같은 경우 여러가지 방법으로 조회하는 방법이 있지만 단순 값을 DTO로 바로 조회하는 방법이 편하다.
package hellojpa.jpql;
public class MemberDTO {
private String username;
private int age;
public MemberDTO(String username, int age) {
this.username = username;
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
em.persist(member);
List<MemberDTO> resultList = em.createQuery("SELECT new hellojpa.jpql.MemberDTO(m.username, m.age) FROM Member m", MemberDTO.class)
.getResultList();
MemberDTO memberDTO = resultList.get(0);
System.out.println(memberDTO.getUsername());
System.out.println(memberDTO.getAge());
- 패키지 명을 포함한 전체 클래스 명 입력
- 순서와 타입이 일치하는 생성자 필요
페이징
JPA는 페이징을 다음 두 API로 추상화
- setFirstResult(int startPosition) : 조회 시작 위치
setFirstResult(10)으로 설정하면, 쿼리 결과의 11번째 항목부터 데이터를 가져오기 시작한다. 이는 0부터 시작하기 때문이다. - setMaxResults(int maxResult) : 조회할 데이터 수
setMaxResults(10)으로 설정하면, 최대 10개의 결과를 가져온다.
for(int i = 0; i < 100; ++i)
{
Member member = new Member();
member.setUsername("member" + i);
member.setAge(i);
em.persist(member);
}
String jpql = "select m from Member m order by m.age desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setFirstResult(0)
.setMaxResults(10)
.getResultList();
System.out.println("result size = " + resultList.size());
for(Member member : resultList){
System.out.println(member);
}
조인
- 내부 조인 : 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
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
member.setTeam(team);
em.persist(member);
String query = "select m from Member m inner join m.team t where t.name = :teamName";
List<Member> resultList = em.createQuery(query, Member.class)
.setParameter("teamName", "teamA")
.getResultList();
for (Member m : resultList) System.out.println(m);
주어진 JPQL 쿼리는 teamA라는 이름을 가진 팀에 속한 모든 멤버를 조회하는 쿼리이다.
조인 대상 필터링
예) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
- 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'
연관관계 없는 엔티티 외부 조인
예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
- 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
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
member.setTeam(team);
em.persist(member);
String query = "select m from Member m left join m.team t on t.name = :teamName";
List<Member> resultList = em.createQuery(query, Member.class)
.setParameter("teamName", "teamA")
.getResultList();
for (Member m : resultList) System.out.println(m);
서브 쿼리
나이가 평균보다 많은 회원
select m from Member m
where m.age > (select avg(m2.age) from Member m2)
- outter 쿼리의 m과 inner 쿼리의 m2는 상관이 없다.
- 또한 outter와 inner의 대상이 서로 달라야(상관이 없어야) 쿼리의 성능이 향상된다.
한 건이라도 주문한 고객
select m from Member m
where (select count(o) from Order o where m = o.member) > 0
서브 쿼리 지원 함수
- [NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
- {ALL | ANY | SOME} (subquery)
- ALL 모두 만족하면 참
- ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참
- [NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
팀A 소속인 회원
select m from Member 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)
JPQL 타입 표현
- 문자: ‘HELLO’, ‘She’’s’
- 숫자: 10L(Long), 10D(Double), 10F(Float)
- Boolean: TRUE, FALSE
- ENUM: jpabook.MemberType.Admin (패키지명 포함)
- 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
member.setTeam(team);
member.setType(MemberType.USER);
em.persist(member);
//String query = "select m.username from Member m where m.type = hellojpa.jpql.MemberType.USER";
String query = "select m.username from Member m where m.type = :userType";
Object singleResult = em.createQuery(query)
.setParameter("userType", MemberType.USER)
.getSingleResult();
System.out.println(singleResult);
기타
- SQL과 문법이 같은 식
- EXISTS, IN
- AND, OR, NOT
- =, >, >=, <, <=, <>
- BETWEEN, LIKE, IS NULL
CASE(조건식)
기본 CASE 식
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금'
end
from Member m
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
member.setTeam(team);
member.setType(MemberType.USER);
em.persist(member);
em.flush();
em.clear();
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 result : resultList) {
System.out.println("result = " + result);
}
단순 CASE식
select
case t.name
when '팀A' then '인센티브110%'
when '팀B' then '인센티브120%'
else '인센티브105%'
end
from Team t
COALESCE: 여러 인수를 받아서 그 중 첫 번째로 널이 아닌 값을 반환
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername(null); //null
member.setAge(10);
member.setTeam(team);
member.setType(MemberType.USER);
em.persist(member);
em.flush();
em.clear();
String query = "select coalesce(m.username, '이름 없는 회원') from Member m";
List<String> resultList = em.createQuery(query, String.class).getResultList();
for (String result : resultList) {
System.out.println("result = " + result);
}
COALESCE 함수는 SQL 및 JPQL에서 널(null) 값을 처리하는 데 사용된다.
여러 인수를 받아서 그 중 첫 번째로 널이 아닌 값을 반환한다.
COALESCE는 데이터베이스 쿼리에서 널 값을 대체하거나 기본 값을 제공하는 데 매우 유용하다.
COALESCE 함수 설명
용도: 여러 인수 중 첫 번째 널이 아닌 값을 반환한다.
형식: COALESCE(value1, value2, ..., valueN)
반환 값: 인수 중 첫 번째 널이 아닌 값. 모든 인수가 널이면 널을 반환한다.
SELECT COALESCE(m.nickname, m.username) FROM Member mString jpql = "SELECT COALESCE(m.nickname, m.username) FROM Member m"; TypedQuery<String> query = em.createQuery(jpql, String.class); List<String> names = query.getResultList(); for (String name : names) { System.out.println(name); }
-> Member 엔티티에서 nickname이 널인 경우 username을 반환한다.
NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("관리자"); //null
member.setAge(10);
member.setTeam(team);
member.setType(MemberType.USER);
em.persist(member);
em.flush();
em.clear();
String query = "select NULLIF(m.username, '관리자') from Member m";
List<String> resultList = em.createQuery(query, String.class).getResultList();
for (String result : resultList) {
System.out.println("result = " + result);
}
String jpql = "SELECT NULLIF(m.nickname, m.username) FROM Member m";
TypedQuery<String> query = em.createQuery(jpql, String.class);
List<String> results = query.getResultList();
for (String result : results) {
System.out.println(result);
}
SELECT NULLIF(m.nickname, m.username) FROM Member m
->nickname이 username과 같으면 NULL, 다르면 nickname을 반환한다.
JPQL 기본 함수
- CONCAT() : 두 개 이상의 문자열을 연결
- SUBSTRING() : 문자열의 일부를 반환
- TRIM() : 문자열의 앞뒤 공백을 제거
- LOWER(), UPPER() : 문자열을 (대)소문자로 변환
- LENGTH() : 문자열의 길이를 반환
- LOCATE() : 문자열 내에서 특정 문자열의 위치를 반환
- ABS(), SQRT(), MOD() : 절댓값, 제곱근, 나머지 반환
- SIZE, INDEX(JPA 용도) : 컬렉션의 크기를 반환, 리스트에서 요소의 위치를 반환
'Back-End > JPA' 카테고리의 다른 글
[JPA] 병합(Merge)과 변경 감지(Dirty Checking) (0) | 2024.08.05 |
---|---|
[JPA] JPQL 고급 (0) | 2024.08.03 |
[JPA] 값 타입(Value Type) (0) | 2024.07.28 |
[JPA] 즉시 로딩, 지연 로딩, 영속성 전이, 고아 객체 (0) | 2024.07.27 |
[JPA] 프록시(Proxy) (0) | 2024.07.26 |