line / kotlin-jdsl

Kotlin library that makes it easy to build and execute queries without generated metamodel
https://kotlin-jdsl.gitbook.io/docs/
Apache License 2.0
651 stars 85 forks source link

When use selectDistinctNew function in findPage, there are some paging error. #694

Closed veluxer62 closed 2 months ago

veluxer62 commented 2 months ago

안녕하세요. 아래와 같이 selectDistinctNew를 사용하여 페이징 쿼리를 호출하면 일부 페이지에서 오류가 발생하는 현상을 발견하여 문의드립니다.

현상

val pageable = PageRequest.of(5, 10)

val actual = boardRepository.findPage(pageable) {
    selectDistinctNew<Pair<Board, Int?>>(
        entity(Board::class),
        path(BoardStats::viewCount),
    )
        .from(
            entity(Board::class),
            leftJoin(BoardStats::class)
                .on(path(Board::id).eq(path(BoardStats::boardId)),)
        )
}

assertEquals(5, actual.totalElements)
org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.query.SyntaxException: At 1:26 and token 'kotlin', no viable alternative at input 'SELECT count(DISTINCT NEW *kotlin.Pair(Board, BoardStats.viewCount)) FROM Board AS Board LEFT JOIN BoardStats AS BoardStats ON Board.id = BoardStats.boardId' [SELECT count(DISTINCT NEW kotlin.Pair(Board, BoardStats.viewCount)) FROM Board AS Board LEFT JOIN BoardStats AS BoardStats ON Board.id = BoardStats.boardId]
    at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:371)
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:246)
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:550)
    at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
    at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:335)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:152)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:164)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.data.repository.core.support.MethodInvocationValidator.invoke(MethodInvocationValidator.java:95)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:220)
    at jdk.proxy3/jdk.proxy3.$Proxy108.findPage(Unknown Source)
    at com.example.demo.BoardRepositoryTest.test_join(BoardRepositoryTest.kt:44)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:728)
    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:156)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
    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.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:218)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:214)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:139)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69)
    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:1596)
    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:1596)
    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.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:119)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:94)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:89)
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:62)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
    at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
    at jdk.proxy1/jdk.proxy1.$Proxy2.stop(Unknown Source)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
    at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65)
    at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
    at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Caused by: java.lang.IllegalArgumentException: org.hibernate.query.SyntaxException: At 1:26 and token 'kotlin', no viable alternative at input 'SELECT count(DISTINCT NEW *kotlin.Pair(Board, BoardStats.viewCount)) FROM Board AS Board LEFT JOIN BoardStats AS BoardStats ON Board.id = BoardStats.boardId' [SELECT count(DISTINCT NEW kotlin.Pair(Board, BoardStats.viewCount)) FROM Board AS Board LEFT JOIN BoardStats AS BoardStats ON Board.id = BoardStats.boardId]
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:143)
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:167)
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:173)
    at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:848)
    at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:136)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:319)
    at jdk.proxy3/jdk.proxy3.$Proxy100.createQuery(Unknown Source)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.JpqlEntityManagerUtils.createQuery(JpqlEntityManagerUtils.kt:84)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.JpqlEntityManagerUtils.access$createQuery(JpqlEntityManagerUtils.kt:14)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.JpqlEntityManagerUtils$createEnhancedQuery$1.invoke(JpqlEntityManagerUtils.kt:74)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.JpqlEntityManagerUtils$createEnhancedQuery$1.invoke(JpqlEntityManagerUtils.kt:72)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.EnhancedTypedQuery$countQuery$2.invoke(EnhancedTypedQuery.kt:9)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.EnhancedTypedQuery$countQuery$2.invoke(EnhancedTypedQuery.kt:9)
    at kotlin.UnsafeLazyImpl.getValue(Lazy.kt:81)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.EnhancedTypedQuery.getCountQuery(EnhancedTypedQuery.kt:9)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.repository.KotlinJdslJpqlExecutorImpl$createJpaEnhancedQuery$2.invoke(KotlinJdslJpqlExecutorImpl.kt:234)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.repository.KotlinJdslJpqlExecutorImpl$createJpaEnhancedQuery$2.invoke(KotlinJdslJpqlExecutorImpl.kt:232)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.EnhancedTypedQuery$countQuery$2.invoke(EnhancedTypedQuery.kt:9)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.EnhancedTypedQuery$countQuery$2.invoke(EnhancedTypedQuery.kt:9)
    at kotlin.UnsafeLazyImpl.getValue(Lazy.kt:81)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.EnhancedTypedQuery.getCountQuery(EnhancedTypedQuery.kt:9)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.repository.KotlinJdslJpqlExecutorImpl.createPage$lambda$4(KotlinJdslJpqlExecutorImpl.kt:295)
    at org.springframework.data.support.PageableExecutionUtils.getPage(PageableExecutionUtils.java:70)
    at org.springframework.data.support.PageableExecutionUtilsAdaptor.getPage(PageableExecutionUtilsAdaptor.kt:9)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.repository.KotlinJdslJpqlExecutorImpl.createPage(KotlinJdslJpqlExecutorImpl.kt:294)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.repository.KotlinJdslJpqlExecutorImpl.findPage(KotlinJdslJpqlExecutorImpl.kt:108)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.repository.KotlinJdslJpqlExecutorImpl.findPage(KotlinJdslJpqlExecutorImpl.kt:98)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:351)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:765)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:392)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:765)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:717)
    at com.linecorp.kotlinjdsl.support.spring.data.jpa.repository.KotlinJdslJpqlExecutorImpl$$SpringCGLIB$$0.findPage(<generated>)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:351)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
    at jdk.proxy3/jdk.proxy3.$Proxy68.findPage(Unknown Source)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:351)
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:277)
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170)
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158)
    at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:516)
    at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:628)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:168)
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:143)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:70)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:392)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
    ... 90 more
