반응형
JPQL: Java Persistence Query Language
`em.find()` 메서드는 엔티티의 기본키를 이용하여 데이터를 가져오는 방법입니다. 하지만 `em.find()` 메서드만으로는 더 복잡한 검색 조건이나 특정 필드 기반의 조회 등의 다양한 쿼리를 작성하기는 어렵습니다. 이러한 처리를 위해 SQL과 같은 유연한 처리를 위해 JPQL를 제공합니다.
JPQL은 다음과 같은 특징을 갖고 있습니다.
- 객체 지향적 쿼리 작성:
SQL: 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리
JPQL: Entity 객체를 대상으로 하는 객체지향 쿼리 - 데이터 베이스의 독립성:
JPQL은 SQL 문법을 추상화하였기 때문에 데이터베이스 독립성을 제공합니다. - 복잡한 쿼리 작성 용이:
기존 JPA와는 다르게 WHERE 절을 통해 다양한 검색조건을 표현할 수 있고, JOIN등을 이용하여 복잡한 쿼리도 작성할 수 있습니다. SQL과 매우 유사하기 때문에 쉽게 익힐 수 있습니다. 또한 JPQL을 사용하면 JPA는JPQL을 분석하여적절한 SQL을 만들어 쿼리를 실행합니다.
💁♂️JPQL 외 JPA 다양한 쿼리 방법들
JPQL은 객체 지향적인 쿼리 작성을 할 수 있고, 기존 JPA 문법에 비해 다양한 쿼리를 작성할 수 있습니다. 모든 상황에 적합하진 않았습니다.(복잡한 조인이나 집계 함수 등) 그래서 더 나은 편리성과 성능을 제공하기 위해 다양한데이터 조작 방법이 있습니다.
Criteria API:
JPA에서 제공하는 Criteria API JPQL을 편하게 작성하도록 도와주는 API입니다. Java 코드를 이용하여 타입 안전한 쿼리를 생성하는 기능을 제공하여, 동적인 쿼리 작성을 더 편리하게 할 수 있게 해줍니다. 복잡하고 실용성이 부족하여 QueryDSL 사용을 권장합니다.
QueryDSL:
외부 라이브러리인 QueryDSL은 JPQL의 더욱 편리한 확장으로써 JPQL을 편하게 작성하도록 도와줍니다. 타입 세이프한 쿼리를 작성할 수 있게 해줍니다. JPQL보다 더 간결하고 가독성 있는 코드를 작성할 수 있으며, 코드 생성 도구를 통해 컴파일 시점에서 오류를 발견할 수 있습니다.
네이티브 SQL:
네이티브 SQL은 JPA에서 공식 지원합니다. 특정 데이터베이스 벤더의 기능을 활용하거나 복잡한 쿼리를 작성할 때 JPA에서 직접 SQL을 사용할 수 있습니다. 하지만 DBMS에 종속적이여서, JPA의 이점을 제한적으로 활용할 수 있습니다.
MyBatis:
SQL Mapper 프레임워인 MyBatis는 SQL을 XML 파일에 작성하고, 자바 코드에 해당 SQL을 호출하여 데이터베이스를 조작합니다. 네이티브SQL과 마찬가지로 특정 데이터베이스 벤더의기능을 활용하거나 복잡한 쿼리를 작성할 때 용이하지만 JPA의 이점을 깎아 먹습니다.
※ SQL Mapper 프레임워크나 JDBC와 같은 수동적인 데이터베이스 접근 방식을 선택할 경우, Java Persistence API (JPA)를 우회하여 데이터베이스에 접근하게 됩니다. 이러한 경우, SQL을 실행하기 전에 명시적으로 영속성 컨텍스트를 갱신하고 데이터베이스와의 일관성을 유지하기 위해 영속성 컨텍스트를 수동으로 동기화해야 합니다.
스프링의 AOP를 활용하면, 이러한 작업을 보다 편리하게 처리할 수 있습니다. AOP를 활용하면 데이터베이스에 접근하는 메서드에 접근할 때마다 자동으로 영속성 컨텍스트를 플러시하는 로직을 적용할 수 있습니다.
JPQL 사용 방법(문법)
- `SELECT`, `FROM`과 같은 키워드는 대소문자를 구분하지 않습니다.
- 엔티티와 속성은 대소문자를 구분합니다.
- 엔티티 이름을 사용하며, 테이블 이름을 사용하지 않습니다.
- 별칭(alias)은 필수입니다.
기본적으로 SQL 문법과 비슷합니다. (SELECT, UPDATE, DELETE)
INSERT 문은 없습니다. → EntityManager.persist() 메서드를 사용해야합니다.
SELECT [DISTINCT] <프로젝션 대상>
FROM <엔티티명> [별칭]
[WHERE <조건식>]
[ORDER BY <정렬식>]
UPDATE_절
[WHERE <조건식>]
DELETE_절
[WHERE <조건식>]
- <프로젝션>: 조회할 대상을 지정하는 것으로 엔티티, 임베디드 타입, 스칼라 타입을 지정할 수 있습니다.
- <엔티티명>: JPQL에서는 엔티티명을 사용하며, SQL의 테이블명 대신 사용합니다.
- [별칭]: 별칭은 필수입니다. SQL에서 전체를 의미하는 '*'대신 JPQL에서는 별칭을 사용해주어야 합니다.
TypeQuery와 Query 객체:
TypeQuery와 Query 객체는 반환 타입에 따라 선택하여 사용합니다.
TypeQuery:
- 반환 타입이 명확할 때
TypeQuery<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);
}
Query:
- 반환 타입이 명확하지 않은 경우
- 조회 대상이 하나인 경우 Object 반환
- 조회 대상이 둘 이상인 경우 Object[] 반환
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List<Member> resultList = query.getResultList();
for(Object object : resultList) {
Object[] result = (Object[]) object;
System.out.println("이름: " + result[0]);
System.out.println("나이: " + result[1]);
}
파라미터 바인딩:
이름 기준 파라미터 바인딩은 위치 기준 파라미터 바인딩보다 권장됩니다. 이 방식이 더 명확하고 SQL 인젝션방지와 재사용성 향상에 도움을 주기 때문입니다.
// 1. 이름 기준 파라미터 바인딩
TypeQuery<Member> query =
em.createQuery("select m from Member m where m.username = :username", Member.class);
query.setParameter("username", param); // 바인딩
// 2. 위치 기준 파라미터 바인딩
TypeQuery<Member> query =
em.createQuery("select m from Member m where m.username = ?1", Member.class);
query.setParameter(1, param); // 바인딩
결과 조회:
// 리스트 반환, 결과가 없으면 빈 리스트 반환
query.getResultList()
// 단일 객체 반환, 결과가 2개 이상인 경우 예외 발생
query.getSingleResult()
JPQL API 메서드 체인 방식 가능:
List<Member> members =
em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", param)
.getResultList();
페치 조인(fetch join):
- 페치 조인은 연관 엔티티를 한 번에 조회하여 성능을 최적화하는 방법입니다. 페치 조인을 사용하면 N+1 쿼리 문제를 방지하고 성능 향상을 끌어낼 수 있습니다.
- join문에 fetch(지연 로딩으로 설정된 연관관계에 대해서 같이 즉시로딩 해줌)를 걸어주는 것
- 페치 조인은 기본적으로 `INNER JOIN`을 사용하여 연관된 엔티티를 로드합니다. 때문에 일대다 관계에서 페치 조인은 중복 데이터가 존재할 수 있기 때문에 `DISTINCT`를 이용하여 중복데이터를 제거해주어야 합니다.
// 엔티티 프로젝션
SELECT m FROM Member m
// 엔티티 프로젝션 (페치 조인)
SELECT m FROM Member m JOIN FETCH m.orders
// DISTINCT 적용
SELECT DISTINCT m FROM Member m JOIN FETCH m.orders
예제. member → order의 `일대다` 조인
MEMBER ORDER ID NAME ID NAME MEMBER_ID 1 회원1 1 주문1 1 2 회원2 2 주문2 1 3 회원3 3 주문3 2 4 주문4 3
member와 order를 조인하면 관계형 데이터베이스의 조인 결과는 다음과 같이 나옵니다.
1. 회원1 | 주문1
2. 회원1 | 주문2
3. 회원2 | 주문3
4. 회원3 | 주문4
조인을 하였을 때 회원1은 주문1과 주문2를 갖고 있기 때문에 데이터 ROW가 한 개가 아닌 여러 개가 나오게 됩니다. 이때 회원1에는 주문1과 주문2가 있기 때문에 JPA는 이를 객체스럽게 변경해주어 다음과 같은 객체 그래프를 생성합니다.
1. 회원1 | 주문1, 주문2
2. 회원1 | 주문1, 주문2
3. 회원2 | 주문3
4. 회원3 | 주문4
1과 2의 데이터가 완전 동일한 데이터가 만들어지게 되고, 반환된 두 개의 데이터(객체)는 같은 주소 값을 가지게됩니다.
JPQL에서 불필요한 중복을 제거하려면 SELECT DISTINCT을 주면 됩니다. DISTINCT를 추가하면 JPA는 SQL에 distinct 옵션도 추가하지만, 위 처럼 객체 그래프 상에서 완전히 동일한 1,2 번의 중복도 제거합니다. 따라서 JPQL을 사용할 때 DISTINCT 옵션을 주면 다음과 같은 결과를 얻을 수 있습니다.
1. 회원1 | 주문1, 주문2
2. 회원2 | 주문3
3. 회원3 | 주문4
페이징 API와 조인:
페이징 API를 사용하면 데이터베이스마다 다른 페이징 SQL 문법을 추상화하여 사용할 수 있습니다.
조인은 내부 조인, 외부 조인, 컬렉션 조인, 세타 조인 등이 가능합니다.
// 페이징 API 사용
String jpqlQuery = "SELECT m FROM Member m ORDER BY m.name DESC";
List<Member> resultList = em.createQuery(jpqlQuery, Member.class)
.setFirstResult(10) // 조회 시작 위치 (0부터)
.setMaxResults(20) // 조회할 데이터 수
.getResultList();
JPQL 동적 쿼리 vs 정적 쿼리(Named Query):
동적 쿼리는 런타임에 조건에따라 JPQL을 동적으로 구성하며, 정적 쿼리는 미리 정의된 쿼리에 이름을 부여하여 사용합니다. 2개 이상 정의하고 싶은 경우 `@NamedQueries` 어노테이션 내에 `@NamedQuery`를 여러 작성해주면 됩니다.
// 동적 쿼리 생성
String username = ...;
String jpqlQuery = "SELECT m FROM Member m WHERE m.username = :username";
TypedQuery<Member> query = em.createQuery(jpqlQuery, Member.class);
query.setParameter("username", username);
// 정적 쿼리(Named Query)
@Entity
@NamedQuery(
name = "Member.findAdults",
query = "SELECT m FROM Member m WHERE m.age >= 18"
)
public class Member {
// ...
}
// 정적 쿼리 사용
TypedQuery<Member> query = em.createNamedQuery("Member.findAdults", Member.class);
List<Member> adults = query.getResultList();
주의사항과 팁:
- JPQL로 DB에서 조회한 엔티티가 영속성 컨텍스트에 있다면, JPQL 실행 전에 영속성 컨텍스트를 플러시하여 동기화해야 합니다.
- `em.find`는 영속성 컨텍스트에 데이터가 있을 경우 DB 조회를 생략하므로 효율적입니다.
- 페치 조인 대상에는 별칭을 줄 수 없고, 둘 이상의 컬렉션을 페치하는 것은 지원하지 않습니다.
- 사용자 정의 함수를 호출하려면 하이버네이트 방언에 추가하여 사용할 수 있습니다.
반응형
'Spring Data > JPA' 카테고리의 다른 글
JPA Entity의 기본 생성자가 필수인 이유 (0) | 2023.08.30 |
---|---|
JPA Spring Data JPA: 스프링 프레임워크에서 JPA 편리하게 사용하기 (0) | 2023.08.25 |
JPA '값 타입'의 선언과 활용, @AttributeOverride와 @ElementCollection, @CollectionTable 역할 (1) | 2023.08.23 |
JPA 영속성 전이(CASCADE)와 고아 객체 제거: 데이터 일관성과 관리 효율 높이는 전략(방법) (0) | 2023.08.23 |
JPA 프록시와 지연로딩: 성능 최적화를 위한 기술(방법) (0) | 2023.08.22 |