Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to support rendering query that contains entities with value class fields? #559

Closed
leviathan-n opened this issue Jan 1, 2024 · 6 comments
Assignees
Labels
documentation Improvements or additions to documentation

Comments

@leviathan-n
Copy link

leviathan-n commented Jan 1, 2024

spring data add support for kotlin value class at version 3.2 , so we can define an entity like this to avoid primitive obsession:

import jakarta.persistence.*

@Entity
class Brand(
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    val id: BrandId = BrandId(0),

    val name: String,
) {

    @Version
    val version: Int = 0
}

@JvmInline
value class BrandId(private val value: Long) : Comparable<BrandId> {  // jdsl Expressionable.gt requires type implements Comparable

    override fun compareTo(other: BrandId): Int =
        value.compareTo(other.value)
}

@Entity
class Product(

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    val id: ProductId = ProductId(0),

    val name: String,

    @Enumerated(EnumType.STRING)
    val type: ProductType,

    val brandId: BrandId,
) {

    @Version
    val version: Int = 0
}

@JvmInline
value class ProductId(val value: Long)

enum class ProductType {
    DRINK,
    TOOL,
    FOOD
}

and saying that there is a simple query

data class ProductWithBrand(
    val productId: ProductId,
    val productName: String,
    val brandId: BrandId,
    val brandName: String,
)

@Service
class TestService(
    private val jpqlRenderContext: JpqlRenderContext,
    private val entityManager: EntityManager,
) {

    fun test(): List<ProductWithBrand> {
        val query = jpql {
            select<ProductWithBrand>(
                path(Product::id),
                path(Product::name),
                path(Brand::id),
                path(Brand::name),
            )
                .from(
                    entity(Product::class),
                    join(Brand::class).on(path(Product::brandId).eq(path(Brand::id)))
                )
                .where(
                    entity(Product::class)(Product::name).like("coca")
                        .or(
                            path(Brand::name).eq("a")
                                .and(path(Brand::id).gt(BrandId(1L)))
                        )
                )
                .orderBy(path(Product::id).desc())
        }

        return entityManager.createQuery(query, jpqlRenderContext).apply { maxResults = 1 }.resultList
    }
}

and got an exception when executing it

org.hibernate.type.descriptor.java.CoercionException: Cannot coerce value 'BrandId(value=1)' [com.example.springpg.entity.BrandId] to Long
	at org.hibernate.type.descriptor.java.LongJavaType.coerce(LongJavaType.java:157) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.type.descriptor.java.LongJavaType.coerce(LongJavaType.java:24) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.query.internal.QueryParameterBindingImpl.coerce(QueryParameterBindingImpl.java:164) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.query.internal.QueryParameterBindingImpl.setBindValue(QueryParameterBindingImpl.java:113) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.query.spi.AbstractCommonQueryContract.setParameter(AbstractCommonQueryContract.java:835) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.query.spi.AbstractSelectionQuery.setParameter(AbstractSelectionQuery.java:900) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.query.sqm.internal.QuerySqmImpl.setParameter(QuerySqmImpl.java:1258) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.query.sqm.internal.QuerySqmImpl.setParameter(QuerySqmImpl.java:140) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at com.linecorp.kotlinjdsl.support.spring.data.jpa.JpqlEntityManagerUtils.setParams(JpqlEntityManagerUtils.kt:220) ~[spring-data-jpa-support-3.2.0.jar:na]
	at com.linecorp.kotlinjdsl.support.spring.data.jpa.JpqlEntityManagerUtils.createQuery(JpqlEntityManagerUtils.kt:92) ~[spring-data-jpa-support-3.2.0.jar:na]
	at com.linecorp.kotlinjdsl.support.spring.data.jpa.JpqlEntityManagerUtils.createQuery(JpqlEntityManagerUtils.kt:30) ~[spring-data-jpa-support-3.2.0.jar:na]
	at com.linecorp.kotlinjdsl.support.spring.data.jpa.extension.EntityManagerExtensionsKt.createQuery(EntityManagerExtensions.kt:20) ~[spring-data-jpa-support-3.2.0.jar:na]
	at com.example.springpg.service.TestService.test(TestService.kt:41) ~[main/:na]
	at com.example.springpg.SpringPgApplication$route$1$1$1.invoke(SpringPgApplication.kt:25) ~[main/:na]
	at com.example.springpg.SpringPgApplication$route$1$1$1.invoke(SpringPgApplication.kt:24) ~[main/:na]
	at org.springframework.web.servlet.function.RouterFunctionDsl.GET$lambda$0(RouterFunctionDsl.kt:153) ~[spring-webmvc-6.1.2.jar:6.1.2]
	at org.springframework.web.servlet.function.support.HandlerFunctionAdapter.handle(HandlerFunctionAdapter.java:107) ~[spring-webmvc-6.1.2.jar:6.1.2]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.2.jar:6.1.2]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.2.jar:6.1.2]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.2.jar:6.1.2]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.1.2.jar:6.1.2]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[tomcat-embed-core-10.1.17.jar:6.0]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.2.jar:6.1.2]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.17.jar:6.0]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.17.jar:10.1.17]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.2.jar:6.1.2]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.2.jar:6.1.2]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.2.jar:6.1.2]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.2.jar:6.1.2]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.2.jar:6.1.2]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.2.jar:6.1.2]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:340) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1744) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.17.jar:10.1.17]
	at java.base/java.lang.VirtualThread.run(VirtualThread.java:309) ~[na:na]
