Search code examples
javahibernatewildfly

In Hibernate 6 CriteriaQuery max() aggregator fails when configuring a UserType on a serializable class


I have a Java (openjdk-17) web application using Hibernate 6 for connecting to a datasource configured in Wildfly 29 and I am unable to use CriteriaQuery max() aggregation because Hibernate report error:

org.hibernate.QueryException: Parameter 1 of function max() has type COMPARABLE, but argument is of type ...

The issue comes from an entity attribute annotated with @Type, because attribute class implements interface Serializable

@Type(value = SalaryType.class)
private Salary salary;

If Serializable is removed from Salary class definition then the query works fine. Unfortunately, I cannot remove the Serializable interface from the class definition, so I need to fin another solution.

My web application is quite complex but I have reproduced the error in a simple web app that I will be sharing here.

DB table

CREATE TABLE t_jd_office_employee (
  id varchar(40) NOT NULL PRIMARY KEY,
  salary varchar
);

Entity

@Entity
@Table(name = "t_jd_office_employee")
public class OfficeEmployee {

  @Id
  @Column(length = 40)
  private String id;

  @Type(value = SalaryType.class)
  private Salary salary;
}

Metamodel

@StaticMetamodel(OfficeEmployee.class)
@Generated("org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
public abstract class OfficeEmployee_ {

    public static volatile SingularAttribute<OfficeEmployee, Salary> salary;
}

Salary

    public class Salary implements Serializable, Comparable<Salary> {

    private Long amount;

    private String currency;


    public Long getAmount() {
        return amount;
    }


    public void setAmount(Long amount) {
        this.amount = amount;
    }


    public String getCurrency() {
        return currency;
    }


    public void setCurrency(String currency) {
        this.currency = currency;
    }

    public String asStringValue() {
        return Stream.of(amount, currency)
                .filter(Objects::nonNull)
                .map(Objects::toString)
                .reduce((a, b) -> a + " " + b)
                .orElse("");
    }


    @Override
    public int compareTo(Salary o) {
        return this.asStringValue().compareTo(o.asStringValue());
    }


    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass())
            return false;
        Salary salary = (Salary) o;
        return Objects.equals(this.asStringValue(), salary.asStringValue());
    }


    @Override
    public int hashCode() {
        return Objects.hash(this.asStringValue());
    }


    @Override
    public String toString() {
        return this.asStringValue();
    }
}

UserType

    public class SalaryType implements UserType<Salary> {

    @Override
    public int getSqlType() {
        return Types.VARCHAR;
    }


    @Override
    public Class<Salary> returnedClass() {
        return Salary.class;
    }


    @Override
    public boolean equals(Salary x, Salary y) {
        return x != null && x.equals(y);
    }


    @Override
    public int hashCode(Salary x) {
        return x == null ? Objects.hashCode(null) : x.hashCode();
    }


    @Override
    public Salary nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner)
            throws SQLException {
        final var salaryValue = rs.getString(position);
        if (salaryValue == null) {
            return null;
        }
        final var matcher = Pattern.compile("^(\\d+)?( )*([A-Z]{3})?$").matcher(salaryValue);
        if (matcher.matches()) {
            final var amount = matcher.group(1) == null ? null : Long.parseLong(matcher.group(1));
            final var salary = new Salary();
            salary.setAmount(amount);
            salary.setCurrency(matcher.group(3));
            return salary;
        } else {
            return null;
        }
    }


    @Override
    public void nullSafeSet(PreparedStatement st, Salary value, int index, SharedSessionContractImplementor session)
            throws SQLException {
        if (Objects.isNull(value))
            st.setNull(index, Types.VARCHAR);
        else {
            st.setString(index, value.asStringValue());
        }
    }


    @Override
    public Salary deepCopy(Salary value) {
        if (value == null) {
            return null;
        }
        final var salary = new Salary();
        salary.setAmount(value.getAmount());
        salary.setCurrency(value.getCurrency());
        return salary;
    }


    @Override
    public boolean isMutable() {
        return false;
    }


    @Override
    public Serializable disassemble(Salary value) {
        return value;
    }


    @Override
    public Salary assemble(Serializable cached, Object owner) {
        return (Salary) cached ;
    }
}

QuereriaQuery

