[JPA] 객체지향 쿼리 언어(Query DSL)
Updated:
들어가며
해당 글은 자바 ORM 표준 프로그래밍을 정리한 글입니다.
JPA에서 사용할 수 있는 객체지향 쿼리 종류에 대해 소개하고 Query DSL에서 어떤 식으로 쿼리를 사용하는지 알아본다.
객체 지향 쿼리 소개
JPQL
- 엔티티 객체를 조회하는 객체지향 쿼리이다.
- JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다.
- SQL 보다 간결하다.
- 오타가 발생시에 컴파일 시점에 알 수가 없다.
Criteria Query
- Criteria의 장점은 문자가 아닌 query.select(m).where(…) 처럼 프로그래밍 코드로 JPQL을 작성할 수 있다.
- 컴파일 시점에 오류를 발견할 수 있다.
- IDE를 사용하면 코드 자동완성을 지원한다.
- 동적 쿼리를 작성하기 편하다.
- 사용하기 불편하고, 작성한 코드도 한눈에 들어오지 않는 단점이 있다.
Native SQL
- 데이터베이스에 의존하는 기능을 사용해야 할 때 사용 가능하지만 데이터베이스를 변경하게 되면 네이티브 SQL도 변경해야 한다.
- em.createNativeQuery()를 사용한다.
Query DSL
- 코드 기반이면서 단순하고 사용하기 쉽다.
- 어노테이션 프로세서를 사용해서 쿼리 전용 클래스를 만들어야 한다.
- JDBC 직접 사용, Mybatis SQL 매퍼 프레임워크 사용
- JDBC나 마이바티스를 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야 한다.
- JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 데이터베이스와 영속성 컨텍스트를 동기화하면 된다.
Query DSL
문자가 아닌 코드로 작성하면서 쉽고 간결하며 그 모양도 쿼리와 비슷하게 개발할 수 있는 오픈 소스 프로젝트이다. Query DSL을 사용하면 Q로 시작하는 쿼리 타입의 엔티티를 생성해야 한다.
셋팅
검색 조건 쿼리
@Transactional
@AutoConfigureTestEntityManager
@SpringBootTest
class UserServiceTest {
@Autowired
private TestEntityManager em;
@Test
void queryDslTest() {
User user = new User();
em.persist(user); em.flush();
JPAQuery<User> query = new JPAQuery(em.getEntityManager());
query.from(QUser.user)
.where(QUser.user.id.eq(user.getId()));
User actual = query.fetchOne();
assertThat(actual).isNotNull();
}
}
Hibernate:
select
user0_.id as id1_10_
from
user user0_
where
user0_.id=?
결과 조회
- fetchOne()
- 조회 결과가 한 건일 때 사용한다. 조회 결과가 없으면 null을 반환하고 결과가 하나 이상이면 NonUniqueResultException 예외가 발생한다.
- fetch()
- 결과가 하나 이상일 때 사용한다. 결과가 없으면 빈 컬렉션을 반환한다.
- fetchResults()
- 전체 데이터 조회를 위한 count 쿼리가 한 번 더 실행하고, QueryResults 반환하는데 이 객체에서 전체 데이터 수를 조회 할 수 있다.
- fetchCount()
- 해당 쿼리의 count를 구할 때 사용한다.
페이징과 정렬
- orderBy
- 해당 키워드를 사용해 정렬
JPAQuery<User> query = new JPAQuery(em.getEntityManager());
query.from(QUser.user)
.where(QUser.user.id.eq(1L))
.orderBy(QUser.user.id.desc());
- offset, limit
- 해당 키워드를 사용해 제한적인 데이터를 가져온다.
JPAQuery<User> query = new JPAQuery(em.getEntityManager());
query.from(QUser.user)
.where(QUser.user.id.eq(1L))
.limit(100)
.offset(0)
.orderBy(QUser.user.id.desc());
- restrict()
- QueryModifiers 파라메터를 이용해서 offset, limit을 대체할 수 있다.
JPAQuery<User> query = new JPAQuery(em.getEntityManager());
query.from(QUser.user)
.restrict(new QueryModifiers(100L, 0L));
QueryResults<User> result = query.fetchResults();
그룹
- groupby을 사용하고 그룹화된 결과를 제한하려면 having을 사용 하면 된다.
QUser user = QUser.user;
query.from(user)
.groupBy(user.name, user.id)
.having(user.price.sum().gt(10));
조인
- JPQL에서도 마찬가지지만 해당 엔티티의 참조 객체를 사용해서 join을 해야 올바른 문법으로 인식한다.
- innerJoin(join), leftJoin, rightJoin, fullJoin을 사용할 수 있고 성능 최적화를 위한 fetch 조인도 사용할 수 있다.
- 조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭으로 사용할 쿼리 타입을 지정하면 된다.
@Test
void queryDslJoinTest() {
User user = new User();
UserInventory userInventory = new UserInventory();
user.setUserInventory(userInventory);
em.persist(user);
em.persist(userInventory);
em.flush();
JPAQuery<User> query = new JPAQuery(em.getEntityManager());
QUser qUser = QUser.user;
QUserInventory qUserInventory = QUserInventory.userInventory;
query.from(qUser)
.join(qUser.userInventory, qUserInventory)
.where(qUser.userInventory.id.eq(userInventory.getId()));
User result = query.fetchOne();
assertThat(result).isNotNull();
}
Hibernate:
select
user0_.id as id1_10_,
user0_.name as name2_10_,
user0_.price as price3_10_,
user0_.user_inventory_id as user_inv4_10_
from
user user0_
inner join
user_inventory userinvent1_
on user0_.user_inventory_id=userinvent1_.id
where
user0_.user_inventory_id=?
서브쿼리
- JPAExpressions를 생성해서 사용하고 SubQueryExpression으로 변환해야 사용 가능하다.
@Test
void queryDslSubQueryTest() {
User user = new User();
em.persist(user);
em.flush();
JPAQuery<User> query = new JPAQuery(em.getEntityManager());
QUser qUser = QUser.user;
SubQueryExpression subQueryExpression = JPAExpressions.select(qUser)
.from(qUser)
.where(qUser.name.eq("woojin"));
query.from(qUser)
.where(qUser.id.in(subQueryExpression));
List<User> result = query.fetch();
assertThat(result).isNotNull();
}
Hibernate:
select
user0_.id as id1_10_,
user0_.name as name2_10_,
user0_.price as price3_10_,
user0_.user_inventory_id as user_inv4_10_
from
user user0_
where
user0_.id in (
select
user1_.id
from
user user1_
where
user1_.name=?
)
프로젝션과 결과 반환
- Tuple을 이용해서 Map과 비슷한 내부 타입으로 조회가 가능하다.
수정, 삭제 배치 쿼리
- JPQL 배치 쿼리와 같이 영속성 컨텍스트를 무시하고 데이터베이스를 직접 쿼리한다는 점에 유의 해야 한다.
- JPAUpdateClause, JPADeleteClause를 사용한다.
동적 쿼리
- BooleanBuilder를 사용하면 특정 조건에 따른 동적 쿼리를 편리하게 생성할 수 있다.
@Test
void dynamicQueryTest() {
User user = new User();
em.persist(user);
em.flush();
JPAQuery<User> query = new JPAQuery(em.getEntityManager());
QUser qUser = QUser.user;
query.from(qUser);
BooleanBuilder builder = new BooleanBuilder();
String name = "woojin";
if (name != null) {
builder.and(qUser.name.eq(name));
}
query.where(builder);
List<User> result = query.fetch();
assertThat(result).isNotNull();
}
Hibernate:
select
user0_.id as id1_10_,
user0_.name as name2_10_,
user0_.price as price3_10_,
user0_.user_inventory_id as user_inv4_10_
from
user user0_
where
user0_.name=?
객체 지향 쿼리 심화
벌크 연산시 주의점
- 벌크 연산이 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리 한다는 점에 주의해야 한다.
- 업데이트 벌크 연산을 한 후에 영속성 컨텍스트를 통해서 조회하게 되면 영속성 컨텍스트에 이미 존재했던 엔티티의 경우 변경되지 않은 값으로 조회가 될 수 있다.
해결 방법
- em.refresh() 사용
- 벌크 연산 먼저 실행 후 조회
- 벌크 연산 수행 후 영속성 컨텍스트 초기화(em.clear())
영속성 컨텍스트와 JPQL
- select m from Member m // 엔티티 조회(관리O)
- select o.address from Order o //임베디드 타입 조회(관리X)
- select m.id, m.username from Member m // 단순 필드 조회 (관리X)
- JPQL로 조회한 엔티티는 영속상태이다.
- 영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환한다.
- 같은 식별자를 가진 엔티티는 영속성 컨텍스트에 중복해서 존재할 수 없다
- 영속성 컨텍스트에 있는 엔티티가 수정 중일 수 있으니 조회해서 가져온 것과 대체할 수 없다
- 영속성 컨텍스트에서 조회한 데이터는 동일성을 보장해야 하기에 새로 검색한 엔티티를 버리고 기존 엔티티를 반환한다.
find() vs JPQL
- find()의 경우 영속성 컨텍스트에 존재하면 영속성 컨텍스트의 엔티티를 반환하지만 JPQL은 실제 데이터베이스에서 조회를 한 후에 영속성 컨텍스트에 엔티티가 존재하는지 비교 후 엔티티를 반환한다.
- 따라서 JPQL로 조회하는 경우 SQL이 매번 실행 된다.
JPQL과 플러시 모드
- AUTO
- JPQL 쿼리가 실행되기 전과 커밋이 실행 될 때 플러시가 동작한다.
- Commit
- 트랜잭션 커밋이 실행 될 때 플러시가 동작하므로 성능의 이점을 가져올 수 있으나 사용시 주의 해야 한다.
마치며
객체지향 쿼리 언어는 여러가지 종류가 존재하지만 현재 개발조직에서는 Query DSL을 다루고 있어 Query DSL 부분에 대해서만 정리하였다.
각 라이브러리 마다 장단점이 존재하겠지만 객체로 쿼리를 작성하면서 실제 쿼리가 어떻게 동작하겠는지 쉽게 추측할 수 있는게 Query DSL의 장점 인것 같다.👍
- JPA 리뷰 글 보러가기
Leave a comment