Caused by: org.hibernate.query.SyntaxException: At 1:26 and token 'kotlin', no viable alternative at input 'SELECT count(DISTINCT NEW *kotlin.Pair(Board, BoardStats.viewCount)) FROM Board AS Board LEFT JOIN BoardStats AS BoardStats ON Board.id = BoardStats.boardId' [SELECT count(DISTINCT NEW kotlin.Pair(Board, BoardStats.viewCount)) FROM Board AS Board LEFT JOIN BoardStats AS BoardStats ON Board.id = BoardStats.boardId]
    at org.hibernate.query.hql.internal.StandardHqlTranslator$1.syntaxError(StandardHqlTranslator.java:108)
    at org.antlr.v4.runtime.ProxyErrorListener.syntaxError(ProxyErrorListener.java:41)
    at org.antlr.v4.runtime.Parser.notifyErrorListeners(Parser.java:544)
    at org.antlr.v4.runtime.DefaultErrorStrategy.reportNoViableAlternative(DefaultErrorStrategy.java:310)
    at org.antlr.v4.runtime.DefaultErrorStrategy.reportError(DefaultErrorStrategy.java:136)
    at org.hibernate.grammars.hql.HqlParser.queryExpression(HqlParser.java:1809)
    at org.hibernate.grammars.hql.HqlParser.selectStatement(HqlParser.java:407)
    at org.hibernate.grammars.hql.HqlParser.statement(HqlParser.java:337)
    at org.hibernate.query.hql.internal.StandardHqlTranslator.parseHql(StandardHqlTranslator.java:132)
    at org.hibernate.query.hql.internal.StandardHqlTranslator.translate(StandardHqlTranslator.java:67)
    at org.hibernate.query.internal.QueryInterpretationCacheStandardImpl.createHqlInterpretation(QueryInterpretationCacheStandardImpl.java:165)
    at org.hibernate.query.internal.QueryInterpretationCacheStandardImpl.resolveHqlInterpretation(QueryInterpretationCacheStandardImpl.java:147)
    at org.hibernate.internal.AbstractSharedSessionContract.interpretHql(AbstractSharedSessionContract.java:790)
    at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:840)
    ... 153 more

한편, 아래와 같이 페이지를 호출하면 오류가 발생하지 않는데요. 다만 페이징을 위해 호출되는 count 쿼리는 호출되지 않아 이부분도 함께 확인해주시면 감사하겠습니다.

val pageable = PageRequest.of(0, 10)

val actual = boardRepository.findPage(pageable) {
    selectDistinctNew<Pair<Board, Int?>>(
        entity(Board::class),
        path(BoardStats::viewCount),
    )
        .from(
            entity(Board::class),
            leftJoin(BoardStats::class)
                .on(path(Board::id).eq(path(BoardStats::boardId)),)
        )
}

assertEquals(5, actual.totalElements)
2024-04-20T12:28:19.065+09:00  INFO 91769 --- [demo] [    Test worker] o.s.d.j.r.query.QueryEnhancerFactory     : Hibernate is in classpath; If applicable, HQL parser will be used.
Hibernate: insert into board (content,title,id) values (?,?,?)
Hibernate: insert into board (content,title,id) values (?,?,?)
Hibernate: insert into board (content,title,id) values (?,?,?)
Hibernate: insert into board_stats (board_id,view_count,id) values (?,?,?)
Hibernate: insert into board_stats (board_id,view_count,id) values (?,?,?)
Hibernate: insert into board_stats (board_id,view_count,id) values (?,?,?)
Hibernate: insert into board_stats (board_id,view_count,id) values (?,?,?)
Hibernate: insert into board_stats (board_id,view_count,id) values (?,?,?)
Hibernate: select distinct b1_0.id,b1_0.content,b1_0.title,bs1_0.view_count from board b1_0 left join board_stats bs1_0 on b1_0.id=bs1_0.board_id offset ? rows fetch first ? rows only