private Optional<Salary> queryMaxSalary() {
        try (var em = emf.createEntityManager()) {
            final var criteriaBuilder = em.getCriteriaBuilder();
            final var query = criteriaBuilder.createQuery(Salary.class);
            final var root = query.from(OfficeEmployee.class);
            query.select(criteriaBuilder.greatest(root.get(OfficeEmployee_.salary)));
            try {
                final var result = em.createQuery(query).getSingleResult();
                return Optional.of(result);
            }
            catch (NoResultException ex) {
                logger.warn("No max salary found");
                return Optional.empty();
            }
        }
    }

persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence
             https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="3.0">
    <persistence-unit name="p5ee" transaction-type="JTA">

        <!-- Hibernate provider (Jakarta or JPA) -->
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>

        <!-- JNDI name for the WildFly data source -->
        <jta-data-source>java:jboss/datasources/P5XADS</jta-data-source>


        <!-- Properties -->
        <properties>
            <!-- Hibernate dialect -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>

            <!-- Hibernate show SQL (useful for debugging) -->
            <property name="hibernate.show_sql" value="true"/>

            <!-- Hibernate format SQL -->
            <property name="hibernate.format_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

The error

2025-02-21 08:54:46,247 ERROR [org.jboss.resteasy.core.providerfactory.DefaultExceptionMapper] (default task-1) RESTEASY002375: Error processing request GET /p5-office/rest/office/salary/max - local.jd.example.office.OfficeResource.getMaxSalary: org.hibernate.QueryException: Parameter 1 of function max() has type COMPARABLE, but argument is of type local.jd.example.office.Salary

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>local.jd.example.office</groupId>
    <artifactId>office-webapp</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <!-- Wildfly -->
        <version.server>29.0.1.Final</version.server>
        <version.bom.ee>${version.server}</version.bom.ee>
        <version.plugin.wildfly>5.1.1.Final</version.plugin.wildfly>
        <!-- Wildfly sever -->
        <wildfly.hostname>localhost</wildfly.hostname>
        <wildfly.port>9990</wildfly.port>
        <wildfly.username>admin</wildfly.username>
        <wildfly.password>******</wildfly.password>
    </properties>

    <dependencies>
        <!-- Jakarta -->
        <dependency>
            <groupId>jakarta.inject</groupId>
            <artifactId>jakarta.inject-api</artifactId>
            <version>2.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.enterprise</groupId>
            <artifactId>jakarta.enterprise.cdi-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.ws.rs</groupId>
            <artifactId>jakarta.ws.rs-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.16</version>
            <scope>provided</scope>
        </dependency>
        <!-- Hibernate ORM -->
        <dependency>
            <groupId>org.hibernate.orm</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>6.2.6.Final</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>p5-office</finalName>

        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.wildfly.plugins</groupId>
                    <artifactId>wildfly-maven-plugin</artifactId>
                    <version>${version.plugin.wildfly}</version>
                    <configuration>
                        <hostname>${wildfly.hostname}</hostname>
                        <port>${wildfly.port}</port>
                        <username>${wildfly.username}</username>
                        <password>${wildfly.password}</password>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
        <plugins>
            <!--Build configuration for the WAR plugin: -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.4.0</version>
                <configuration>
                    <!-- Jakarta EE doesn't require web.xml, Maven needs to catch up! -->
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Full stack trace

