The Jakarta Persistence Query Language (JPQL; formerly Java Persistence Query Language) is a platform-independent defined as part of the (JPA; formerly Java Persistence API) specification.
JPA 에 의해서 정의된 것으로 엔티티 객체를 대상으로 하는 쿼리 언어다. 그래서 특정 데이터베이스에 종속되지 않고 사용할 수 있다. 결국 해석되어 SQL 로 변환되어서 사용된다.
JPQL 이 특별히 어떠한 패키지나 자원으로 정의되어 있는 것이 아니라 entityManager 의 createQuery 와 같은 함수에 의해서 해석되는 String query 가 JPQL 로 짜여져서 동작해야 하는데, 이 때 사용되는 쿼리의 체계에 대한 용어라고 보면 된다.
아래 예시 코드에서 보면 createQuery() 내부에 사용된 String 의 쿼리 '체계'를 두고 JPQL 이라고 말하는 것이다.
@Override
@Transactional
public void run(ApplicationArguments args) {
Query query = this.entityManager.createQuery("select m from Member m where id = :id");
query.setParameter("id", 1L);
query.getSingleResult();
}
JPQL 과
HQL 은 '하이버네이트 질의 언어' 이다. 위 코드에서도 그렇고 entityManager 에 의해서 JPQL 이 동작하고 있어서 HQL 과 개념을 구분을 짓고, 연관 관계를 파악하는 것이 필요하다.
HQL과 JPQL 의 차이는 아래와 같다. 공식 레퍼런스가 아니고 gpt 답변이라서 참고만 하자.
네, JPQL(Jakarta Persistence Query Language)은 Hibernate Query Language(HQL)의 일부로 볼 수 있습니다.
HQL은 Hibernate ORM에서 사용되는 객체 지향 쿼리 언어로, 데이터베이스에 대한 특정 벤더 종속성을 피하면서 객체 그래프를 쿼리하는 기능을 제공합니다. JPQL은 JPA(Java Persistence API)의 일부로서 HQL을 기반으로 하여 개발되었습니다. JPA는 ORM(Object-Relational Mapping) 표준 인터페이스이며, 다양한 JPA 구현체 중에는 Hibernate가 가장 널리 사용되기 때문에 JPQL과 HQL은 많은 부분에서 유사한 기능을 가지고 있습니다.
따라서 JPQL은 HQL의 일종으로 볼 수 있으며, JPA를 사용하는 환경에서는 JPQL을 사용하여 객체 지향적인 쿼리를 작성할 수 있습니다.
정리를 하자면 'JPQL 과 HQL 은 엄연히 다른 개념이며, JPQL이 HQL 로서 동작할 수 있다' 라고 이해하고 넘어가자.
fetch join
fetch join 이란
책의 표현을 빌리면 ‘연관된 엔티티나 컬렉션을 한번에 조회하는 기능’이다.
fetch join 의 장점
fetch 전략을 LAZY로 해두었다고 해도 fetch join 으로 조회를 할 경우 해당 엔티티를 프록시 객체가 아닌 실제 엔티티로 할당한다. 실제 엔티티로 할당되기 때문에 그 엔티티를 반드시 사용할 경우라면 굳이 한번 더 쿼리를 수행할 필요가 없이(LAZY하게 가져올 필요 없이) 최초 한번으로 다 가져올 수 있기에 성능상 유리하다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "select m from Member as m join fetch m.team where m.team.name = :teamName")
List<Member> findAllByTeamNameFetchJoin(@Param(value = "teamName") String teamName);
List<Member> findAll();
}
@Override
@Transactional
public void run(ApplicationArguments args) throws Exception {
List<Member> members = memberRepository.findAll();
System.out.println(members.get(0).getTeam().getName());
System.out.println(members.get(0).getTeam().getClass().getName());
}
Hibernate:
select
member0_.id as id1_0_,
member0_.name as name2_0_,
member0_.team_id as team_id3_0_
from
member member0_
Hibernate:
select
team0_.id as id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.id=?
com.fistkim.springjpawhiteshipstudy.Team$HibernateProxy$4OTJPoST
LAZY 를 기본 fetch 전략으로 선택했기 때문에 쿼리가 두번 요청된 것을 볼 수 있고, Team 이 프록시 객체가 할당 된 것을 알 수 있다. 반면 fetch join을 사용하면 아래와 같이 Entity 가 나온다.
@Override
@Transactional
public void run(ApplicationArguments args) throws Exception {
List<Member> members = memberRepository.findAllByTeamNameFetchJoin("NewTeam");
System.out.println(members.get(0).getTeam().getName());
System.out.println(members.get(0).getTeam().getClass().getName());
}
Hibernate:
select
member0_.id as id1_0_0_,
team1_.id as id1_1_1_,
member0_.name as name2_0_0_,
member0_.team_id as team_id3_0_0_,
team1_.name as name2_1_1_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.id
where
team1_.name=?
com.fistkim.springjpawhiteshipstudy.Team
inner join 으로 한 번의 쿼리로 Team 까지 가져왔고, Team 객체 역시 프록시 객체가 아니고 엔티티임을 확인할 수 있다. fetch 라는 말 그대로 가서 엔티티를 가져온 것이다.
책에서는 아래와 같이 entityManager를 이용해서 가져오는 예제를 보여주고 있다. spring data jpa 를 이용해서 fetch join을 사용한 것과 동일한 쿼리가 수행된다.
@Override
@Transactional
public void run(ApplicationArguments args) throws Exception {
String jpql = "select m from Member as m join fetch m.team where m.team.name = :teamName";
List<Member> members = entityManager.createQuery(jpql, Member.class)
.setParameter("teamName", "NewTeam")
.getResultList();
System.out.println(members.get(0).getTeam().getName());
System.out.println(members.get(0).getTeam().getClass().getName());
}
Hibernate:
select
member0_.id as id1_0_0_,
team1_.id as id1_1_1_,
member0_.name as name2_0_0_,
member0_.team_id as team_id3_0_0_,
team1_.name as name2_1_1_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.id
where
team1_.name=?
com.fistkim.springjpawhiteshipstudy.Team
fetch join 과 inner join
fetch join 은 묵시적으로 inner join 이 발생한다. 따라서 OneToMany 에서 One 에서 Many 를 fetch join 하는 경우 명시적으로 left join 을 해주지 않으면 One 을 참조하는 Many 가 없는 경우 조회하고자 하는 One 자체가 조회가 안되는 문제가 발생할 수 있다. 이를 조심해야 한다.
fetch join 과 distinct
1개의 Team 과 이를 외래키로 참조하는 2개의 Member 가 실제로 데이터베이스 상에 존재하는 상황에 아래와 같이 실행하면 2개의 사이즈가 출력된다.
@Override
@Transactional
public void run(ApplicationArguments args) {
Query query = this.entityManager
.createQuery("select t from Team t join fetch t.members where t.id = :id");
query.setParameter("id", 1L);
System.out.println("Size : " + query.getResultList().size());
}
Hibernate:
select
team0_.id as id1_1_0_,
members1_.id as id1_0_1_,
team0_.name as name2_1_0_,
members1_.name as name2_0_1_,
members1_.team_id as team_id3_0_1_,
members1_.team_id as team_id3_0_0__,
members1_.id as id1_0_0__
from
team team0_
inner join
member members1_
on team0_.id=members1_.team_id
where
team0_.id=?
Size : 2
분명히 PK가 1L 인 Team은 하나가 존재하지만 inner join 을 통해서 2 개의 row 가 실행되었으므로 최종적으로 쿼리의 결과가 2개가 나온 것이다.
따라서 이렇게 OneToMany 의 관계에서 fetch join 을 사용할 경우 반드시 distinct 를 걸어줘야 한다.
@Override
@Transactional
public void run(ApplicationArguments args) {
Query query = this.entityManager
.createQuery("select distinct t from Team t join fetch t.members where t.id = :id");
query.setParameter("id", 1L);
System.out.println("Size : " + query.getResultList().size());
}
Hibernate:
select
distinct team0_.id as id1_1_0_,
members1_.id as id1_0_1_,
team0_.name as name2_1_0_,
members1_.name as name2_0_1_,
members1_.team_id as team_id3_0_1_,
members1_.team_id as team_id3_0_0__,
members1_.id as id1_0_0__
from
team team0_
inner join
member members1_
on team0_.id=members1_.team_id
where
team0_.id=?
Size : 1
sql의 distinct 는 중복된 결과를 제거하는 명령이지만 jpql의 distinct 는 아래 두 가지 기능을 수행한다.
중복된 결과를 제거.
어플리케이션에 올린뒤 중복되는 객체를 제거.
위 자료에서도 확인할 수 있듯이 SQL 에서의 distinct 와 JPQL 에서의 distinct 는 약간 다르다. JPQL 의 distinct 는 PK를 기준으로 같은 객체는 중복을 제거해준다.
다대일, 일대일 상황에서는 fetch join 을 해도 엔티티 수가 증가할 일은 없다는 것을 인지하자.
fetch join의 한계
fetch join 대상에는별칭을 사용할 수 없다
JPA 표준에는 정의되어 있지 않지만 하이버네이트를 포함한 몇몇 구현체들은 페치 조인에 별칭을 지원 한다고 한다. 하지만 별칭을 사용할 경우 데이터 무결성이 깨질 수 있다고 책에 언급되어 있다.
별칭은 조인을 거는 테이블과 다른 테이블을 조인할 때 종종 사용하게 되는데 유의하도록 하자.
둘 이상의 컬렉션을 페치할 수 없다
책에서는 '구현체에 따라 되기도 하는데' 라고 언급되어 있어서 조금 애매하다. 일단 하이버네이트에서는 예외가 발생한다.
아래와 같이 테스트를 해보았다.
@Test
@Transactional
@Rollback(value = false)
void sampleTest() {
User targetUser = this.init();
queryFactory = new JPAQueryFactory(em);
User result = queryFactory.selectFrom(user)
.distinct()
.innerJoin(user.books).fetchJoin()
.innerJoin(user.pencils).fetchJoin()
.where(user.id.eq(targetUser.getId()))
.fetchOne();
System.out.println("book size : " + result.getBooks().size());
System.out.println("pencil size : " + result.getPencils().size());
}
private User init() {
User user = new User();
em.persist(user);
Book book1 = new Book();
Book book2 = new Book();
book1.setUser(user);
book2.setUser(user);
em.persist(book1);
em.persist(book2);
Pencil pencil1 = new Pencil();
Pencil pencil2 = new Pencil();
pencil1.setUser(user);
pencil2.setUser(user);
em.persist(pencil1);
em.persist(pencil2);
em.flush();
em.clear();
return user;
}
cannot simultaneously fetch multiple bags
java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.example.querydslstudy.User.books, com.example.querydslstudy.User.pencils]
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:141)
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181)
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:188)
at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:757)
at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:114)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:315)
at com.sun.proxy.$Proxy112.createQuery(Unknown Source)
at com.querydsl.jpa.impl.AbstractJPAQuery.createQuery(AbstractJPAQuery.java:132)
at com.querydsl.jpa.impl.AbstractJPAQuery.fetchOne(AbstractJPAQuery.java:325)
at com.example.querydslstudy.QuerydslStudyApplicationTests.sampleTest(QuerydslStudyApplicationTests.java:34)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Suppressed: org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:752)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711)
at org.springframework.test.context.transaction.TransactionContext.endTransaction(TransactionContext.java:131)
at org.springframework.test.context.transaction.TransactionalTestExecutionListener.afterTestMethod(TransactionalTestExecutionListener.java:255)
at org.springframework.test.context.TestContextManager.afterTestMethod(TestContextManager.java:445)
at org.springframework.test.context.junit.jupiter.SpringExtension.afterEach(SpringExtension.java:206)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeAfterEachCallbacks$12(TestMethodTestDescriptor.java:257)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeAllAfterMethodsOrCallbacks$13(TestMethodTestDescriptor.java:273)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeAllAfterMethodsOrCallbacks$14(TestMethodTestDescriptor.java:273)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeAllAfterMethodsOrCallbacks(TestMethodTestDescriptor.java:272)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeAfterEachCallbacks(TestMethodTestDescriptor.java:256)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:141)
... 47 more
Caused by: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.example.querydslstudy.User.books, com.example.querydslstudy.User.pencils]
at org.hibernate.loader.BasicLoader.postInstantiate(BasicLoader.java:76)
at org.hibernate.loader.hql.QueryLoader.<init>(QueryLoader.java:110)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.createQueryLoader(QueryTranslatorImpl.java:249)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:213)
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.compile(QueryTranslatorImpl.java:144)
at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:112)
at org.hibernate.engine.query.spi.HQLQueryPlan.<init>(HQLQueryPlan.java:73)
at org.hibernate.engine.query.spi.QueryPlanCache.getHQLQueryPlan(QueryPlanCache.java:162)
at org.hibernate.internal.AbstractSharedSessionContract.getQueryPlan(AbstractSharedSessionContract.java:636)
at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:748)
... 79 more
페이징을 사용하면 안된다(사용이 가능은 하지만 사용하지 말자)
fetch join 과 페이징을 같이 사용할 수는 있으나 전체 데이터를 메모리에 올려서 페이징 처리를 하므로 문제가 있을 수 있다.
@Query(value = "select t from Team as t join fetch t.members", countQuery = "select count(t) from Team as t inner join t.members")
Page<Team> findAllFetchJoin(Pageable pageable);
.QueryTranslatorImpl : HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
Hibernate:
select
team0_.id as id1_1_0_,
members1_.id as id1_0_1_,
team0_.name as name2_1_0_,
members1_.name as name2_0_1_,
members1_.team_id as team_id3_0_1_,
members1_.team_id as team_id3_0_0__,
members1_.id as id1_0_0__
from
team team0_
inner join
member members1_
on team0_.id=members1_.team_id
이렇게 limit, offet 같은게 하나도 없이 모두 조회되어서 메모리상에 올라간다. 그래서 ‘firstResult/maxResults specified with collection fetch; applying in memory!’ 와 같은 경고 문구가 뜬다.
해결책으로는 fetch join을 사용하지 않고 LAZY 로딩을 그대로 사용하면서 대신 default_batch_fetch_size 를 이용하는 방법이 있다. 나의 예제 코드에 적용해보면 아래와 같이 나오게 된다.
@Query(value = "select t from Team as t", countQuery = "select count(t) from Team as t")
Page<Team> findAllPage(Pageable pageable);
@Override
@Transactional
public void run(ApplicationArguments args) throws Exception {
Page<Team> teams = teamRepository.findAllPage(PageRequest.of(0, 2));
teams.forEach(team -> System.out.println(team.getMembers().size()));
}
# paging query. page 번호를 0으로 넣어서 limit 만 들어간 모습이다.
Hibernate:
select
team0_.id as id1_1_,
team0_.name as name2_1_
from
team team0_ limit ?
# 내가 설정해준 count query.
Hibernate:
select
count(team0_.id) as col_0_0_
from
team team0_
# 지연로딩시 모아서 한번에 in 절로 나간 모습.
Hibernate:
select
members0_.team_id as team_id3_0_1_,
members0_.id as id1_0_1_,
members0_.id as id1_0_0_,
members0_.name as name2_0_0_,
members0_.team_id as team_id3_0_0_
from
member members0_
where
members0_.team_id in (
?, ?
)
fetch join 정리
fetch join 을 적극적으로 활용하여 최대한 쿼리 수를 줄이자
fetch join 사용시 OneToMany 인 경우 묵시적 inner join 임을 명심하자.(필요시 명시적 left join)
fetch join 사용시 OneToMany 인 경우 distinct 처리를 해주어야 한다.
fetch join 과 페이징을 같이 쓰면 안된다. fetch join 을 포기하고 LAZY 로딩이 걸리도록 한 뒤 default_batch_fetch_size 를 설정해주자.
QueryDSL
querydsl 은 따로 김영한님의 querydsl 전용으로 만든 강의를 수강했기에 거기서 자세한 내용은 정리할 예정이다. 다만 여기서 인지할 부분은 querydsl 은 결국 JPQL 빌더로서 역할을 한다는 것이다. 타입 세이프하게 JPQL 빌더로 활용되고 결국 만들어진 JPQL 이 SQL 로 해석되어서 실행된다.
1+N 문제
지연 로딩을 사용함으로써 수반되는 문제인데, 프록시 객체가 로드되는 과정에서 쿼리가 추가로 발생해서 생기는 문제이다. 지연 로딩이 실행되면서 쿼리야 당연히 실행되는 것이 맞지만 최종적으로 얻는 결과를 봤을때 한 번만 혹은 두 번만 쿼리가 실행되면 얻을 수 있는 결과임에도 무지성으로 지연 로딩이 실행되면서 쿼리가 비효율적으로 많이 실행되는 문제이다.
예를 들어 Team과 Member 가 있을때 N건의 Member 를 조회해와서 각 Member의 Team 이름을 출력하는 로직이 있을때 Member 를 가져오면서 Team도 join 을 통해서 1번의 쿼리로 모두 가져올 수 있지만, 이를 Member 만 불러온 후 Team 을 지연 로딩 하게 되면 최악의 경우 1 번(Member 들 조회) + N 번(N건의 Member 들 각각의 Team 에 대한 지연 로딩) 이 발생하는 문제이다.
이는 fetch join 을 이용하면 해결할 수 있다. 또 다른 방법으로는 default_batch_fetch_size 설정이 있다.
fetch join
fetch join 은 위에서 이미 충분히 알아보았으니 생략한다. fetch join 으로 member 를 조회할때 team 도 같이 조회해서 오면 1번의 쿼리로 n건의 member 가 조회된 상황에서 n건의 member 각각의 team 에 프록시 객체가 아닌 실제 entity 가 할당되게 된다. 프록시 객체가 아니라 실제 entity 가 할당되니까 지연로딩이 발생하지 않을 것이기에 1+N 문제가 해결된다.
@Override
@Transactional
public void run(ApplicationArguments args) throws Exception {
List<Team> teams = teamRepository.findAll();
teams.forEach(team -> System.out.println(team.getMembers().size()));
}
Hibernate:
select
team0_.id as id1_1_,
team0_.name as name2_1_
from
team team0_
Hibernate:
select
members0_.team_id as team_id3_0_1_,
members0_.id as id1_0_1_,
members0_.id as id1_0_0_,
members0_.name as name2_0_0_,
members0_.team_id as team_id3_0_0_
from
member members0_
where
members0_.team_id in (
?, ?
)
개별 엔티티에 @BatchSize 를 이용해서 사이즈를 지정해주는 방식도 있다.
@BatchSize(size = 100)
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
private Set<Member> members = new HashSet<>();
@Override
@Transactional
public void run(ApplicationArguments args) throws Exception {
List<Team> teams = teamRepository.findAll();
teams.forEach(team -> System.out.println(team.getMembers().size()));
}
이 ‘to go after and bring back (someone or something)’이다. 여태 일을 하며 fetch라는 단어를 많이 접했었는데, 막상 순수하게 단어 뜻을 보니 왜 fetch라고 사용되어 왔었는지 이해가 바로 되었다. (역시 근본적인 이해는 참 중요하다)
가 있어 링크를 남긴다.
에도 잘 나와 있는데, querydsl 을 설명하면서 빠질 수 없는 단어가 '타입 세이프' 이다. 태초에 탄생 배경 자체가 타입세이프에 대한 니즈에서 비롯되었다.
Querydsl was born out of the need to maintain HQL queries in a typesafe way. Incremental construction of queries requires String concatenation and results in hard to read code. Unsafe references to domain types and properties via plain Strings were another issue with String based HQL construction.
는 LAZY 로딩을 사용하게 되면 매우 흔히 겪을 수 있는 문제이다. 이직을 할때 면접에서도 자주 질문을 받았었다.
default_batch_fetch_size 의 원리는 지연 로딩으로 할당된 프록시 객체가 호출될 때 바로 호출하지 않고 모아서 in 절로 호출하는 원리이다. 에 정리가 잘 되어있다.