전체 코드

Application.kt

@SpringBootApplication
@EnableJpaRepositories
class DemoApplication

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

@Entity
class Board(
    @Column(nullable = false)
    val title: String,

    @Column(nullable = false)
    val content: String,
) {
    @Id
    val id: UUID = UUID.randomUUID()
}

@Entity
class BoardStats(
    @Column(nullable = false)
    val boardId: UUID,

    @Column
    val viewCount: Int,
) {
    @Id
    val id: UUID = UUID.randomUUID()
}

interface BoardRepository : JpaRepository<Board, UUID>, KotlinJdslJpqlExecutor

BoardRepositoryTest.kt

@DataJpaTest(showSql = true)
@Import(KotlinJdslAutoConfiguration::class)
class BoardRepositoryTest {
    @Autowired
    private lateinit var testEntityManager: TestEntityManager

    @Autowired
    private lateinit var boardRepository: BoardRepository

    @Test
    fun test_join() {
        val board1 = Board("게시판1", "내용1")
        val board2 = Board("게시판2", "내용2")
        val board3 = Board("게시판3", "내용3")

        val stats1 = BoardStats(boardId = board1.id, viewCount = 2)
        val stats2 = BoardStats(boardId = board1.id, viewCount = 3)
        val stats3 = BoardStats(boardId = board1.id, viewCount = 3)
        val stats4 = BoardStats(boardId = board1.id, viewCount = 1)
        val stats5 = BoardStats(boardId = board2.id, viewCount = 5)

        testEntityManager.persist(board1)
        testEntityManager.persist(board2)
        testEntityManager.persist(board3)
        testEntityManager.persist(stats1)
        testEntityManager.persist(stats2)
        testEntityManager.persist(stats3)
        testEntityManager.persist(stats4)
        testEntityManager.persist(stats5)

        val pageable = PageRequest.of(5, 10)

        val actual = boardRepository.findPage(pageable) {
            selectDistinctNew<Pair<Board, Int?>>(
                entity(Board::class),
                path(BoardStats::viewCount),
            )
                .from(
                    entity(Board::class),
                    leftJoin(BoardStats::class)
                        .on(path(Board::id).eq(path(BoardStats::boardId)),)
                )
        }

        assertEquals(5, actual.totalElements)
    }
}

build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "3.2.4"
    id("io.spring.dependency-management") version "1.1.4"
    kotlin("jvm") version "1.9.23"
    kotlin("plugin.spring") version "1.9.23"
    kotlin("plugin.jpa") version "1.9.23"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"

java {
    sourceCompatibility = JavaVersion.VERSION_17
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("com.linecorp.kotlin-jdsl:jpql-dsl:3.4.0")
    implementation("com.linecorp.kotlin-jdsl:jpql-render:3.4.0")
    implementation("com.linecorp.kotlin-jdsl:spring-data-jpa-support:3.4.0")
    runtimeOnly("com.h2database:h2")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs += "-Xjsr305=strict"
        jvmTarget = "17"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}
shouwn commented 2 months ago

안녕하세요. 먼저 두 번째 문의주신 count 쿼리가 호출되지 않는 이유는 호출할 필요가 없기 때문입니다. 올려주신 내용을 보면 board가 총 3건 들어가 있고 Pageable로 10건 데이터를 조회하는 것으로 보입니다.

Spring은 리스트 조회에서 요청받은 10건 보다 적은 건수의 데이터가 조회되는 것을 보고 count의 추가 조회는 무의미하다고 판단합니다. 왜냐하면 10건 보다 적다면 이게 마지막 페이지일 것이기 때문입니다.

자세한 내용은 org.springframework.data.support.PageableExecutionUtils을 참고 부탁드립니다.

첫 번째 경우는 Spring에서 Pageable을 select distinct new 쿼리를 제대로 파싱하지 못 하는 문제로 보여집니다. 이 이슈는 Spring Data JPA 측에 문의를 부탁드립니다. Kotlin JDSL은 Pageable의 count 쿼리 생성을 Spring 쪽에 위임하고 있기 때문입니다.

Kotlin JDSL을 경유하지 않고 Spring의 @Query 어노테이션을 사용해도 동일한 현상이 발생할 것입니다.

veluxer62 commented 2 months ago

네.

Spring Data JPA 이슈에 아래와 같은 이슈가 있군요. 다른방법을 모색해봐야 할듯 합니다. 확인 감사합니다. https://github.com/spring-projects/spring-data-jpa/issues/3098

카운트 쿼리도 말씀하신 부분도 PageableExecutionUtils 코드에서 확인되었습니다. 설명해주셔서 감사합니다 :)