컴공댕이 공부일지
[JPA] 10장 - 객체지향 쿼리 언어(1) 본문
본 글은 도서 '자바 ORM 표준 JPA 프로그래밍'의 10장 3챕터까지의 요약 정리본입니다.
📖 객체지향 쿼리 소개
엔티티를 쿼리하는 다양한 방법들 알아보기
JPA가 공식 지원하는 다양한 검색 방법
- JPQL ⭐
- Criteria 쿼리 ; JPQL 보조 도구. JPQL을 직접 문자열로 쓰는 대신, 자바 코드로 객체를 조립하듯이 쿼리를 만듦
- 네이티브 SQL ; 그냥 직접 SQL 쓰는 방식
but DB 종속적이고 유지보수가 어려움.
+ 공식은 아니지만 알아둘 가치가 있는 것들
- QueryDSL ; 훨씬 간결하고 강력하게 쿼리를 만들 수 있도록 도와주는 오픈 소스 라이브러리
- JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용
📌 JPQL (Java Persistence Query Language)
JPA(Java Persistence API)에서 엔티티 객체를 대상으로 쿼리를 작성하기 위한 객체 지향 쿼리 언어
관계형 데이터베이스의 테이블이 아닌, 자바 애플리케이션의 엔티티(객체)를 대상으로 쿼리를 작성한다는 점이 가장 큰 특징
1. 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리
2. SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음
한 마디로, 객체지향 SQL !
엔티티 직접 조회, 묵시적 조인, 다형성 지원으로 SQL보다 코드가 간결 !
SELECT u FROM User u WHERE u.username = 'kim'
📌 Criteria Query - JPQL을 생성하는 빌더 클래스
문자가 아닌 프로그래밍 코드로 JPQL을 작성할 수 있다
문자기반 쿼리인 JPQL은 오타가 있어도 컴파일이 성공해 어플리케이션을 서버에 배포하지만, 해당 쿼리가 실행되는 런타임 시점에 오류가 발생한다.
다만 코드로 JPQL을 작성하는 Criteria는 컴파일 시점에 오류를 발견할 수 있다.
👍 Criteria 장점
- 코드로 작성되므로, 컴파일 시점에 오류를 발견할 수 있음
- IDE를 사용 시 코드 자동완성 지원
- 동적 쿼리를 작성하기 편함
query.select(m).where(cb.equal(m.get("username"), "kim"));
메타 모델 API를 사용해 문자 부분까지 코드로 작성한 ver
m.get(Member_.username)
* 메타 모델
자바의 어노테이션 프로세서 기능을 사용해 어노테이션을 분석해 클래스를 생성.
Criteria 전용 클래스 Member_를 생성함. = 메타모델
ex ) Member 클래스가 id라는 필드를 가지고 있다는 정보를 문자열 "id"가 아닌, 정말로 id라는 필드 자체를 나타내는 자바 객체로 만들어 주는 거
👎Criteria 단점
Criteria가 가진 장점이 많지만 모든 장점을 상쇄할 정도로 복잡하고 장황해서
사용하기 불편한 건 물론이고 Criteria로 작성한 코드도 한 눈에 들어오지 않음.
📌QueryDSL
Criteria처럼 JPQL 빌더 역할을 함
👍 QueryDSL 장점
코드 기반이면서 단순하고 사용 쉬움
작성한 코드도 JPQL과 비슷해 한눈에 들어옴
JPAFactoryQuery query = new JPAQueryFactory(em);
QMember member = QMember.member;
List<Member> members =
query.from(member)
.where(member.username.eq("kim"))
.list(member);
Qmember는 Member 엔티티 클래스를 기반으로 생성한 QueryDSL 쿼리 전용 클래스
📌네이티브 SQL
가끔 특정 DB에 의존하는 기능을 사용해야 할 때, 네이티브 SQL 사용.
특정 DB에 의존하는 SQL을 작성해야 하므로, DB를 변경하면 네이비트 SQL도 수정해야함.
📌JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용
드문 경우지만.. JDBC 커넥션에 직접 접근하고 싶을 때, JPA 구현체가 제공하는 방법을 사용.
JPA 엔티티 매니저에서 하이버네이트 세션을 구하고,세션의 dowork 메소드를 호출
( JPA 표준 기능이 아닌, 특정 JPA 구현체(여기서는 Hibernate)의 고유한 기능을 직접 사용하겠다 )
JDBC나 마이바티스를 JPA와 함께 사용하면, 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야함
JPA를 우회해서 DB에 접근하므로, 영속성 컨텍스트와 DB를 불일치 상태로 만들어 데이터 무결성을 훼손할 수도 있음.
데이터 변경하고 아직 플러시 안했는데 JPA를 우회해서 DB에 직접 조회하면 변경 전의 값이 조회되는 문제..!!
이런 이슈를 해결하기 위해 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 데이터베이스와 영속성 컨텍스트를 동기화하면 된다 !
+ 스프링 프레임워크의 AOP를 활용해, DB에 접근하는 메소드를 호출할 때마다 영속성 컨텍스트를 플러시하는 방법
📖 JPQL
다시 정리하는 JPQL의 특징
- JPQL은 객체지향 쿼리 언어. 따라서 테이블 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
- JPQL은 SQL을 추상화해서 특정 DB SQL에 의존하지 않는다.
- JPQL은 결국 SQL로 변환된다.

