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
-
add RSQL JPA dependency into
build.gradle
-
Change JPA Repository to extend
JpaSpecificationExecutor<EntityClass>
-
In the controller accept a query paratemer and pass it to service layer
-
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,
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
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
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 castString
toLong
.
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
@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.
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.