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
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;
}
}