2022. 7. 27. 21:09ㆍLanguage`/JPA
일반적으로 SQL에 query를 날리는 방식은 다음과 같다
-- Member 테이블의 모든 정보 가져오기
SELECT * FROM Member;
하지만 JPA에서 생성하는 query는 이와 다르다
- 물론 JPA에서도 native query를 생성해서 DB로 날릴 수 있다 (nativeQuery)
-- Member 테이블의 모든 정보 가져오기
SELECT m FROM Member m
여기서 native Query와 JPA에서 생성하는 query의 차이는 다음과 같다
native query : DB의 테이블을 대상으로 쿼리 생성
JPQL : 엔티티(객체)를 대상으로 쿼리 생성
일반적인 SQL이 DB Table을 대상으로 하는 "데이터 중심의 쿼리"라면 JPQL이란 엔티티 객체를 대상으로 하는 "객체지향 쿼리"이다
JPA에서 활용할 수 있는 다양한 query 형태는 다음과 같다
- JPQL (Java Persistence Query Language)
- Criteria Query
- QueryDSL
- Native Query
- JDBC 직접 사용, SQL Mapper Framework(MyBatis, JdbcTemplate) 사용
여기서 가장 중요한 것은 "JPQL"이다
Criteria나 QueryDSL은 JPQL을 편리하게 작성할 수 있게 도와주는 "빌더 클래스"일 뿐이다.
따라서 JPQL을 반드시 완벽하게 이해하고 나서 이를 편리하게 작성할 수 있도록 도와주는 빌더 클래스를 고려해야 한다
1. JPQL
JPQL은 "엔티티 객체를 대상으로 하는 객체지향 쿼리"이다
JPQL은 SQL을 추상화해서 엔티티를 대상으로 작성하기 때문에 DB에 의존하지 않고 쿼리를 작성할 수 있다
그리고 JPQL의 가장 큰 장점은 다음과 같다
DB Vendor에 의존적이지 않은 추상적인 쿼리 생성
프로젝트상에서 "DB Dialect"만 변경하면 JPQL을 수정하지 않아도 각 DB Vendor에 알맞은 SQL을 생성해서 DB로 날릴 수 있다
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/> // MySQL
<property name="hibernate.dialect" value="org.hibernate.dialect.Oracle12cDialect"/> // Oracle
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQL9Dialect"/> // PostgreSQL
<property name="hibernate.dialect" value="org.hibernate.dialect.MariaDB53Dialect"/> // MariaDB
// MySQL Vendor
List<Member> members = em.createQuery(
"SELECT m FROM Member m WHERE m.username like '김%' ORDER BY m.age DESC",
Member.class
).getResultList();
이러한 JPQL이 DB로 쿼리가 날라갈때 어떤 SQL로 변환되어서 날라가는지 살펴보자
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.team_id as team_id4_0_,
member0_.username as username3_0_
from
member member0_
where
member0_.username like '김%'
order by
member0_.age DESC
2. Criteria Query
Criteria는 "JPQL을 생성하는 빌더 클래스"이다
<장점>
- 프로그래밍 코드로 JPQL 작성
- 컴파일 시점에 오류 발견 가능
- 동적 쿼리 작성의 편리함
<단점>
- 너무 복잡
- 따라서 유지보수성이 굉장히 떨어진다
위에서 작성한 JPQL을 Criteria Query로 작성해보자
// Criteria Builder 생성
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
// 루트 클래스 (조회를 할 대상 클래스)
Root<Member> member = query.from(Member.class);
// 쿼리 생성
CriteriaQuery<Member> cq = query.select(member)
.where(cb.like(member.get("username"), "김%"))
.orderBy(cb.desc(member.get("age")));
List<Member> members = em.createQuery(cq).getResultList();
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.team_id as team_id4_0_,
member0_.username as username3_0_
from
member member0_
where
member0_.username like '김%'
order by
member0_.age DESC
역시 위에서 JPQL의 결과와 동일한 SQL Query가 나감을 알 수 있다
결국 Criteria든 QueryDSL이든 JPQL을 더 편리하게 동적으로 작성할 수 있게 도와주는 빌더 클래스이다
따라서 JPQL을 심도있게 이해하는 것이 가장 중요하다
3. QueryDSL
QueryDSL도 Criteria처럼 JPQL 빌더 역할을 해주는 클래스이다
위의 Criteria 코드를 보면 뭔 코드인지 이해하기도 힘들고 저 코드들이 JPQL로 어떻게 번역될지 상상하기도 힘들다
- Criteria는 유지보수 측면에서 최악이다
QueryDSL은 Criteria처럼 코드 기반이지만 다른점은 굉장히 간단하고 사용하기 쉽고 JPQL로 어떻게 번역될지 상상하기가 굉장히 쉽다
QueryDSL은 JPA 표준이 아닌 "오픈소스 프로젝트"이다
JPA뿐만 아니라 MongoDB, Lucene, ...등도 거의 동일한 문법으로 쿼리를 지원한다
실무에서는 웬만하면 Spring Data JPA + QueryDSL은 필수로 가져가야 하는 스킬이다
QueryDSL을 사용하려면 빌드 자동화 도구(build.gradle, pom.xml)에 다음 라이브러리를 넣어줘야 한다
com.querydsl:querydsl-jpa // QueryDSL JPA 라이브러리
com.querydsl:querydsl-apt:5.0.0:jpa // 쿼리 타입(Q)을 생성할 때 필요한 라이브러리
그리고 프로젝트를 build하면 다음과 같은 도메인 쿼리 타입들이 생성된다
그러면 이제 위에서 작성한 JPQL을 QueryDSL로 새롭게 작성해보자
List<Member> members = query
.select(QMember.member)
.from(QMember.member)
.where(QMember.member.username.like("김%"))
.orderBy(QMember.member.age.desc())
.fetch();
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.team_id as team_id4_0_,
member0_.username as username3_0_
from
member member0_
where
member0_.username like '김%' escape '!'
order by
member0_.age desc
굉장히 코드가 간결하고 Criteria보다 알아보기도 훨씬 쉽다
그리고 QueryDSL의 가장 큰 장점은 "동적 쿼리 생성의 편리함"이다
4. Native Query
JPA에서 Native Query를 사용하려면 EntityManager에서 제공해주는 "createNativeQuery"를 활용하면 된다
SQL에서만 지원하고 JPQL에서는 지원하지 않는 기능을 사용하기 위해서는 어쩔수 없이 Native Query를 DB로 날려야 한다
List<Member> members = em.createNativeQuery(
"SELECT * FROM Member WHERE username LIKE '김%' ORDER BY age DESC",
Member.class
).getResultList();
SELECT
*
FROM
Member
WHERE
username LIKE '김%'
ORDER BY
age DESC
5. JDBC 직접 사용, SQL Mapper Framework
JDBC 커넥션에 직접 접근하고 싶다면 JPA에서는 JDBC Connection을 획득하는 API를 제공하지 않기 때문에 "JPA 구현체(Hibernate)가 제공하는 방법"을 사용해야 한다
1. JPA EntityManager에서 Hibernate가 제공하는 Session 구하기
2. Session의 doWork() 호출
Session session = em.unwrap(Session.class);
session.doWork(con -> {
String sql = "SELECT * FROM Member WHERE username LIKE '김%' ORDER BY age DESC";
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(sql);
...
...
...
// work...
});
SELECT
*
FROM
Member
WHERE
username LIKE '김%'
ORDER BY
age DESC
여기서 JDBC나 MyBatis를 JPA와 함께 사용한다면 "영속성 컨텍스트를 반드시 적절한 시점에 강제 Flush 해야 한다"
JDBC를 직접 사용하든 MyBatis, JdbcTemplate과 같은 SQL Mapper를 사용하든 모두 JPA를 우회해서 DB에 접근한다
하지만 여기서 JPA를 우회하는 SQL에 대해서는 JPA가 전혀 인식하지 못한다
따라서 어떤 쿼리가 날라가는지 인식을 못하고 여기서 날라가는 쿼리는 영속성 컨텍스트를 거치지않고 바로 DB로 날라가기 때문에 잘못되면 DB와 영속성 컨텍스트간 동기화 문제로 인한 데이터 정합성에 문제가 발생할 수 있다
따라서 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 "수동 flush"해서 DB와 영속성 컨텍스트를 동기화하는 작업을 반드시 수행해줘야 한다