2025-02-23 10:38:35,683 ERROR [org.jboss.resteasy.core.providerfactory.DefaultExceptionMapper] (default task-1) RESTEASY002375: Error processing request GET /p5-office/rest/office/salary/max - local.jd.example.office.OfficeResource.getMaxSalary: org.hibernate.QueryException: Parameter 1 of function max() has type COMPARABLE, but argument is of type local.jd.example.office.Salary
    at org.hibernate@6.2.6.Final//org.hibernate.query.sqm.produce.function.ArgumentTypesValidator.throwError(ArgumentTypesValidator.java:253)
    at org.hibernate@6.2.6.Final//org.hibernate.query.sqm.produce.function.ArgumentTypesValidator.checkType(ArgumentTypesValidator.java:199)
    at org.hibernate@6.2.6.Final//org.hibernate.query.sqm.produce.function.ArgumentTypesValidator.validate(ArgumentTypesValidator.java:98)
    at org.hibernate@6.2.6.Final//org.hibernate.query.sqm.function.AbstractSqmFunctionDescriptor.generateSqmExpression(AbstractSqmFunctionDescriptor.java:104)
    at org.hibernate@6.2.6.Final//org.hibernate.query.sqm.function.SqmFunctionDescriptor.generateSqmExpression(SqmFunctionDescriptor.java:117)
    at org.hibernate@6.2.6.Final//org.hibernate.query.sqm.internal.SqmCriteriaNodeBuilder.greatest(SqmCriteriaNodeBuilder.java:873)
    at org.hibernate@6.2.6.Final//org.hibernate.query.sqm.internal.SqmCriteriaNodeBuilder.greatest(SqmCriteriaNodeBuilder.java:182)
    at deployment.p5-office.war//local.jd.example.office.OfficeResource.queryMaxSalary(OfficeResource.java:56)
    at deployment.p5-office.war//local.jd.example.office.OfficeResource.getMaxSalary(OfficeResource.java:36)
    at deployment.p5-office.war//local.jd.example.office.OfficeResource$Proxy$_$$_WeldClientProxy.getMaxSalary(Unknown Source)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:154)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:118)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:560)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:452)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$2(ResourceMethodInvoker.java:413)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:321)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:415)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:378)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:356)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:70)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:429)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:240)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:154)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:321)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:157)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:229)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:222)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:55)
    at org.jboss.resteasy.resteasy-core@6.2.5.Final//org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:51)
    at jakarta.servlet.api@6.0.0//jakarta.servlet.http.HttpServlet.service(HttpServlet.java:614)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
    at org.wildfly.security.elytron-web.undertow-server@4.0.0.Final//org.wildfly.elytron.web.undertow.server.ElytronRunAsHandler.lambda$handleRequest$1(ElytronRunAsHandler.java:68)
    at org.wildfly.security.elytron-base@2.2.1.Final//org.wildfly.security.auth.server.FlexibleIdentityAssociation.runAsFunctionEx(FlexibleIdentityAssociation.java:103)
    at org.wildfly.security.elytron-base@2.2.1.Final//org.wildfly.security.auth.server.Scoped.runAsFunctionEx(Scoped.java:161)
    at org.wildfly.security.elytron-base@2.2.1.Final//org.wildfly.security.auth.server.Scoped.runAs(Scoped.java:73)
    at org.wildfly.security.elytron-web.undertow-server@4.0.0.Final//org.wildfly.elytron.web.undertow.server.ElytronRunAsHandler.handleRequest(ElytronRunAsHandler.java:67)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:117)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
    at io.undertow.core@2.3.7.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.core@2.3.7.Final//io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
    at io.undertow.core@2.3.7.Final//io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
    at org.wildfly.security.elytron-web.undertow-server-servlet@4.0.0.Final//org.wildfly.elytron.web.undertow.server.servlet.CleanUpHandler.handleRequest(CleanUpHandler.java:38)
    at io.undertow.core@2.3.7.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at org.wildfly.extension.undertow@29.0.1.Final//org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61)
    at io.undertow.core@2.3.7.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at org.wildfly.extension.undertow@29.0.1.Final//org.wildfly.extension.undertow.deployment.GlobalRequestControllerHandler.handleRequest(GlobalRequestControllerHandler.java:68)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.handlers.SendErrorPageHandler.handleRequest(SendErrorPageHandler.java:52)
    at io.undertow.core@2.3.7.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:276)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:135)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:132)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
    at org.wildfly.extension.undertow@29.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1430)
    at org.wildfly.extension.undertow@29.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1430)
    at org.wildfly.extension.undertow@29.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1430)
    at org.wildfly.extension.undertow@29.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1430)
    at org.wildfly.extension.undertow@29.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1430)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:256)
    at io.undertow.servlet@2.3.7.Final//io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:101)
    at io.undertow.core@2.3.7.Final//io.undertow.server.Connectors.executeRootHandler(Connectors.java:393)
    at io.undertow.core@2.3.7.Final//io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:859)
    at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
    at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
    at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
    at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1377)
    at org.jboss.xnio@3.8.9.Final//org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1282)
    at java.base/java.lang.Thread.run(Thread.java:833)

Debug

enter image description here


Solution

  • In the end, the cause of this problem seems to be a bug in Hibernate 6.2.6.Final. This is the version included in Wildfly 29. I ran the same test with Wildfly 35 and Hibernate 6.6.8.Final and it worked fine.