객체지향 쿼리 언어 소개
JPA는 다양한 쿼리 방법을 지원
- JPQL
- JPA Criteria
- QueryDSL
- 네이티브 SQL
- JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 함께 사용
JPQL 소개
- 가장 단순한 조회 방법
- EntityManager.find()
- 객체 그래프 탐색(a.getB().getC())
- 나이가 18살 이상인 회원을 모두 검색하고 싶다면?
JPQL
- JPA를 사용하면 테이블이 아닌 엔티티 객체를 중심으로 개발해야 한다.
- SQL은 테이블을 대상으로 작성되기에 테이블에 종속적이게 된다.
- 문제는 검색 쿼리다.
- 왜냐면 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색해야 되기 때문이다.
- 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능하다.
- 결국은 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요하다.
- 애플리케이션에서 데이터베이스의 데이터를 가져올 때는 최소한의 데이터만 가져와야 한다.
이런 문제들을 해결하기 위해 JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공한다.
SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN처럼 ANSI 표준 SQL이 지원하는 문법은 다 지원한다. JPQL은 엔티티 객체를 대상으로 한 쿼리이고 반면에 SQL은 데이터베이스 테이블을 대상으로 한 쿼리이다.
JPQL 예시
List<Member> result = em.createQuery("select m from Member m where m.name like '%kim%'", Member.class)
.getResultList();
실행쿼리
Hibernate:
/* select
m
from
Member m
where
m.name like '%kim%' */ select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.city,
m1_0.street,
m1_0.zipcode,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
m1_0.USERNAME,
m1_0.endDate,
m1_0.startDate,
m1_0.TEAM_ID
from
Member m1_0
where
m1_0.USERNAME like '%kim%' escape ''
SQL과는 살짝 다르다. SQL은 모든 컬럼 조회 시 select 뒤에 * 이 들어가는데 JPA는 m이 들어간다.
m은 Member 엔티티를 가리킨다.
정리
테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리이다.
SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
JPQL을 한마디로 정의하면 객체 지향 SQL이다.
Criteria 소개
JPQL 작성하는 부분에서 em.createQuery("select m from Member m where m.name like '%kim%'") ()안에 있는 부분은 그냥 문자열이다. 그래서 동적 쿼리를 작성하기 굉장히 불편하다. 그래서 대안으로 나온 게 Criteria이다.
코드예시
//Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
//루트 클래스 (조회를 시작할 클래스)
Root<Member> m = query.from(Member.class);
//쿼리 생성
CriteriaQuery<Member> cq = query.select(m);
String name = "sadf";
if (name != null) {
cq = query.select(m).where(cb.equal(m.get("name"), "kim"));
}
List<Member> resultList = em.createQuery(cq).getResultList();
실행쿼리
Hibernate:
/* <criteria> */ select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.city,
m1_0.street,
m1_0.zipcode,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
m1_0.USERNAME,
m1_0.endDate,
m1_0.startDate,
m1_0.TEAM_ID
from
Member m1_0
where
m1_0.USERNAME=?
- 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
- 컴파일 시점에 SQL 오타를 잡아준다.
- 동적 쿼리도 JPQL에 비해 깔끔하게 짤 수 있다.
- JPQL 빌더 역할
- JPA 공식 기능
- 단점: 너무 복잡하고 실용성이 없다.
- Criteria 대신에 QueryDSL 사용 권장
QueryDSL 소개
//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();
- 문자가 아닌 자바코드로 JPQL을 작성할 수 있다.
- JPQL 빌더 역할
- 컴파일 시점에 문법 오류를 찾을 수 있다.
- 동적쿼리 작성 편리하다.
- 단순하고 쉬움
- 실무 사용 권장
네이티브 SQL 소개
- JPA가 제공하는 SQL을 직접 사용하는 기능
- JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능
- 예) 오라클 CONNECT BY, 특정 DB만 사용하는 SQL 힌트
String sql = "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = ‘kim’";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();
sql에 완전 쌩 SQL문을 작성하면 된다.
JDBC 직접 사용, SpringJdbcTemplate 등
- JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, 마이바티스등을 함께 사용 가능
- 단 영속성 컨텍스트를 적절한 시점에 강제로 플러시하는게 필요하다.
- 예) JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 플러시
Member member = new Member();
member.setName("member1");
em.persist(member);
connection.executeQuery("select * from member");
위에서 connection은 데이터베이스의 커넥션을 얻어온 객체이다.
이 객체를 통해 DB에 SQL문을 전송한다.
JPQL 같은 경우 JPQL을 DB로 전송 시 자동으로 플러시가 발생한다.
하지만 위 방식은 JPA와 아무 연관이 없어 플러시가 발생하지 않는다.
그래서 쓰기 지연 저장소에 쌓인 쿼리들이 DB로 전송되지 않아 DB에 데이터가 존재하지 않는다.
결국 connection.executeQuery("select * from member"); 시 잘못된 데이터를 가져올 수 있다.
그래서 위 문제를 해결하기 위해 아래처럼 중간에 flush()를 강제로 실행하면 된다.
Member member = new Member();
member.setName("member1");
em.persist(member);
em.flush();
connection.executeQuery("select * from member");
기본 문법과 기능
- JPQL은 객체지향 쿼리 언어다. 따라서 테이블을 대상으로 쿼리 하는 것이 아니라 엔티티 객체를 대상으로 쿼리 한다.
- JPQL은 SQL을 추상화해서 특정데이터베이스 SQL에 의존하지 않는다.
- JPQL은 결국 매핑 정보랑 방언이 조합돼서 SQL로 변환된다.
JPQL 문법
select_문 :: =
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
- select m from Member as m where m.age > 18
- 엔티티와 속성은 대소문자 구분한다 (Member, age)
- JPQL 키워드는 대소문자 구분하지 않는다. (SELECT, FROM, where)
- 엔티티 이름 사용, 테이블 이름이 아님(Member)
- 별칭은 필수(m) (as는 생략가능)
집합과 정렬
select
COUNT(m), //회원수
SUM(m.age), //나이 합
AVG(m.age), //평균 나이
MAX(m.age), //최대 나이
MIN(m.age) //최소 나이
from Member m
- GROUP BY, HAVING
- ORDER BY
TypeQuery, Query
- TypeQuery: 반환 타입이 명확할 때 사용
- Query: 반환 타입이 명확하지 않을 때 사용
// 반환 타입이 명확한 경우
TypedQuery<Member> typedQuery = em.createQuery("select m from Member m", Member.class);
TypedQuery<String> typedQuery1 = em.createQuery("select m.username, m.age from Member m", String.class);
// 반환 타입이 명확하지 않은 경우
Query query = em.createQuery("select m.username, m.age from Member m");
결과 조회 API
- query.getResultList(): 결과가 하나 이상일 때, 리스트 반환
- 결과가 없으면 빈 리스트 반환
- query.getSingleResult(): 결과가 정확히 하나, 단일 객체 반환
- 결과가 없으면: javax.persistence.NoResultException
- 둘 이상이면: javax.persistence.NonUniqueResultException
// 조회 결과가 없을때 NoResultException 발생
// 조회 결과가 복수일때 NonUniqueResultException 발생
// 조회 결과가 단일인 경우 getSingleResult() 사용
Member member1 = typedQuery2.getSingleResult();
// 결과가 하나 이상일 때 getResultList() 사용
List<Member> resultList = typedQuery2.getResultList();
파라미터 바인딩 - 이름 기준, 위치 기준
이름 기준
Member singleResult = em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
System.out.println("singleResult: " + singleResult.getUsername());
위치 기준
SELECT m FROM Member m where m.username=?1
query.setParameter(1, usernameParam);
위치 기반 파라미터 바인딩은 안 쓰는 게 좋다.
순서를 잘못 사용하면 장애가 발생할 수 있다.
프로젝션
- SELECT 절에 조회할 대상을 지정하는 것
- 프로젝션 대상: 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)
- 관계형 DB같은 경우에는 select에 기본 데이터 타입들만(스칼라 타입) 선택할 수 있다.
- SELECT m FROM Member m -> 엔티티 프로젝션
- m은 Member 엔티티의 alias이다.
- SELECT m.team FROM Member m -> 엔티티 프로젝션
- m.team을 통해 member와 연관된 엔티티인 Team을 가져온다.
- 결과는 엔티티인 Team이다.
- SELECT m.address FROM Member m -> 임베디드 타입 프로젝션
- SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션
- DISTINCT로 중복 제거 가능하다.
- 프로젝션을 통해 조회한 엔티티들은 영속성 컨텍스테에 저장되서 관리된다.
엔티티 프로젝션 조회 예시
List<Member> result = em.createQuery("select m from Member m", Member.class)
.getResultList();
위 코드를 예시로 들면 조회한 member들은 모두 영속성 컨텍스트에서 저장되고 관리된다.
List<Team> result2 = em.createQuery("select m.team from Member m", Team.class)
.getResultList();
Team findTeam = result2.get(0);
m.team을 조회 시 Team 엔티티를 조회하므로 반환 타입은 Team이다.
위 코드를 실행 시 아래와 같이 쿼리가 나간다.
쿼리를 살펴보면 member와 team을 조인해서 데이터를 가져온다.
Hibernate:
/* select
m.team
from
Member m */ select
t1_0.id,
t1_0.name
from
Member m1_0
join
Team t1_0
on t1_0.id=m1_0.TEAM_ID
JPQL은 최대한 SQL에 가깝게 작성하는 게 좋다.
그래서 위 JPQL을 아래처럼 조인을 사용하는 식으로 수정하는 게 좋다.
List<Team> result2 = em.createQuery("select t from Member m join m.team t", Team.class)
.getResultList();
임베디드 타입 프로젝션 조회 예시
List<Address> resultList = em.createQuery("select o.address from Order o", Address.class)
.getResultList();
실행 쿼리
Hibernate:
/* select
o.address
from
Order o */ select
o1_0.city,
o1_0.street,
o1_0.zipcode from
ORDERS o1_0
스칼라 타입 프로젝션 예시
em.createQuery("select m.username, m.age from Member m")
.getResultList();
스칼라 타입 프로젝션 사용 시 위와 같이 응답 타입이 2개 이상인 경우에 대해 어떻게 가져올지 고민해봐야 한다.
프로젝션 - 여러 값 조회
여러 필드가 select에 나열되어 있고 나열된 필드들이 서로 타입이 다를 경우 3가지 방법으로 조회할 수 있다.
- SELECT m.username, m.age FROM Member m
- 1. Query 타입으로 조회
- 2. Object[] 타입으로 조회
- 3. new 명령어로 조회
- 단순 값을 DTO로 바로 조회
SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m - 패키지 명을 포함한 전체 클래스 명 입력
- 순서와 타입이 일치하는 생성자 필요
- 단순 값을 DTO로 바로 조회
Query 타입으로 조회 예시
List query = em.createQuery("select m.username, m.age from Member m")
.getResultList();
Object o = query.get(0);
Object[] result = (Object[]) o;
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
Object[] 타입으로 조회 조회 예시
List<Object[]> query = em.createQuery("select m.username, m.age from Member m")
.getResultList();
Object[] result = query.get(0);
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
new 명령어로 조회 예시
List<MemberDTO> result = em.createQuery("select new hellojpa.jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
.getResultList();
MemberDTO memberDTO = result.get(0);
System.out.println("username = " + memberDTO.getUsername());
System.out.println("age = " + memberDTO.getAge());
// 주의 : MemberDTO에 생성자 필요
@Getter
@Setter
public class MemberDTO {
public MemberDTO(String username, int age) {
this.username = username;
this.age = age;
}
private String username;
private int age;
}
실행결과
Hibernate:
/* select
new hellojpa.jpql.MemberDTO(m.username, m.age)
from
Member m */ select
m1_0.username,
m1_0.age
from
Member m1_0
username = member1
age = 10
페이징 API
- JPA는 페이징을 다음 두 API로 추상화
- setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
- setMaxResults(int maxResult) : 조회할 데이터 수
- 위 두 API의 의미는 몇 번째부터 몇개 가져올래 라고 이해하면 된다.
페이징 API 예시
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(0)
.setMaxResults(10)
.getResultList();
System.out.println("result.size = " + result.size());
for (Member member1 : result) {
System.out.println("member1 = " + member1);
}
실행결과
Hibernate:
/* select
m
from
Member m
order by
m.age desc */ select
m1_0.id,
m1_0.age,
m1_0.TEAM_ID,
m1_0.username
from
Member m1_0
order by
m1_0.age desc
offset
? rows
fetch
first ? rows only
result.size = 10
member1 = Member{id=100, team=null, username='member99', age=99}
member1 = Member{id=99, team=null, username='member98', age=98}
member1 = Member{id=98, team=null, username='member97', age=97}
member1 = Member{id=97, team=null, username='member96', age=96}
member1 = Member{id=96, team=null, username='member95', age=95}
member1 = Member{id=95, team=null, username='member94', age=94}
member1 = Member{id=94, team=null, username='member93', age=93}
member1 = Member{id=93, team=null, username='member92', age=92}
member1 = Member{id=92, team=null, username='member91', age=91}
member1 = Member{id=91, team=null, username='member90', age=90}
페이징 API - MySQL 방언
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 ?, ?
페이징 API - Oracle 방언
SELECT * FROM
( SELECT ROW_.*, ROWNUM ROWNUM_
FROM
( 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
) ROW_
WHERE ROWNUM <= ?
)
WHERE ROWNUM_ > ?
조인
- 내부 조인:
- 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
내부 조인 예시
String query = "select m from Member m inner join m.team t";
List<Member> result = em.createQuery(query, Member.class).getResultList();
실행결과
Hibernate:
/* select
m
from
Member m
join
m.team t */ select
m1_0.id,
m1_0.age,
m1_0.TEAM_ID,
m1_0.username
from
Member m1_0
join
Team t1_0
on t1_0.id=m1_0.TEAM_ID
외부 조인 예시
String query = "select m from Member m left join m.team t";
List<Member> result = em.createQuery(query, Member.class).getResultList();
실행결과
Hibernate:
/* select
m
from
Member m
left join
m.team t */ select
m1_0.id,
m1_0.age,
m1_0.TEAM_ID,
m1_0.username
from
Member m1_0
세타 조인 예시
String query = "select m from Member m, Team t where m.username = t.name";
List<Member> result = em.createQuery(query, Member.class).getResultList();
조인 - ON 절
ON절을 활용한 조인(JPA 2.1부터 지원)
- 1. 조인 대상 필터링
- 2. 연관관계없는 엔티티 외부 조인(하이버네이트 5.1부터) 가능
1. 조인 대상 필터링
조인 대상을 필터링한다는 것은 예를 들어 회원과 팀을 조인하면서, 팀 이름이 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'
2. 연관관계 없는 엔티티 외부 조인
예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
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
서브 쿼리
SQL에서 말하는 서브 쿼리와 똑같다.
나이가 평균보다 많은 회원
select m from Member m where m.age > (select avg(m2.age) from Member m2)
위 쿼리는 메인 select 문에서 사용한 m을 서브 select 문에서 사용하지 않는다.
즉 메인 쿼리와 서브 쿼리가 관계가 없다. 이런 식의 쿼리가 보통 성능이 잘 나온다.
한 건이라도 주문한 고객
select m from Member m where (select count(o) from Order o where m = o.member) > 0
위 쿼리는 메인 select 문에서 사용한 m을 서브 select 문에서 사용한다.
이런 경우에 보통 성능이 잘 안 나온다.
서브 쿼리 지원 함수
- [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)
JPA 서브 쿼리 한계
- JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능
- SELECT 절도 가능(하이버네이트에서 지원)
- FROM 절의 서브 쿼리는 현재 JPQL에서 불가능
- 조인으로 풀 수 있으면 풀어서 해결
하이버네이트6 변경 사항
하이버네이트6부터는FROM 절의 서브쿼리를 지원한다.
참고 링크
https://in.relation.to/2022/06/24/hibernate-orm-61-features/
JPQL 타입 표현
JPQL에서 문자, 숫자를 어떻게 표현할지에 대한 내용이다.
- 문자: ‘HELLO’, ‘She’’s’
- 숫자: 10L(Long), 10D(Double), 10F(Float)
- Boolean: TRUE, FALSE
- ENUM: jpabook.MemberType.Admin (패키지명 포함)
- 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)
예시 코드
String sql2 = "select m.username, 'HELLO', true From Member m " +
"where m.memberType = :userType";
List<Object[]> result = em.createQuery(sql2)
.setParameter("userType", MemberType.ADMIN)
.getResultList();
for (Object[] objects : result) {
System.out.println("objects = " + objects[0]);
System.out.println("objects = " + objects[1]);
System.out.println("objects = " + objects[2]);
}
JPQL 기타
- 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
단순 CASE 식
select
case t.name
when '팀A' then '인센티브110%'
when '팀B' then '인센티브120%'
else '인센티브105%'
end
from Team t
조건식 - CASE 식
- COALESCE: 하나씩 조회해서 null이 아니면 반환
- NULLIF: 두 값이 같으면 null 반환, 다르면 첫 번째 값 반환
사용자 이름이 없으면 이름 없는 회원을 반환
select coalesce(m.username,'이름 없는 회원') from Member m
사용자 이름이 ‘관리자’면 null을 반환하고 나머지는 본인의 이름을 반환
select NULLIF(m.username, '관리자') from Member m
코드예시
String query2 = "select coalesce(m.username, '이름 없는 회원') as username from Member m";
List<String> resultList = em.createQuery(query2, String.class).getResultList();
String query3 = "select nullif(m.username, '이름 없는 회원') as username from Member m";
List<String> resultLis2 = em.createQuery(query3, String.class).getResultList();
JPQL 기본 함수
- CONCAT
- SUBSTRING
- TRIM
- LOWER, UPPER
- LENGTH
- LOCATE
- ABS, SQRT, MOD
- SIZE, INDEX(JPA 용도)
위 함수는 데이터베이스에 관계없이 사용할 수 있다.
사용자 정의 함수 호출
데이터베이스의 함수를 가져다 쓰는 방법이다.
쓰기 위해서는 하이버네이트는 사용전 방언에 추가해야 한다.
사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.
select function('group_concat', i.name) from Item i
참고
김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본 편 강의를 보고 정리한 내용입니다.
'JPA > JPA 기본' 카테고리의 다른 글
11. 객체지향 쿼리 언어(JPQL) - 2 (0) | 2024.08.13 |
---|---|
9. 값 타입 (0) | 2024.08.12 |
8. 프록시와 연관관계 관리 (0) | 2024.08.11 |
7. 상속관계 매핑 (0) | 2024.08.10 |
6. 다양한 연관관계 매핑 (0) | 2024.08.10 |