Search code examples
javahibernatehqlprojectionresulttransformer

Hibernate HQL Projection Issue with Set Using AliasToBeanResultTransformer


I am having a problem with Hibernate HQL Projection using the AliasToBeanResultTransformer, basically the result I am trying to return isn't being mapped properly to the bean, here is the situation:

The HQL query that I am using is this:

select entity.categoryTypes as categoryTypes from nz.co.doltech.ims.server.entities.IncidentEntity entity where (entity.id = :id105019)

I want to get the CategoryType's from the IncidentEntity based on its join relationship. This works fine when I'm not attempting to use any transformer on it. categoryTypes is a Set and the transformer keeps trying to check the Method's parameter types and fails because instead of finding a CategoryTypeEntity it finds a java.util.Set as if its trying to map a single CategoryTypeEntity into the categoryTypes field. I would have thought that because its a Set it would pull the data out as a Set and then try map it to the categoryTypes field. Apparently not though.

@javax.persistence.Entity(name = "incidents")
@Cache(usage=CacheConcurrencyStrategy.TRANSACTIONAL)
public class IncidentEntity implements Entity {

    ...

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "incident_categorytype", joinColumns = { 
            @JoinColumn(name = "incident_id", nullable = false, updatable = false) }, 
        inverseJoinColumns = {
            @JoinColumn(name = "categorytype_id", nullable = false, updatable = false) 
    })
    private Set<CategoryTypeEntity> categoryTypes = new HashSet<CategoryTypeEntity>();

    ...

    public Set<CategoryTypeEntity> getCategoryTypes() {
        return categoryTypes;
    }
    public void setCategoryTypes(Set<CategoryTypeEntity> categoryTypes) {
        this.categoryTypes = categoryTypes;
    }
}

Here is the call I make:

Query query = session.createQuery("select entity.categoryTypes as categoryTypes from nz.co.doltech.ims.server.entities.IncidentEntity entity " + 
                                  "where (entity.id = :id105019)")

query.setResultTransformer(Transformers.aliasToBean(IncidentEntity.class));

return query.list();

The exceptions I get are:

Caused by: org.hibernate.PropertyAccessException: IllegalArgumentException occurred while calling setter of nz.co.doltech.ims.server.entities.IncidentEntity.categoryTypes
...
Caused by: java.lang.IllegalArgumentException: argument type mismatch

And the hibernate log message is:

Jun 27, 2014 12:32:11 AM org.hibernate.property.BasicPropertyAccessor$BasicSetter set
SEVERE: IllegalArgumentException in class: nz.co.doltech.ims.server.entities.IncidentEntity, setter method of property: categoryTypes
Jun 27, 2014 12:32:11 AM org.hibernate.property.BasicPropertyAccessor$BasicSetter set
SEVERE: expected type: java.util.Set, actual value: nz.co.doltech.ims.server.entities.CategoryTypeEntity


Using Hibernate 3.6.10

Can anyone see what is going on here? It really doesn't seem like normal behavior, perhaps I have done something wrong. Would appreciate any help I can get!

