Cannot use 'like' operand when an AttributeConverter is used to convert the attribute in a String

Description

It seems like it is not possible to use “like” operand on a non-string property even if an AttributeConverter is defined, converting it in a String to be stored in the database.

For example, given an entity like this one

@Entity public class TestEntity { public static class SetConverter implements AttributeConverter<Set<String>, String> { @Override public String convertToDatabaseColumn(final Set<String> attribute) { if (attribute != null && !attribute.isEmpty()) { return String.join(",", attribute); } return null; } @Override public Set<String> convertToEntityAttribute(final String dbData) { if (dbData != null) { return Arrays.stream(dbData.split(",")).collect(Collectors.toSet()); } return null; } } @Id @GeneratedValue public Long id; @Convert(converter = SetConverter.class) public Set<String> descriptions; }

It is not possible to execute a query like

entityManager.createQuery( "select e from TestEntity e where e.descriptions like :text", TestEntity.class ) .setParameter("text", "%,P_2,%") .getResultList();

Hibernate raises this exception

java.lang.IllegalArgumentException: org.hibernate.query.SemanticException: Operand of 'like' is of type 'java.util.Set' which is not a string (it is not an instance of 'java.lang.String' or 'char[]') 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.SessionImpl.createQuery(SessionImpl.java:198) at org.hibernate.bugs.JPAUnitTestCase.hhh123Test(JPAUnitTestCase.java:44) 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.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26) at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27) at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) at org.junit.runners.ParentRunner.run(ParentRunner.java:413) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69) at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38) at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11) at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55) Caused by: org.hibernate.query.SemanticException: Operand of 'like' is of type 'java.util.Set' which is not a string (it is not an instance of 'java.lang.String' or 'char[]') at org.hibernate.query.sqm.internal.TypecheckUtil.assertString(TypecheckUtil.java:513) at org.hibernate.query.sqm.tree.predicate.SqmLikePredicate.<init>(SqmLikePredicate.java:61) at org.hibernate.query.sqm.tree.predicate.SqmLikePredicate.<init>(SqmLikePredicate.java:85) at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitLikePredicate(SemanticQueryBuilder.java:2508) at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitLikePredicate(SemanticQueryBuilder.java:269) at org.hibernate.grammars.hql.HqlParser$LikePredicateContext.accept(HqlParser.java:6113) at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitWhereClause(SemanticQueryBuilder.java:2244) at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitWhereClause(SemanticQueryBuilder.java:269) at org.hibernate.grammars.hql.HqlParser$WhereClauseContext.accept(HqlParser.java:5905) at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitQuery(SemanticQueryBuilder.java:1159) at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitQuerySpecExpression(SemanticQueryBuilder.java:941) at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitQuerySpecExpression(SemanticQueryBuilder.java:269) at org.hibernate.grammars.hql.HqlParser$QuerySpecExpressionContext.accept(HqlParser.java:1869) at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitSimpleQueryGroup(SemanticQueryBuilder.java:926) at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitSimpleQueryGroup(SemanticQueryBuilder.java:269) at org.hibernate.grammars.hql.HqlParser$SimpleQueryGroupContext.accept(HqlParser.java:1740) at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitSelectStatement(SemanticQueryBuilder.java:443) at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitStatement(SemanticQueryBuilder.java:402) at org.hibernate.query.hql.internal.SemanticQueryBuilder.buildSemanticModel(SemanticQueryBuilder.java:311) at org.hibernate.query.hql.internal.StandardHqlTranslator.translate(StandardHqlTranslator.java:71) 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) ... 31 more

This is applicable also in case a generic object is converted in a String.

The problem seems caused by the fact Hibernate is checking just the Java type and not eventually the type on the database, that in this case is changed by the AttributeConverter.

Probably it is enough in class org.hibernate.query.sqm.internal.TypecheckUtil updating the assertString method to check also the relational java type (something like the example below)

public static void assertString(SqmExpression<?> expression) { final SqmExpressible<?> nodeType = expression.getNodeType(); if ( nodeType != null ) { final Class<?> javaType = nodeType.getExpressibleJavaType().getJavaTypeClass(); final Class<?> relationalJavaType = nodeType.getRelationalJavaType().getJavaTypeClass(); if ( javaType != String.class && javaType != char[].class && relationalJavaType != String.class && relationalJavaType != char[].class ) { throw new SemanticException( "Operand of 'like' is of type '" + nodeType.getTypeName() + "' which is not a string (it is not an instance of 'java.lang.String' or 'char[]')" ); } } }

Test Case

https://github.com/stefanogianelli/hibernate-issue-like-with-converter

Activity

Show:

Gavin King February 1, 2024 at 6:08 PM

Awesome, thanks!

Marco Belladelli February 1, 2024 at 3:53 PM

Thanks again , I tried following your suggestions in my pr. Let me know what you think.

Gavin King February 1, 2024 at 2:13 PM

Yeah, I agree that the Duration case is a bit tricky. But perhaps the solution is as simple as just introducing a DurationJdbcType.

Marco Belladelli February 1, 2024 at 1:23 PM

Alright then, I will try implementing it through JdbcType, the only thing I need to figure out is the correct check for assertDuration.

Gavin King February 1, 2024 at 1:13 PM
Edited

[And I would prefer to not fix this particular issue than to add hacks into TypecheckUtil, for reasons that I’ve clearly documented.]

Fixed

Details

Assignee

Reporter

Worked in

Sprint

Fix versions

Affects versions

Priority

Created January 31, 2024 at 3:07 PM
Updated May 30, 2024 at 12:02 PM
Resolved February 8, 2024 at 9:08 AM