Hibernate Version: 6.6.23.Final
We are migrating from Hibernate 5 to Hibernate 6 and noticed a behavior change involving JPA @Converter
and Criteria queries.
In Hibernate 5, when using a @Converter(autoApply = true)
on a String
attribute, the converter was invoked for both persistence and query parameter binding, even if the attribute was wrapped in a function like cb.lower()
.
In Hibernate 6, when we use CriteriaBuilder.lower(root.get("field"))
inside a like
predicate, the converter is not invoked for the parameter value. As a result, custom normalization logic (such as trimming or empty-string-to-null handling) is skipped. This leads to queries that used to work in Hibernate 5 no longer returning results in Hibernate 6.
<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="templatePU" transaction-type="RESOURCE_LOCAL">
<description>Hibernate test case template Persistence Unit</description>
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="hibernate.archive.autodetection" value="class, hbm"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.connection.driver_class" value="org.h2.Driver"/>
<property name="hibernate.connection.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1"/>
<property name="hibernate.connection.username" value="sa"/>
<property name="hibernate.connection.pool_size" value="5"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="create-drop"/>
<property name="hibernate.max_fetch_depth" value="5"/>
<property name="hibernate.cache.region_prefix" value="hibernate.test"/>
<property name="hibernate.cache.region.factory_class"
value="org.hibernate.testing.cache.CachingRegionFactory"/>
<!--NOTE: hibernate.jdbc.batch_versioned_data should be set to false when testing with Oracle-->
<property name="hibernate.jdbc.batch_versioned_data" value="true"/>
<property name="jakarta.persistence.validation.mode" value="NONE"/>
<property name="hibernate.service.allow_crawling" value="false"/>
<property name="hibernate.session.events.log" value="true"/>
</properties>
</persistence-unit>
</persistence>
package org.hibernate.entity;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Convert;
import jakarta.persistence.Converter;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
public class TestEntity {
@Converter(autoApply = true)
public static class SetConverter implements AttributeConverter<String, String> {
@Override
public String convertToDatabaseColumn(final String attribute) {
return attribute.trim();
}
@Override
public String convertToEntityAttribute(final String dbData) {
return dbData;
}
}
@Id
@GeneratedValue
public Long id;
@Convert(converter = SetConverter.class)
public String descriptions;
}
package org.hibernate.bugs;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import org.hibernate.entity.TestEntity;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import java.util.List;
/**
* This template demonstrates how to develop a test case for Hibernate ORM, using the Java Persistence API.
*/
public class JPAUnitTestCase {
private EntityManagerFactory entityManagerFactory;
@Before
public void init() {
entityManagerFactory = Persistence.createEntityManagerFactory("templatePU");
}
@After
public void destroy() {
entityManagerFactory.close();
}
// Entities are auto-discovered, so just add them anywhere on class-path
// Add your tests, using standard JUnit.
@Test
public void hhh17693Test() {
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
final TestEntity entity = new TestEntity();
entity.descriptions = "P_1";
entityManager.persist(entity);
entityManager.getTransaction().commit();
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<TestEntity> cq = cb.createQuery(TestEntity.class);
Root<TestEntity> root = cq.from(TestEntity.class);
// Create the LIKE predicate
Predicate likePredicate = cb.like(cb.lower(root.get("descriptions")), cb.lower(cb.literal(" P_1")), '\\');
cq.select(root).where(likePredicate);
List<TestEntity> results = entityManager.createQuery(cq).getResultList();
Assertions.assertEquals(1, results.size());
entityManager.close();
}
}