📜 기본 문법과 쿼리 API
SQL과 비슷하게 JPQL도 SELECT UPDATE DELETE문 사용.
저장은 .persist() 메소드 사용
1️⃣ SELECT 구문
- 대소문자 구분됨
-
JPQL 키워드는 대소문자 구분 안함 ㅎㅅㅎ - 엔티티 이름
- 디폴트는 클래스명 ! 클래스 명을 엔티티 명으로 사용하는 걸 추천함.
- 별칭은 필수
- m.username ! 미사용 시 문법 오류 발생. Member m로 AS 생략도 가능
2️⃣ 쿼리 객체 - TypeQuery, Query
반환할 타입을 명확하게 지정할 수 있으면 TypeQuery 객체, 반환 타입을 명확하게 지정할 수 없으면 Query 객체를 사용.
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);
}
조회 대상이 Member 타입으로 조회 대상 타입이 명확 ! → TypeQuery
Query query = em.createQuery("SELECT m.username, m.age from Member m");
List resultList = query.getResultList();
for(Object o : resultList) {
Object[] list = (Object[]) o; // 결과가 둘 이상이면 Object[], 하나면 object 반환.
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
}
조회 대상이 String 타입인 유저 이름, Integer 타입인 나이이므로 조회 대상 타입이 명확하지 않음 ! → Query
3️⃣ 결과 조회
아래 메소드들 호출 시, 실제 쿼리를 실행해 DB를 조회
- query.getResultList() : 실행된 쿼리의 결과를 리스트(List) 형태로 반환. 결과 없으면 빈 컬렉션 반환
- query.getSingleResult() : 결과가 정확히 하나일 때 사용. - 하나 초과or미만인 경우 예외 발생.
📜 파라미터 바인딩
📌 이름 기준 파라미터
파라미터를 이름을 구분. 앞에 : 사용
String usernameParam = "User1";
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class);
query.setParameter("username", usernameParam);
List<Member> resultList = query.getResultList();
쿼리 객체에 username이 "User1"인 사용자 객체를 찾으라는 조건을 설정, - setParameter()
엔티티 객체들을 DB에서 조회해 와서 List<member> 형태로 반환 - getResultList()
📌 위치 기준 파라미터
? 다음에 위치 값을 주기 ← 이름 기준 파라미터 바인딩 방식이 더 명확.
List<Member> members = em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class)
.setParameter(1, usernameParam)
.getResultList();
위치 기준 파라미터 바인딩으로, 1번 위치에 usernameParam을 맵핑해 바인딩 !
위의 이름 기준 파라미터와 똑같은 결과를 도출함.
String jpql = "SELECT m FROM Member m WHERE m.username = ?1 AND m.email = ?2";
TypedQuery<Member> query = em.createQuery(jpql, Member.class);
// ?1 위치에 usernameParam 값을 바인딩
query.setParameter(1, usernameParam);
// ?2 위치에 emailParam 값을 바인딩
query.setParameter(2, emailParam);
List<Member> members = query.getResultList();
📜 프로젝션
SELECT 절에 조회할 대상을 지정하는 것
📌 엔티티 프로젝션
SELECT m FROM Member m // 회원
SELECT m.team FROM Member m // 팀
칼럼들을 나열해 조회하는 sql과는 다르게 원하는 객체를 바로 조회.
이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리됨.
📌 임베디드 타입 프로젝션
String query = "SELECT o.address FROM Order o";
List<Address> addresses = em.createQuery(query, Address.class).getResultList();
임베디드 타입은 조회의 시작점이 될 수 없다. (제약)
Order 엔티티를 시작점으로 해서, 엔티티를 통해 임베디드 타입을 조회할 수 있음.
임베디드 타입은 엔티티 타입이 아닌 값 타입이다.
따라서 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않음.
📌 스칼라 타입( 숫자, 문자 등 기본 데이터 타입 ) 프로젝션
Double orderAmountAvg = em.createQuery("SELECT AVG(o.orderAmount) FROM Order o", Double.class).getSingleResult();
DISTINCT - 중복 데이터 제거
📌 여러 값 조회
프로젝션에 여러 값 선택하려면 TypeQuery말고 Query를 사용
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList = query.getResultList();
Iterator iterator = resultList.iterator();
while(iterator.hasNext()) {
Object[] row = (Object[]) iterator.next();
String username = (String) row[0];
Integer age = (Integer) row[1];
}
스칼라 타입 뿐 아니라 엔티티 타입도 여러 값을 함께 조회할 수 있음.
List<Object[]> resultList = em.createQuery("SELECT o.member, o.product, o.orderAmount FROM Order o").getResultList();
for(Object[] row : resultList) {
Member member = (Member) row[0]; // 엔티티
Product product = (Product) row[1]; // 엔티티
int orderAMount = (Integer) row[2]; // 스칼라
}
조회한 엔티티는 영속성 컨텍스트에서 관리됨
📌 New 명령어
NEW 명령어 사용 전
// 객체 변환 작업
List<UserDTO> userDtos = new ArrayList<UserDto>();
for(Object[] row : resultList) {
UserDto userDto = new UserDTO((String) row[0], (Integer)row[1]);
userDtos.add(userDto);
}
return userDtos;
public class UserDTO {
private String username;
private int age;
public UserDTO(String username, int age) {
this.username = username;
this.age = age;
}
}
NEW 명령어 사용 ver
TypedQuery<UserDTO> query = em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m", UserDTO.class);
List<UserDTO> resultList = query.getResultList();
SELECT 뒤에 NEW 명령어를 사용하면 반환받을 클래스를 지정할 수 있음.
이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다.
+ NEW 명령어를 사용한 클래스로 TypeQuery 사용할 수 있어서 지루한 객체 변환 작업을 줄일 수 있음
NEW 명령어 사용 시 주의할 점
- 패키지 명을 포함한 전체 클래스 명을 입력해야함
- 순서와 타입이 일치하는 생성자가 필요
📜 페이징 API
페이징 처리용 SQL은 반복적이며 데이터베이스마다 페이징을 처리하는 SQL문법이 다르다.
JPA는 페이징을 다음 두 API로 추상화한다.
- setFirstResult(int startPosition): 조회 시작 위치
- setMaxResult(int maxResult): 조회할 데이터 수
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class);
query.setFirstResult(10);
query.setMaxResults(20);
query.getResultList();
데이터베이스 방언 덕분에 데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있다. 페이징 API를 더 최적화하고 싶다면 네이티브 SQL을 직접 사용한다.
📜 집합과 정렬
집합은 집합함수와 함께 통계 정보를 구할 때 사용한다.
집합함수
함수설명
COUNT | 결과 수를 구한다. 반환 타입: Long |
MAX, MIN | 최대, 최소 값을 구한다. 문자, 숫자, 날짜 등에 사용한다. |
AVG | 평균값을 구한다. 숫자 타입만 사용할 수 있다. 반환 타입: Double |
SUM | 합을 구한다. 숫자 타입만 사용할 수 있다. 반환 타입: 정수합 Long, 소수합 Double, BigInteger합 BigInteger, BigDecimal합 BigDecimal |
집합 함수 사용 시 참고 사항
NULL 값은 무시하므로 통계에 잡히지 않는다(DISTINCT가 정의되어 있어도 무시된다)
만약 값이 없는데 SUM, AVG, MAX, MIN 함수를 사용하면 NULL 값이 된다. 단 COUNT는 0이 된다.
DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있다.
- 집합 함수 사용 시 참고 사항
- NULL 값은 무시하므로 통계에 잡히지 않는다(DISTINCT가 정의되어 있어도 무시된다)
- 만약 값이 없는데 SUM, AVG, MAX, MIN 함수를 사용하면 NULL 값이 된다. 단 COUNT는 0이 된다.
- DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있다.
- DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.
GROUP BY, HAVING
GROUP BY는 통계 데이터를 구할 때 특정 그룹끼리 묶어준다. HAVING은 GROUP BY와 함께 사용하여 그룹화된 통계 데이터를 기준으로 필터링한다.
select 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)
ORDER BY는 결과를 정렬할 때 사용한다.
- ASC: 오름차순(기본값)
- DESC: 내림차순
select m from Member m order by m.age DESC, m.username ASC
📜 JPQL 조인
내부 조인
INNER JOIN을 사용한다. INNER은 생략가능
String teamName = "팀A";
String query = "SELCT 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();
연관 필드를 사용한다는 점이 가장 큰 특징이다.
- FROM Member m : 회원을 선택하고 m이라는 별칭을 주었다.
- Member m JOIN m.team t : 회원이 가지고 있는 연관 필드로 팀과 조인한다. 조인한 팀에는 t라는 별칭을 주었다.
외부 조인
SELECT m
FROM Member m LEFT [OUTER] JOIN m.team t
OUTER는 생략가능해서 보통 LEFT JOIN이라고 쓴다.
컬렉션 조인
일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것은 컬렉션 조인이라고 한다.
- [회원 -> 팀]으로의 조인은 다대일 조인이면서 단일 값 연관 필드(m.team)를 사용한다.
- [팀 -> 회원]은 반대로 일대다 조인이면서 컬렉션 값 연관 필드(m.members)를 사용한다.
세타 조인
WHERE 절을 사용해서 세타 조인을 할 수 있다. 세타 조인은 내부 조인만 지원한다. 세타 조인을 사용하면 전혀 관계 없는 엔티티도 조인할 수 있다.
// JPQL
select count(m) from Member m, Team t
where m.username = t.name
// SQL
SELECT COUNT(M.ID)
FROM MEMBER M CROSS JOIN TEAM T
WHERE M.USERNAME=T.NAME
전혀 관련없는 Member.usernam과 Team.name을 조인한다.
JOIN ON절(JPA 2.1)
JPA 2.1부터 조인할 때 ON절을 지원한다. ON절을 사용하면 조인 대상을 필터링하고 조인할 수 있다. 내부 조인의 ON절은 WHERE절을 사용할 때와 결과적으로 같아서 보통 ON은 외부 조인에서만 사용한다.
// JPQL
select m, t from Member m
left join m.team t on t.name = 'A'
📜 페치 조인
엔티티 패치 조인
select m from Member m join fetch m.team
회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회한다. 별칭을 사용할 수 없다. 팀 객체가 객체 그래프를 유지하면서, 회원만 선택해서 조회했는데도 같이 조회된다. 조팀 엔티티도 프록시가 아닌 실제 엔티티이므로 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있다.
컬렉션 패치 조인
select t from Team t join fetch t.members where t.name = '팀A'
팀만 선택해도 연관된 회원이 같이 조회된다. 팀 테이블에서 각 팀은 하나씩이지만 회원 테이블과 조인하면서 결과가 증가하되어 여러개의 데이터가 조회된다.
패치 조인과 DISTINCT
JPQL의 DISTINCT는 SQL에 DISTINCT를 추가하여 중복된 결과를 제거하고, 애플리케이션에서 한 번 더 중복을 제거한다.
select distinct t from Team t join fetch t.members where t.name = '팀A'
컬렉션 패치 조인 예제 코드의 결과로 팀A가 중복 조회된 것을 위의 코드로 다시 실행해보면, SQL의 결과에서는 각 로우가 달라서 제거되지 않지만, 애플리케이션에서는 팀을 기준으로 중복을 제거해서 하나만 조회된다.
패치 조인과 일반 조인 차이
JPQL은 결과를 반환할 때 연관관계까지 고려하지 않아서 단지 SELECT절에 지정한 엔티티만을 조회할 뿐이다. 반면에 패치 조인을 사용하면 연관된 엔티티도 함께 조회된다.
패치 조인의 특징과 한계
페치조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다. 패치 조인은 엔티티에 직접 적용하는 로딩 전략, 글로벌 로딩 전략보다 최적화에 효과적이다.
또한 연관된 엔티티를 쿼리 시점에 조회하므로 지연 로딩이 발생하지 않아 준영속 상태에서도 객체 그래프를 탐색할 수 있다.
그러나 다음과 같은 한계가 존재한다.
- 패치 조인 대상에는 별칭을 줄 수 없다.
- 둘 이상의 컬렉션을 패치할 수 없다.
- 컬렉션을 패치 조인하면 페이징 API를 사용할 수 없다.
📜 경로 표현식
.을 찍어 객체 그래프를 탐색하는 것
경로 표현식과 특징
경로
- 상태 필드 : 단순히 값을 저장하기 위한 필드(필드 or 프로퍼티)
- 연관 필드 : 연관관계를 위한 필드, 임베디드 타입 포함(필드 or 프로퍼티)
- 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티
- 컬레션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션
3가지 경로에 따른 특징
- 상태 필드 경로: 경로 탐색의 끝. 더는 탐색할 수 없다.
- 연관 필드 : 묵시적으로 내부 조인이 일어난다.
- 단일 값 연관 필드 : 계속 탐색할 수 있다.
- 컬레션 값 연관 필드 : 더는 탐색할 수 없다. 단, FROM 절에서 별칭을 얻으면 별칭을 사용하여 탐색할 수 있다.
상태 필드 경로 탐색
select m.username, m.age from Member m
단일 값 연관 경로 탐색
select o.member from Order o
단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는데 이것을 묵시적 조인이라 한다. 묵시적 조인은 모두 내부 조인이다.
- 명시적 조인: JOIN을 직접 적어주는 것.
- 묵시적 조인: 경로 표현식에 의해 묵시적으로 조인이 일어나는 것. 내부 조인 INNER JOIN만 할 수 있다.
컬렉션 값 연관 경로 탐색
컬렉션까지는 경로 탐색이 가능하지만 컬렉션에서 경로 탐색을 시작하는 것은 불가능하다. 컬렉션에서 경로 탐색을 하고싶으면 조인을 사용해서 새로운 별칭을 획득해야한다.
select m.username from Team t join t.members m
select t.members.size from Team t
경로 탐색을 사용한 묵시적 조인 시 주의사항
- 항상 내부조인이다.
- 컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야한다.
- 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM 절에 영향을 준다.
📜 서브 쿼리
서브 쿼리 함수
- [NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참. NOT은 반대.
- {ALL | ANY | SOME} (subquery): 비교 연산자와 같이 사용한다.
- ALL: 조건을 모두 만족하면 참.
- ANY 혹은 SOME: 조건을 하나라도 만족하면 참.
- {NOT} IN (suquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참. 서브쿼리가 아닌 곳에서도 사용가능.
📜 조건식
타입 표현
종류설명
문자 | 작은 따옴표 사이에 표현 작은 따옴표를 표현하고 싶으면 작은 따옴표 연속 두개를 사용 |
숫자 | L(Long 타입 지정) D(Double 타입 지정) F(Float 타입 지정) |
날짜 | DATE {d 'yyyy-mm-dd'} TIME {t 'hh-mm-ss'} DATETIME {ts 'yyyy-mm-dd hh:mm:ss.f'} |
Boolean | TRUE, FALSE |
Enum | 패키지명을 포함한 전체 이름을 사용해야 한다. |
엔티티 타입 | 엔티티의 타입을 표현한다. 주로 상속과 관련해서 사용한다. |
연산자 우선 순위
- 경로 탐색 연산: .
- 수학 연산: +, -(단항 연산자), *, /, +, -
- 비교 연산: =, >, =>, <, <=, <>, BETWEEN, LIKE, IN, IS NULL, IS EMPTY, MEMBER, EXISTS
- 논리 연산: AND, OR, NOT
CASE 식
특정 조건에 따라 분기할 때 사용한다.
- 기본 CASE 식
SELECT
CASE WHEN m.age <= 10 THEN '학생요금'
WHEN m.age >= 60 THEN '경로요금'
ELSE '일반요금'
END
FROM Member m
출처: https://dev-meung.tistory.com/entry/자바-ORM-표준-JPA-프로그래밍-기본편-9-객체지향-쿼리-언어1-기본-문법#조건식(CASE 등)과 함수-1 [IT::Coding:티스토리]
- 심플 CASE 식: 조건식을 사용할 수 없지만 문법이 단순하다. switch case문과 비슷하다.
SELECT
CASE t.name
WHEN '팀A' THEN '인센티브110%'
WHEN '팀B' THEN '인센티브120%'
ELSE '인센티브105%'
END
FROM Team t
출처: https://dev-meung.tistory.com/entry/자바-ORM-표준-JPA-프로그래밍-기본편-9-객체지향-쿼리-언어1-기본-문법#조건식(CASE 등)과 함수-1 [IT::Coding:티스토리]
- COALCASE: 하나씩 조회해서 NULL이 아니면 반환한다.
SELECT COALESCE(m.username, '이름 없는 회원') FROM Member m
- NULLIF: 두 값이 같으면 null을 반환하고 다르면 첫 번째 값을 반환한다. 집합함수는 보통 NULL을 포함하지 않으므로 집합함수와 함께 보통 사용한다.
SELECT NULLIF(m.username, '관리자') FROM Member m
📜 다형성 쿼리
TREAT(JPA 2.1)
자바의 타입 캐스팅과 비슷하다. 상속 구조에서 부모 타입을 특정 자식 타임으로 다룰 때 사용한다. JPA에서는 FROM, WHERE에서 다룰 수 있고 하이버네이트에서는 SELECT에서도 다룰 수 있다.
// JPQL
select i from Item i where treat(i as Book).author = 'kim'
// SQL
select i.* from Item i
where i.DTYPE='B' and i.author='kim'
부모 타입인 Item을 자식 타입인Book으로 다룰 수 있게 만들어서 author 필드에 접근할 수 있다.
📜 사용자 정의 함수 호출 (JPA 2.1)
JPA 2.1부터 사용자 정의 함수를 지원한다.
function_invocation::= FUNCTION(function_name {, function_arg}*)
하이버네이트 구현체를 사용하면 데이터베이스 방언을 상속해서 구현하고 사용할 데이터베이스 함수를 미리 등록해야한다.
public class MyH2Dialect extends H2Dialect {
public MyH2Dialect() {
registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
}
}
<property name="hiberante.dialect" value="hello.MyH2Dialect" />
다음과 같이 축약해서 사용할 수도 있다.
select group_concat(i.name) from Item i
📜 기타 정리
- enum은 = 비교 연산만 정리한다.
- 임베디드 타입은 비교를 지원하지 않는다.
EMPTY STRING
JPA 표준은 ''을 길이 0인 Empty String으로 정했지만 데이터베이스에 따라 ''를 NULL로 사용하는 데이터베이스도 있으므로 확인하고 사용해야 한다.
NULL 정의
- 조건을 만족하는 데이터가 하나도 없으면 NULL이다.
- NULL은 알 수 없는 값이다. NULL과의 모든 수학적 계산 결과는 NULL이 된다.
- Null == Null은 알 수 없는 값이다.
- Null is Null은 참이다.
📜 엔티티 직접 사용
기본 키 값
객체 인스턴스는 참조 값으로 식별하고 테이블 로우는 기본 키 값으로 식별한다. 따라서 JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값을 사용한다.
외래 키 값
외래키를 사용하는 예를 보자. 아래는 특정 팀에 소속된 회원을 찾는다.
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();
기본키 값이 1L인 팀 엔티티를 파라미터로 사용하고 있다. m.team은 현재 team_id라는 외래키와 매핑되어 있다. 따라서 아래와 같은 SQL이 실행된다.
select m.*
from Member m
where m.team_id=?
엔티티 대신 아래와 같이 식별자 값을 직접 사용할 수 있다.
String qlString = "select m from Member m where m.team.id = :teamId";
List resultList = em.createQuery(qlString).setParameter("teamId", 1L).getResultList();
m.team.id를 보면 Member와 Team 간에 묵시적 조인이 일어날 것 같지만 MEMBER 테이블이 team_id 외래키를 가지고 있으므로 묵시적 조인은 일어나지 않는다. 물론 m.team.name을 호출하면 묵시적 조인이 일어난다. 따라서 m.team을 사용하든 m.team.id를 사용하든 생성되는 SQL은 같다.
📜 Named 쿼리를 XML에 정의
자바 언어로 멀티라인 문자를 다루는 것은 상당히 귀찮은 일이라서 XML을 사용하는 것이 더 편리하다.
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1">
<named-query name="Member.findByUsername">
<query><![CDATA[
[ select m from Member m where m.username = :username
]>]]>
</query>
</named-query>
<named-query name="Member.count">
<query>select count(m) from Member m</query>
</named-query>
</entity-mappings>
정의한 xml파일을 인식하도록 META-INF/persistence.xml에 다음 코드를 추가해야한다.
<persistence-unit name="jpabook">
<mapping-file>META-INF/ormMember.xml</mapping-file>
환경에 따른 설정
XML과 어노테이션에 같은 설정이 있으면 XML을 따른다. 애플리케이션이 운영 환경에 따라 다른 쿼리를 실행해야한다면 각 환경에 맞춘 XML을 준비해두고 XML만 변경해서 배포하는 것이 편하다.
📖 Criteria
JPQL을 편하게 작성하도록 도와주는 API, 빌더클래스 모음
'study > 웹개발 스터디 백엔드 EFUB 💻' 카테고리의 다른 글
[JPA] 6장 - 다양한 연관관계 매핑 (0) | 2025.04.11 |
---|---|
[JPA] 5장 - 연관관계 매핑 기초 (0) | 2025.04.03 |
[JPA] 4장 - 엔티티 매핑 (0) | 2025.03.30 |
[JPA] 2장 - JPA 시작 (0) | 2025.03.16 |
[JPA] 1장 - JPA 소개 (0) | 2025.03.16 |