From 30fec4be79df9fe99b9c6071fd30610f1f246cf8 Mon Sep 17 00:00:00 2001 From: Vlad Mihalcea Date: Tue, 5 Jun 2018 14:10:29 +0300 Subject: [PATCH] HHH-12662 - JPQL queries fail when using the Java attribute type which has an associated AttributeConverter (only the DB column type works) --- .../chapters/domain/basic_types.adoc | 61 ++++++ .../internal/QueryParameterBindingImpl.java | 2 +- .../test/converter/ConverterTest.java | 186 ++++++++++++++++++ 3 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/test/converter/ConverterTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/basic_types.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/basic_types.adoc index 4f720b9d8d..128bb500ed 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/basic_types.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/basic_types.adoc @@ -3,6 +3,7 @@ :modeldir: ../../../../../main/java/org/hibernate/userguide/model :sourcedir: ../../../../../test/java/org/hibernate/userguide/mapping :resourcedir: ../../../../../test/resources/org/hibernate/userguide/ +:converter-sourcedir: ../../../../../../../hibernate-core/src/test/java/org/hibernate/test/converter :extrasdir: extras Basic value types usually map a single database column, to a single, non-aggregated Java type. @@ -536,6 +537,66 @@ JPA explicitly disallows the use of an AttributeConverter with an attribute mark So if using the AttributeConverter approach, be sure not to mark the attribute as `@Enumerated`. ==== +[[basic-attribute-converter-query-parameter]] +====== Using the AttributeConverter entity property as a query parameter + +Assuming you have the following entity: + +[[basic-attribute-converter-query-parameter-entity-example]] +.`Photo` entity with `AttributeConverter` +==== +[source, JAVA, indent=0] +---- +include::{converter-sourcedir}/ConverterTest.java[tags=basic-attribute-converter-query-parameter-entity-example] +---- +==== + +And the `Caption` class looks as follows: + +[[basic-attribute-converter-query-parameter-object-example]] +.`Caption` Java object +==== +[source, JAVA, indent=0] +---- +include::{converter-sourcedir}/ConverterTest.java[tags=basic-attribute-converter-query-parameter-object-example] +---- +==== + +And we have an `AttributeConverter` to handle the `Caption` Java object: + +[[basic-attribute-converter-query-parameter-converter-example]] +.`Caption` Java object AttributeConverter +==== +[source, JAVA, indent=0] +---- +include::{converter-sourcedir}/ConverterTest.java[tags=basic-attribute-converter-query-parameter-converter-example] +---- +==== + +Traditionally, you could only use the dbData `Caption` representation, which in our case is a `String`, when referencing the `caption` entity property. + +[[basic-attribute-converter-query-parameter-converter-dbdata-example]] +.Filtering by the `Caption` property using the DB data representation +==== +[source, JAVA, indent=0] +---- +include::{converter-sourcedir}/ConverterTest.java[tags=basic-attribute-converter-query-parameter-converter-dbdata-example] +---- +==== + +In order to use the Java object `Caption` representation, you have to get the associated Hibernate `Type`. + +[[basic-attribute-converter-query-parameter-converter-object-example]] +.Filtering by the `Caption` property using the Java Object representation +==== +[source, JAVA, indent=0] +---- +include::{converter-sourcedir}/ConverterTest.java[tags=basic-attribute-converter-query-parameter-converter-object-example] +---- +==== + +By passing the associated Hibernate `Type`, you can use the `Caption` object when binding the query parameter value. + [[basic-hbm-attribute-converter]] ====== Mapping an AttributeConverter using HBM mappings diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingImpl.java index 04c2549974..2e36931b22 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingImpl.java @@ -62,10 +62,10 @@ public class QueryParameterBindingImpl implements QueryParameterBinding { if ( isBindingValidationRequired ) { validate( value, clarifiedType ); } - bindValue( value ); if ( clarifiedType != null ) { this.bindType = clarifiedType; } + bindValue( value ); } @Override diff --git a/hibernate-core/src/test/java/org/hibernate/test/converter/ConverterTest.java b/hibernate-core/src/test/java/org/hibernate/test/converter/ConverterTest.java new file mode 100644 index 0000000000..c934533796 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/converter/ConverterTest.java @@ -0,0 +1,186 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.converter; + +import javax.persistence.AttributeConverter; +import javax.persistence.Convert; +import javax.persistence.Entity; +import javax.persistence.Id; + +import org.hibernate.SessionFactory; +import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase; +import org.hibernate.metamodel.spi.MetamodelImplementor; +import org.hibernate.query.Query; +import org.hibernate.type.Type; +import org.hibernate.type.descriptor.converter.AttributeConverterTypeAdapter; + +import org.hibernate.testing.TestForIssue; +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@TestForIssue( jiraKey = "HHH-12662") +public class ConverterTest extends BaseEntityManagerFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Photo.class }; + } + + @Override + protected void afterEntityManagerFactoryBuilt() { + doInJPA( this::entityManagerFactory, entityManager -> { + Photo photo = new Photo(); + photo.setId( 1 ); + photo.setName( "Dorobanțul" ); + photo.setCaption( new Caption( "Nicolae Grigorescu" ) ); + + entityManager.persist( photo ); + } ); + } + + @Test + public void testJPQLUpperDbValueBindParameter() throws Exception { + doInJPA( this::entityManagerFactory, entityManager -> { + //tag::basic-attribute-converter-query-parameter-converter-dbdata-example[] + Photo photo = entityManager.createQuery( + "select p " + + "from Photo p " + + "where upper(caption) = upper(:caption) ", Photo.class ) + .setParameter( "caption", "Nicolae Grigorescu" ) + .getSingleResult(); + //end::basic-attribute-converter-query-parameter-converter-dbdata-example[] + + assertEquals( "Dorobanțul", photo.getName() ); + } ); + } + + @Test + public void testJPQLUpperAttributeValueBindParameterType() throws Exception { + doInJPA( this::entityManagerFactory, entityManager -> { + //tag::basic-attribute-converter-query-parameter-converter-object-example[] + SessionFactory sessionFactory = entityManager.getEntityManagerFactory() + .unwrap( SessionFactory.class ); + + MetamodelImplementor metamodelImplementor = (MetamodelImplementor) sessionFactory.getMetamodel(); + + Type captionType = metamodelImplementor + .entityPersister( Photo.class.getName() ) + .getPropertyType( "caption" ); + + Photo photo = (Photo) entityManager.createQuery( + "select p " + + "from Photo p " + + "where upper(caption) = upper(:caption) ", Photo.class ) + .unwrap( Query.class ) + .setParameter( "caption", new Caption("Nicolae Grigorescu"), captionType) + .getSingleResult(); + //end::basic-attribute-converter-query-parameter-converter-object-example[] + + assertEquals( "Dorobanțul", photo.getName() ); + } ); + } + + //tag::basic-attribute-converter-query-parameter-object-example[] + public static class Caption { + + private String text; + + public Caption(String text) { + this.text = text; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Caption caption = (Caption) o; + return text != null ? text.equals( caption.text ) : caption.text == null; + + } + + @Override + public int hashCode() { + return text != null ? text.hashCode() : 0; + } + } + //end::basic-attribute-converter-query-parameter-object-example[] + + //tag::basic-attribute-converter-query-parameter-converter-example[] + public static class CaptionConverter + implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(Caption attribute) { + return attribute.getText(); + } + + @Override + public Caption convertToEntityAttribute(String dbData) { + return new Caption( dbData ); + } + } + //end::basic-attribute-converter-query-parameter-converter-example[] + + //tag::basic-attribute-converter-query-parameter-entity-example[] + @Entity(name = "Photo") + public static class Photo { + + @Id + private Integer id; + + private String name; + + @Convert(converter = CaptionConverter.class) + private Caption caption; + + //Getters and setters are omitted for brevity + //end::basic-attribute-converter-query-parameter-entity-example[] + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Caption getCaption() { + return caption; + } + + public void setCaption(Caption caption) { + this.caption = caption; + } + //tag::basic-attribute-converter-query-parameter-entity-example[] + } + //end::basic-attribute-converter-query-parameter-entity-example[] +}