Skip to content

RSQL JPA ClassCastException with Spring JPA AbstractPersistable

What are RSQL and RSQL JPA?

RSQL is a query language for parametrized filtering of entries in RESTful APIs. It’s based on FIQL (Feed Item Query Language) – an URI-friendly syntax for expressing filters across the entries in an Atom Feed.

RSQL JPA translates RSQL query into org.springframework.data.jpa.domain.Specification or com.querydsl.core.types.Predicate and support entities association query.

Use RSQL JPA in Spring Boot 3

  1. add RSQL JPA dependency into build.gradle

    Text Only
    // omit other spring boot JPA dependencies
    implementation 'io.github.perplexhub:rsql-jpa-spring-boot-starter:6.0.6'
    
  2. Change JPA Repository to extend JpaSpecificationExecutor<EntityClass>

  3. In the controller accept a query paratemer and pass it to service layer

  4. In the service layer, query with RSQL like,

    Java
    /*
    for example, we have the following entity class, and User has primary key id
    public class UserAddress {
        private User user;
    }
    Then the filter could be "user.id==1", order by clause could be "id,desc"
    */
    Specification<?> specification = RSQLJPASupport.toSpecification(rsqlFilter).and(RSQLJPASupport.toSort(orderBy));
    
    Page<EntityClass> entities = entitiesRepository.findAll((Specification<EntityClass>) specification, pageable);
    

Issues

In my project, I have an User entity like,

Java
public class User extends AbstractPersistable<Long> {
    private String firstName;
    //...
}

public class UserAddress AbstractPersistable<Long> {
    private User user;
    // ...
}

Then I am trying to search an user's address with the following RSQL

Text Only
user.id==1

Then I'll get error class java.lang.String cannot be cast to class java.lang.Long (java.lang.String and java.lang.Long are in module java.base of loader 'bootstrap') with the following stackstrace.

Stackstrace
Text Only
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Long (java.lang.String and java.lang.Long are in module java.base of loader 'bootstrap')
at org.hibernate.type.descriptor.java.LongJavaType.unwrap(LongJavaType.java:24)
at org.hibernate.type.descriptor.jdbc.BigIntJdbcType$1.doBind(BigIntJdbcType.java:62)
at org.hibernate.type.descriptor.jdbc.BasicBinder.bind(BasicBinder.java:61)
at org.hibernate.sql.exec.internal.AbstractJdbcParameter.bindParameterValue(AbstractJdbcParameter.java:118)
at org.hibernate.sql.exec.internal.AbstractJdbcParameter.bindParameterValue(AbstractJdbcParameter.java:108)
at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.bindParameters(DeferredResultSetAccess.java:199)
at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.executeQuery(DeferredResultSetAccess.java:228)
at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.getResultSet(DeferredResultSetAccess.java:163)
at org.hibernate.sql.results.jdbc.internal.JdbcValuesResultSetImpl.advanceNext(JdbcValuesResultSetImpl.java:254)
at org.hibernate.sql.results.jdbc.internal.JdbcValuesResultSetImpl.processNext(JdbcValuesResultSetImpl.java:134)
at org.hibernate.sql.results.jdbc.internal.AbstractJdbcValues.next(AbstractJdbcValues.java:19)
at org.hibernate.sql.results.internal.RowProcessingStateStandardImpl.next(RowProcessingStateStandardImpl.java:66)
at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:198)
at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:33)
at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.doExecuteQuery(JdbcSelectExecutorStandardImpl.java:361)
at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.executeQuery(JdbcSelectExecutorStandardImpl.java:168)
at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.list(JdbcSelectExecutorStandardImpl.java:93)
at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:31)
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.lambda$new$0(ConcreteSqmSelectQueryPlan.java:110)
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:303)
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:244)
at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:518)
at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:367)
at org.hibernate.query.Query.getResultList(Query.java:119)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.readPage(SimpleJpaRepository.java:696)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll(SimpleJpaRepository.java:448)

Analysis

Then I started to debug this issue.

The root cause is that, when hibernate tries to bind the parameter for prepared sql, it expects the value type is Long for user.id, but got String.

That's why hibernate throws ClassCastException, as it can't cast String to Long.

But why it got a String rather than Long? Let's dig into RSQL implementation. In RSQLJPAPredicateConverter, method public Predicate visit(ComparisonNode node, From root) is called when JPA to generated a SQL query from a specification.

Method visit of RSQLJPAPredicateConverter
Java
@Override
public Predicate visit(ComparisonNode node, From root) {
    log.debug("visit(node:{},root:{})", node, root);

    ComparisonOperator op = node.getOperator();
    RSQLJPAContext holder = findPropertyPath(node.getSelector(), root);
    Path attrPath = holder.getPath();
    Attribute attribute = holder.getAttribute();

    if (customPredicates.containsKey(op)) {
        RSQLCustomPredicate<?> customPredicate = customPredicates.get(op);
        List<Object> arguments = new ArrayList<>();
            for (String argument : node.getArguments()) {
                arguments.add(convert(argument, customPredicate.getType()));
            }
            return customPredicate.getConverter().apply(RSQLCustomPredicateInput.of(builder, attrPath, attribute, arguments, root));
        }

    // ..
 }

In the highlight line, it's to find the java type of the RSQL query propety. For user.id==1, RSQL will try to find the java type of id of User, and convert the query value 1 to the target type based on java type returned.

For this issue, it's obvious that it didn't return the correct type for user.id, it returns java.io.Serializable not Long type. RSQL JPA type Issue

After dig into hibernate code, I found that, for User.id, its set in AbstractPersistable<Long>, hibernate will not return the generic type Long.

Solution

Currently, I haven't found a better solution, but changing the User won't extend AbstractPersistable<Long> to define the primary key will work.