UPDATE: This is strange, not directly related to the issue. When I have hibernates use_query_cache property set to true I keep getting the projection result as null in the AliasToBeanResultTransformer (then the result returns as null (or [null, null, null] depending on how many are returned. I think this might be a bug? In regards to the issue at hand, when I remove the result transformer it returns 3 CategoryTypeEntites as expected. When its added I get one CategoryTypeEntity that's being processed in the Transformers transformTuple method. Really confused about both of these issues.

Cheers, Ben


Solution

  • Manage to resolve this issue by rewriting the AliasToBeanResultTransformer class. It will not insert into a collection if the collection types match and the collections generic type match. I also found a great nested bean transformer make by samiandoni that will allow me to map nested projection values too :) Here is how I implemented it for anyone else having this same issue:

    @SuppressWarnings({"serial","rawtypes"})
    public class AliasToBeanResultTransformer implements ResultTransformer, Serializable {
    
        // IMPL NOTE : due to the delayed population of setters (setters cached
        //      for performance), we really cannot properly define equality for
        //      this transformer
    
        private final Class resultClass;
        private boolean isInitialized;
        private String[] aliases;
    
        private Setter[] setters;
        private Getter[] getters;
    
        public AliasToBeanResultTransformer(Class resultClass) {
            if ( resultClass == null ) {
                throw new IllegalArgumentException( "resultClass cannot be null" );
            }
            isInitialized = false;
            this.resultClass = resultClass;
        }
    
        @Override
        public Object transformTuple(Object[] tuple, String[] aliases) {
            Object result;
    
            try {
                if ( ! isInitialized ) {
                    initialize( aliases );
                }
                else {
                    check( aliases );
                }
    
                result = resultClass.newInstance();
    
                for ( int i = 0; i < aliases.length; i++ ) {
                    Setter setter = setters[i];
                    if ( setter != null ) {
                        Class paramType = setter.getMethod().getParameterTypes()[0];
    
                        if(paramType != null) {
                            Object obj = tuple[i];
                            // Check if parameter is a collection
                            if(!obj.getClass().equals(paramType) && isCollection(paramType)) {
                                insertToList(result, obj, getters[i], aliases[i]);
                            }
                            else {
                                setter.set( result, obj, null );
                            }
                        }
                    }
                }
            }
            catch ( InstantiationException e ) {
                throw new HibernateException( "Could not instantiate resultclass: " + resultClass.getName() );
            }
            catch ( IllegalAccessException e ) {
                throw new HibernateException( "Could not instantiate resultclass: " + resultClass.getName() );
            }
    
            return result;
        }
    
        @Override
        public List transformList(List collection) {
            return collection;
        }
    
        @SuppressWarnings("unchecked")
        private boolean insertToList(Object result, Object obj, Getter getter, String alias) {
            Class genClass;
            try {
                genClass = ReflectUtils.getGenericType(resultClass.getDeclaredField(alias));
    
                // Check if the collection can take the obj
                if(genClass.equals(obj.getClass())) {
                    Collection collection = (Collection) getter.get(result);
                    collection.add(obj);
                    return true;
                }
            } catch (NoSuchFieldException | SecurityException e) {}
    
            return false;
        }
    
        private void initialize(String[] aliases) {
            PropertyAccessor propertyAccessor = new ChainedPropertyAccessor(
                    new PropertyAccessor[] {
                            PropertyAccessorFactory.getPropertyAccessor( resultClass, null ),
                            PropertyAccessorFactory.getPropertyAccessor( "field" )
                    }
            );
            this.aliases = new String[ aliases.length ];
            setters = new Setter[ aliases.length ];
            getters = new Getter[ aliases.length ];
            for ( int i = 0; i < aliases.length; i++ ) {
                String alias = aliases[ i ];
                if ( alias != null ) {
                    this.aliases[ i ] = alias;
                    setters[ i ] = propertyAccessor.getSetter( resultClass, alias );
                    getters[ i ] = propertyAccessor.getGetter( resultClass, alias );
                }
            }
            isInitialized = true;
        }
    
        private void check(String[] aliases) {
            if ( ! Arrays.equals( aliases, this.aliases ) ) {
                throw new IllegalStateException(
                        "aliases are different from what is cached; aliases=" + Arrays.asList( aliases ) +
                                " cached=" + Arrays.asList( this.aliases ) );
            }
        }
    
        private boolean isCollection(Class clazz) {
            return Collection.class.isAssignableFrom(clazz);
        }
    
        public boolean equals(Object o) {
            if ( this == o ) {
                return true;
            }
            if ( o == null || getClass() != o.getClass() ) {
                return false;
            }
    
            AliasToBeanResultTransformer that = ( AliasToBeanResultTransformer ) o;
    
            if ( ! resultClass.equals( that.resultClass ) ) {
                return false;
            }
            if ( ! Arrays.equals( aliases, that.aliases ) ) {
                return false;
            }
    
            return true;
        }
    
        public int hashCode() {
            int result = resultClass.hashCode();
            result = 31 * result + ( aliases != null ? Arrays.hashCode( aliases ) : 0 );
            return result;
        }
    }
    

    You will need to implement this RefectUtil method too:

    public static Class<?> getGenericType(Field field) {
        ParameterizedType type = (ParameterizedType) field.getGenericType();
        return (Class<?>) type.getActualTypeArguments()[0];
    }
    

    Then you can make it work with samiandoni's transformer too (just ensure its using your edited AliasToBeanResultTransformer class):

    /**
     * @author samiandoni
     *
     */
    @SuppressWarnings("rawtypes")
    public class AliasToBeanNestedResultTransformer implements ResultTransformer, Serializable {
    
        private static final long serialVersionUID = -8047276133980128266L;
    
        private final Class<?> resultClass;
    
        public AliasToBeanNestedResultTransformer(Class<?> resultClass) {
            this.resultClass = resultClass;
        }
    
        @SuppressWarnings("unchecked")
        public Object transformTuple(Object[] tuple, String[] aliases) {
    
            Map<Class<?>, List<?>> subclassToAlias = new HashMap<Class<?>, List<?>>();
            List<String> nestedAliases = new ArrayList<String>();
    
            try {
                for (int i = 0; i < aliases.length; i++) {
                    String alias = aliases[i];
                    if (alias.contains(".")) {
                        nestedAliases.add(alias);
    
                        String[] sp = alias.split("\\.");
                        String fieldName = sp[0];
                        String aliasName = sp[1];
    
                        Class<?> subclass = resultClass.getDeclaredField(fieldName).getType();
    
                        if (!subclassToAlias.containsKey(subclass)) {
                            List<Object> list = new ArrayList<Object>();
                            list.add(new ArrayList<Object>());
                            list.add(new ArrayList<String>());
                            list.add(fieldName);
                            subclassToAlias.put(subclass, list);
                        }
                        ((List<Object>)subclassToAlias.get(subclass).get(0)).add(tuple[i]);
                        ((List<String>)subclassToAlias.get(subclass).get(1)).add(aliasName);
                    }
                }
            }
            catch (NoSuchFieldException e) {
                throw new HibernateException( "Could not instantiate resultclass: " + resultClass.getName() );
            }
    
            Object[] newTuple = new Object[aliases.length - nestedAliases.size()];
            String[] newAliases = new String[aliases.length - nestedAliases.size()];
            int i = 0;
            for (int j = 0; j < aliases.length; j++) {
                if (!nestedAliases.contains(aliases[j])) {
                    newTuple[i] = tuple[j];
                    newAliases[i] = aliases[j];
                    ++i;
                }
            }
    
            ResultTransformer rootTransformer = new AliasToBeanResultTransformer(resultClass);
            Object root = rootTransformer.transformTuple(newTuple, newAliases);
    
            for (Class<?> subclass : subclassToAlias.keySet()) {
                ResultTransformer subclassTransformer = new AliasToBeanResultTransformer(subclass);
                Object subObject = subclassTransformer.transformTuple(
                    ((List<Object>)subclassToAlias.get(subclass).get(0)).toArray(), 
                    ((List<Object>)subclassToAlias.get(subclass).get(1)).toArray(new String[0])
                );
    
                PropertyAccessor accessor = PropertyAccessorFactory.getPropertyAccessor("property");
                accessor.getSetter(resultClass, (String)subclassToAlias.get(subclass).get(2)).set(root, subObject, null);
            }
    
            return root;
        }
    
        @Override
        public List transformList(List collection) {
            return collection;
        }
    }