@shouwn shouwn added the enhancement New feature or request label Jan 2, 2024
@shouwn shouwn self-assigned this Jan 2, 2024
@shouwn
Copy link
Member

shouwn commented Jan 4, 2024

Hi, leviathan-n.

I'll look into it in detail this weekend, but supporting a value class doesn't seem easy for two reasons.

The first is that Kotlin automatically wraps the value class when it is passed to an external library. Because of this, Kotlin JDSL has to use reflection every time to check if the passed value is the value class and to extract the value of the value class.

The second reason is the most problematic: Kotlin unboxes the value class field when it compiles to Java. It seems difficult to make them compatible without creating a new expression function.

@shouwn
Copy link
Member

shouwn commented Jan 8, 2024

I reviewed value classes and my conclusion was to support them informally with guides.

I think it's too early to formally support value classes, because there are many risks.

  1. we don't know how Kotlin's Java code will change in the future, since Project Valhalla is not ready yet.
  2. Spring doesn't fully support value class compatibility either. If the repository does not reference a value class directly, it throws an exception.
  3. Kotlin is considering to support multi-field value classes before Project Valhalla, but it is not compatible with value classes because the fields are not exposed unwrapped in the entity when there are multiple fields.

For these reasons, I don't think value classes are ready for library support yet.

However, I think it would be a waste not to use value classes, so I thought I'd provide some informal guidance.

The current problem is that the Kotlin JDSL needs to unbox value classes that are passed as parameters. To make this possible, we can implement a JpqlSerializer for JpqlValue.

There are several ways to implement a JpqlSerializer. You can either list the value classes in advance and implement your own logic to unbox the value passed as a parameter if it matches, or you can use Kotlin reflection to get the value using the first property if the type is a value class.

I'll leave it up to the user to decide which one to choose.

If you are comfortable with this approach, I will create a document for value class.

@shouwn
Copy link
Member

shouwn commented Jan 27, 2024

Sorry I saw your reaction too late. I'll leave how to use value class in the documentation.

Thanks for your cooperation!

@shouwn shouwn added documentation Improvements or additions to documentation and removed enhancement New feature or request labels Mar 3, 2024
@shouwn shouwn assigned cj848 and unassigned shouwn Mar 4, 2024
@erie0210
Copy link
Contributor

erie0210 commented May 9, 2024

@shouwn 안녕하세요! 어사인 요청드립니다. :)

@cj848 cj848 assigned erie0210 and unassigned cj848 May 9, 2024
@cj848
Copy link
Collaborator

cj848 commented May 9, 2024

작업 예정이신가요? 어사인 드렸습니다 :) 정신없어서 계속 못하고 있었는데 감사합니다.

@shouwn
Copy link
Member

shouwn commented May 9, 2024

@erie0210 감사합니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

4 participants