JPQL
📌 데이터베이스에 질의(Query)를 실행하기 위해 사용하는 객체지향 쿼리 언어이다. SQL과 유사하지만 데이터베이스 테이블이 아닌 Entity 객체와 필드를 대상으로 작성된다.
JPA의 SQL Query 지원
- JPQL(Java Persistence Query Language)
- 객체지향 쿼리 언어
- Entity 객체를 대상으로 SQL Query를 작성할 수 있도록 도와준다.
- QueryDSL
- Java 기반의 ORM 쿼리 빌더 라이브러리
- 동적 쿼리를 지원한다.
- JPA Criteria
- JPQL과 유사한 쿼리를 코드로 생성할 수 있다.
- 복잡하고 실용성이 없어서 QueryDSL을 사용한다.
- Native SQL
- JPA가 제공하는 SQL을 직접 사용하는 기능
- 표준 SQL이 아닌 Database 종속적인 Query가 필요할 때 주로 사용
JPQL의 등장
- JPA 사용 → 객체 중심적인 설계
- 조회(em.find()) → Entity 객체 그래프 탐색(tutor.getCompany())
- 모든 DB 데이터를 객체로 변환해서 조회하는 경우는 사용이 불가능
- 특정 조건을 가진 데이터를 검색할 수 없다.
- 실제로 필요한 데이터를 조회하기 위한 조건이 포함된 SQL이 필요하다.
- 예시코드
String jpql = "SELECT p FROM Product p WHERE p.price > :minPrice";
List<Product> products = em.createQuery(jpql, Product.class)
.setParameter("minPrice", 1000)
.getResultList();
- SELECT p FROM Product p (SQL의 SELECT * FROM product와 대응)
- 최소 가격(minPrice) 이상의 Product를 조회하는 SQL Query
- JPQL 특징
- 객체를 대상으로 검색하는 객체 지향 쿼리(테이블 X)
- SQL 추상화(DB 종속 X)
- 다양한 데이터베이스에서 사용이 가능하다.
- JPA의 영속성 컨텍스트를 사용하여 1차 캐시, 지연 로딩 등의 기능을 활용할 수 있다.
- 타입 안정성
- Compile 시점에 Entity와 필드의 타입을 확인하여 오류를 줄인다.
동적 쿼리
📌 실행 시점에 특정 조건이나 값에 따라 변화하는 SQL Query를 작성하고 실행하는 방식이다.
String jpql = "SELECT p FROM Product p WHERE 1=1";
if (name != null) {
jpql += " AND p.name = :name";
}
if (price != null) {
jpql += " AND p.price = :price";
}
- 특정 조건에 따라서 실행되는 SQL이 변한다.
- JPQL 동적 쿼리
- JPQL은 정적 쿼리에 최적화 되어있다.
- JPQL은 문자열 SQL을 직접 다루어야 하기때문에 문자열을 동적으로 조합해야 한다.
- 가독성과 유지보수가 어려워지고 다루기 까다롭다.
- 오타나 문법 오류가 발생하기 쉽고 컴파일 시점에 검증되지 못한다.
- 조건이 많아지면 많아질수록 JPQL을 사용하는것은 한계가 존재한다.
기본 문법
📌 JPQL은 Entity 객체를 대상으로 SQL Query를 생성한다.
JPQL 문법 규칙
- 테이블 이름이 아닌 Entity 이름을 사용한다.(클래스 이름이 Default)
- Entity와 필드는 대소문자를 구분한다.
- JPQL 키워드(SELECT, from, Where)는 대소문자를 구분하지 않는다.
- 별칭(alias)은 필수이고 as 는 생략이 가능하다.
SELECT <별칭> FROM <엔티티 이름> [AS <별칭>] [WHERE 조건] [GROUP BY 속성] [HAVING 조건] [ORDER BY 속성]
- update, delete 의 사용도 가능하다.
- GROUP BY, HAVING, ORDER BY 등의 집합과 정렬 기능을 사용할 수 있다.
select
SUM(p.price), // 상품 가격의 총합
AVG(p.price), // 평균 가격
MAX(p.price), // 최대 가격
MIN(p.price) // 최소 가격
COUNT(p), // 상품 수
from Product p
반환 타입 및 결과
📌 JPQL에서 데이터베이스 쿼리를 실행하기 위해 Query와 TypedQuery 두 가지 타입을 제공하며 이들은 JPA의 쿼리 API로 작성된 쿼리를 실행하고 결과를 반환받는 데 사용된다.
- 결과 조회
- getResultList()
- 결과가 하나 이상일 때 사용한다.
- List 반환
- 결과가 없다면 빈 List 반환
- 코드 예시
- getResultList()
List resultList = em.createQuery("select t from Tutor t").getResultList();
- getSingleResult()
- 결과가 딱 하나일 때 사용한다.
- 결과가 없거나 여러개라면 예외 발생
- NoResultException, NonUniqueResultException
- 코드 예시
Tutor singleResult = em.createQuery("select t from Tutor t where t.id = 1L", Tutor.class).getSingleResult();
파라미터 바인딩
📌 파라미터 바인딩은 쿼리에 동적으로 값을 전달하여 SQL 인젝션을 방지하고 쿼리를 재사용할 수 있도록 돕는 기능이다.
Entity 필드 이름
- 코드 예시
Tutor wonuk = em.createQuery("select t from Tutor t where t.name = :name", Tutor.class)
.setParameter("name", "wonuk")
.getSingleResult();
System.out.println("wonuk.getName() = " + wonuk.getName());
System.out.println("wonuk.getAge() = " + wonuk.getAge());
- 메서드 체이닝 사용가능
- 실행결과
파라미터 바인딩 순서
- 코드 예시
Tutor wonuk2 = em.createQuery("select t from Tutor t where t.age = ?1", Tutor.class)
.setParameter(1, 100)
.getSingleResult();
- 중간에 순서가 바뀌면 모두 수정이 필요하다.
- 사용하지 않는다.
Embedded Type
📌 JPA는 새로운 값 타입을 @Embedded , @Embeddable 을 사용하여 직접 정의해서 사용할 수 있고 이것을 임베디드 타입이라 한다. 직접 정의한 임베디드 타입도 int, String과 같은 값 타입이다.
@Entity
public class Tutor {
@Id
@GeneratedValue
private Long id;
private String name;
// Embedded 사용
@Embedded
private Period workPeriod;
}
// Embedded 정의
@Embeddable
public class Period {
@Temporal(TemporalType.DATE)
Date startDate;
@Temporal(TemporalType.Date)
Date endDate;
public boolean isWork (Date date) {
// startDate <= date <= endDate 확인
return (startDate == null || !date.before(startDate)) &&
(endDate == null || !date.after(endDate));
}
}
위 표를 보면 이해가간다. 그러니까 DB에는 startDate,endDate가 존재하지만, 자바 객체로 불러올 당시에 startDate와 endDate를 합쳐서 만든 Period라는 객체를, workPeriod라는 하나의 필드로 관리한다.
요는 코드 가독성의 향상을 위해 보이는 데이터를 압축시키는 것, 당연히 workPeriod가 매핑된게 아니라 매핑된 2개를 가져와서 객체로 만들고 하나의 필드로 관리할 뿐이다.
위 코드는 JPA(Java Persistence API)에서 **새로운 값 타입(Value Type)**을 정의하고 사용하는 예제입니다.
값 타입(Value Type)이란?
값 타입은 JPA에서 **엔티티(Entity)**와는 달리 데이터베이스에서 별도의 테이블로 관리되지 않고, 엔티티 내부에 포함되어 하나의 컬럼 또는 컬럼 그룹으로 관리되는 객체입니다.
이 코드에서 정의한 것
- 새로운 값 타입 Period 정의:
- Period는 특정 **기간(Start Date, End Date)**을 나타내는 값 타입으로, @Embeddable 어노테이션을 사용해 정의되었습니다.
- Period는 별도의 테이블을 생성하지 않고, 엔티티(Tutor) 내부에 포함됩니다.
- Period의 필드(startDate, endDate)는 Tutor 테이블의 컬럼으로 매핑됩니다.
- Tutor 엔티티와 Period 값 타입 통합:
- Tutor는 @Embedded를 사용해 Period 값을 포함하고, workPeriod 필드를 통해 기간 정보를 관리합니다.
- Tutor 테이블은 Period의 startDate와 endDate 필드를 컬럼으로 생성합니다.
값 타입의 역할
- 재사용 가능한 구조:
- Period는 여러 엔티티에서 재사용 가능한 값 타입입니다.
- 예를 들어, Employee, Project 등 다양한 엔티티에서 사용 가능.
- 비즈니스 로직 캡슐화:
- isWork 메서드와 같이 값 타입 내부에 비즈니스 로직을 포함할 수 있습니다.
- 예: 특정 날짜가 Period의 범위에 포함되는지 확인.
- 테이블 설계 단순화:
- 데이터베이스 테이블에서는 startDate, endDate 컬럼을 Tutor 테이블에 포함시키며, 별도의 테이블로 분리되지 않습니다.
데이터베이스 매핑 결과
위 코드는 다음과 같은 데이터베이스 테이블로 매핑됩니다:
Tutor 테이블
id name workPeriod_startDate workPeriod_endDate1 | John | 2023-01-01 | 2023-12-31 |
2 | Jane | 2022-06-01 | NULL |
코드 동작
값 타입 Period 정의
@Embeddable
public class Period {
@Temporal(TemporalType.DATE)
Date startDate;
@Temporal(TemporalType.DATE)
Date endDate;
// 특정 날짜가 해당 기간에 포함되는지 확인하는 메서드
public boolean isWork(Date date) {
return (startDate == null || !date.before(startDate)) &&
(endDate == null || !date.after(endDate));
}
}
- @Embeddable:
- 이 클래스는 값 타입으로 사용되며, 독립적으로 엔티티가 될 수 없습니다.
- JPA가 이 클래스의 필드를 데이터베이스 컬럼으로 매핑합니다.
- @Temporal(TemporalType.DATE):
- 날짜 타입 필드를 데이터베이스의 DATE 타입으로 매핑.
엔티티 Tutor 정의
@Entity
public class Tutor {
@Id
@GeneratedValue
private Long id;
private String name;
@Embedded
private Period workPeriod;
}
- @Entity:
- Tutor는 데이터베이스 테이블과 매핑되는 엔티티입니다.
- @Embedded:
- workPeriod 필드가 Period 클래스임을 나타냅니다.
- Period의 필드(startDate, endDate)는 Tutor 테이블의 컬럼으로 매핑됩니다.
값 타입의 장점
- 중복 코드 제거:
- startDate와 endDate를 여러 엔티티에서 사용하는 경우, Period 클래스를 정의해 중복을 줄일 수 있습니다.
- 캡슐화:
- 값 타입 내부에 비즈니스 로직(예: isWork)을 정의하여 관련 로직을 한 곳에 집중.
- 재사용성:
- 동일한 값 타입을 다른 엔티티에서 쉽게 재사용 가능.
- 테이블 설계 단순화:
- 값 타입의 필드는 엔티티의 테이블에 컬럼으로 포함되어 별도의 조인 없이 사용 가능.
값 타입 사용 예
Tutor 객체 생성
Tutor tutor = new Tutor();
tutor.setName("John");
// Period 값 생성
Period period = new Period();
period.setStartDate(Date.valueOf("2023-01-01"));
period.setEndDate(Date.valueOf("2023-12-31"));
// Tutor에 값 타입 설정
tutor.setWorkPeriod(period);
entityManager.persist(tutor);
데이터 저장
INSERT INTO Tutor (id, name, workPeriod_startDate, workPeriod_endDate)
VALUES (1, 'John', '2023-01-01', '2023-12-31');
데이터 조회
Tutor tutor = entityManager.find(Tutor.class, 1L);
Period period = tutor.getWorkPeriod();
System.out.println(period.getStartDate()); // 2023-01-01
System.out.println(period.getEndDate()); // 2023-12-31
결론
이 코드에서 정의한 것은:
- 값 타입 Period:
- 특정 기간(Start Date, End Date)을 표현하기 위한 값 타입.
- 값 타입은 독립적으로 엔티티가 될 수 없으며, 다른 엔티티에 포함되어 사용됩니다.
- Tutor 엔티티:
- JPA에서 Tutor는 Period 값을 포함하고, 이를 workPeriod라는 필드로 관리합니다.
- 데이터베이스에는 startDate와 endDate 컬럼으로 매핑됩니다.
값 타입은 코드 재사용성을 높이고 테이블 설계를 단순화하는 데 유용한 JPA의 기능입니다.
프로젝션
📌 Entity 전체가 아닌 특정 필드만 선택하여 조회하는 방식으로 필요한 데이터만 조회하여 성능을 최적화하고 네트워크 비용을 줄일 수 있다.
- SELECT 문에 조회할 대상(Entity, Embedded, Scala)을 지정할 수 있다.
- DISTINCT 중복 제거가 가능하다.
- 사용 예시
SELECT t FROM Tutor t // Entity
SELECT t.company FROM Tutor t // Entity
SELECT t.period // Embedded
SELECT t.name, t.age FROM Tutor t // Scala
SELECT DISTINCT t.name, t.age FROM Tutor t // 중복 제거
Entity 프로젝션
Tutor tutor = new Tutor("wonuk", 100);
em.persist(tutor);
// 영속성 컨텍스트 초기화
em.flush();
em.clear();
List<Tutor> tutorList = em.createQuery("select t from Tutor t", Tutor.class).getResultList();
Tutor wonuk = tutorList.get(0);
wonuk.setName("wonuk2");
// 실제로는 다른 방식을 사용한다. -> 묵시적 JOIN
Company company = em.createQuery("select t.company from Tutor t", Company.class).getSingleResult();
// join을 명시적으로 사용 -> 명시적 JOIN
Company companyV2 = em.createQuery("select t from Tutor t join t.company", Company.class).getSingleResult();
- Entity 프로젝션을 사용하면 영속성 컨텍스트가 관리한다.
- 연관된 Entity를 JOIN하여 조회할 수 있다.
Embedded 프로젝션
em.createQuery("select t.period from Tutor t", Period.class).getResultList();
- 실행결과
- 나머지 코드 주석 필요
- select period from Period p 는 불가능하다.
- select t from Tutor t where t.period.startDate < ? Embedded
- select t from Tutor t where t.startDate < ? 상속(@MappedSuperclass)
- 상속을 사용하는것이 편리하고 직관적이다.
Scala 프로젝션
List resultList = em.createQuery("select t.name, t.age from Tutor t").getResultList();
Object o = resultList.get(0);
Object[] result = (Object[]) o;
System.out.println("result[0] = " + result[0]);
System.out.println("result[1] = " + result[1]);
List<Object[]> resultList = em.createQuery("select t.name, t.age from Tutor t").getResultList();
Object[] result = resultList.get(0);
System.out.println("result[0] = " + result[0]);
System.out.println("result[1] = " + result[1]);
- 실행결과
- 나머지 코드 주석 필요
Paging
📌 데이터베이스의 쿼리 결과를 특정 범위로 제한하는 기능으로 대량의 데이터 중에서 원하는 페이지의 데이터만 조회하기 위해 사용된다.
- JPQL Paging
- setFirstResult(int startPosition)
- 조회 시작 위치
- setMaxResult(int maxResult)
- 조회할 데이터 수
- 코드 예시
- setFirstResult(int startPosition)
List<Tutor> tutorList = em.createQuery("select t from Tutor t order by t.age desc", Tutor.class)
.setFirstResult(5)
.setMaxResults(10)
.getResultList();
for (Tutor tutor : tutorList) {
System.out.println("tutor = " + tutor.getId() + ", " + tutor.getName() + ", " + tutor.getAge());
}
- 나이순으로 내림차순(큰 값에서 작은 값으로) 정렬
- 실행결과
JOIN
📌 JPQL의 JOIN은 대상을 필터링(ON) 하거나 연관관계가 없는 Entity와 JOIN이 가능하다.
- Hibernate 5.1 Version 이상에서 연관관계가 없는 Entity와 JOIN이 가능하도록 지원한다.
- JOIN ON
- 연관관계 없는 Entity와 JOIN
- 코드 예시
String query = "select t from Tutor t left join Subject s on t.name = s.name";
List<Tutor> tutorList = em.createQuery(query, Tutor.class).getResultList();
Tutor findTutor = tutorList.get(0);
System.out.println("findTutor.getAge() = " + findTutor.getAge());
CASE
📌 JPQL에서 CASE 표현식은 조건에 따라 다른 값을 반환할 때 사용된다. SQL의 CASE 구문과 유사하며 주로 조건에 따라 계산된 값을 반환하거나 쿼리 결과를 조정하는 데 사용한다
- 조건식 CASE
- 코드 예시
- 문자열 SQL 띄어쓰기 유의
String query =
"select" +
" case when t.age < 10 then '어린이'" +
" when t.age >= 80 then '노약자'" +
" else '청년'" +
" end " +
"from Tutor t";
List<String> resultList = em.createQuery(query, String.class).getResultList();
for (String range : resultList) {
System.out.println("range = " + range);
}
함수
📌 JPQL은 표준 SQL과 유사하게 사용할 수 있는 다양한 내장 함수들을 제공한다. 이 함수들은 문자열 처리, 숫자 계산, 날짜/시간 연산 등을 수행할 때 유용합니다.
- JPQL은 사용자 정의 함수도 함께 지원한다.
- 문자열 함수
- CONCAT('a', 'b')
- a, b 문자열을 더한다.
- SUBSTRING('abcde', 2, 3)
- 두번째 글자부터 세글자 문자열을 자른다.
- TRIM(' a ')
- 문자열 양쪽 공백제거
- LOWER('A'), UPPER('a')
- 각각 문자열을 소문자, 대문자로 변환한다.
- LENGTH('abcde')
- 문자열의 길이를 반환한다.
- CONCAT('a', 'b')
- 숫자 함수
- ABS(-100)
- 절대값을 반환한다.
- MOD(100, 2)
- 나머지를 계산한다.
- SIZE(t.company)
- 연관된 컬렉션의 크기를 반환한다.
- ABS(-100)
Tutor tutor = new Tutor("wonuk", 100);
em.persist(tutor);
ArrayList<String> resultList = new ArrayList<>();
String concatQuery = "select concat(t.name, ' is good') from Tutor t";
List<String> concatResult = em.createQuery(concatQuery, String.class).getResultList();
resultList.add(concatResult.get(0));
String substringQuery = "select substring(t.name, 2, 3) from Tutor t";
List<String> substringResult = em.createQuery(substringQuery, String.class).getResultList();
resultList.add(substringResult.get(0));
Tutor tutor2 = new Tutor(" wonukgap ", 101);
em.persist(tutor2);
String trimQuery = "select trim(t.name) from Tutor t";
List<String> trimResult = em.createQuery(trimQuery, String.class).getResultList();
resultList.add(trimResult.get(1));
String upperQuery = "select upper(t.name) from Tutor t";
List<String> upperResult = em.createQuery(upperQuery, String.class).getResultList();
resultList.add(upperResult.get(0));
String lengthQuery = "select length(t.name) from Tutor t";
List<Integer> lengthResult = em.createQuery(lengthQuery, Integer.class).getResultList();
resultList.add(lengthResult.get(0).toString());
for (String result : resultList) {
System.out.println("result = " + result);
}