stackh34p
stackh34p

Reputation: 8999

Hibernate-generated column aliases break AliasToBeanResultTransformer when using query

What I am trying to achieve is to set a result transformer on a query defined in the following way:

String hqlQueryString = "select o.id as id, o.name as objectName from MyObject"; 
Class resultClass = MyObject.class;
Query query = session.createQuery(hqlQueryString).setResultTransformer(
        new new AliasToBeanResultTransformer(resultClass));
List result = query.list();

MyObject looks like this:

public class MyObject {
    private int id;
    private String objectName;

    public int getId() {
        return id;
    }
    public void setId(int value) {
        this.id = value;
    }

    public String getObjectName() {
        return objectName;
    }
    public void setobjectName(String value) {
        this.objectName = value;
    }
}

The problem is, that although I have specified id and objectName to be my aliases, the actual query being executed uses different aliases. This causes my AliasToBeanResultTransformer to fail to construct MyObject because the aliases do not match property names.

Is it possible to obtain the aliases of the query generated by hibernate programmatically (I can set them to the alias to bean result tranformer)? I tried using query.getReturnAliases() but it returns the aliases that I have defined in my HQL, not the ones that Hibernate actually uses.

Can I explicitly specify the aliases in a createQuery statement? Currently I am trying not to use criterion for this to work, so I'd appreciate an approach that uses query objects, if such exists.


Update

Although the issue described above is invalid for standard HQL queries (see comments), it is valid when executing a native query. To be specific - native queries seemed to treat all aliases as lowecase strings (despite specific capitalization that might have been introduced in the query). This causes the AliasToBeanResultTransformer to fail when setting the properties, in cases where capitalization matters.

Upvotes: 2

Views: 12507

Answers (2)

stackh34p
stackh34p

Reputation: 8999

As for native queries, there was no simple solution involved. I had to look into the implementation of the AliasToBeanResultTransformer class and put a fix in there. I resolved the problem by creating a copy of the AliasToBeanResultTransformer class and modified the private initialize method of that class in the following way:

public class CaseInsensitiveAliasToBeanResultTransformer {
    private void initialize(String[] aliases) {
        this.aliases = new String[ aliases.length ];
        setters = new Setter[aliases.length];
        for ( int i = 0; i < aliases.length; i++ ) {
            String alias = aliases[i];
            if (alias != null) {
                this.aliases[i] = alias;
                setters[i] = CaseInsensitiveSetter.getSetter(resultClass, alias);
            }
        }
        isInitialized = true;
    }
}

This code differs mainly in the line CaseInsensitiveSetter.getSetter(resultClass, alias), where I have introduced a CaseInsensitiveSetter class I will describe below. This class implements the Setter interface and allows retrieving the setter method of a class using case-insensitive matching - so this will allow me to bind the lower-cased query aliases to the proper members of my result class. Here is the code of the custom setter (only the important lines are shown for brevity):

public class CaseInsensitiveSetter {

    public static Setter getSetter(Class<?> theClass, String propertyName) {

        Setter setter;

        if (theClass == Object.class || theClass == null) {
            setter = null;
        } else {
            setter = doGetSetter(theClass, propertyName);

            if (setter != null) {
                if (!ReflectHelper.isPublic(theClass, setter.getMethod())) {
                    setter.getMethod().setAccessible(true);
                }
            } else {
                setter = doGetSetter(theClass.getSuperclass(), propertyName);
                if (setter == null) {
                    Class<?>[] interfaces = theClass.getInterfaces();
                    for (int i = 0; setter == null && i < interfaces.length; i++) {
                        setter = doGetSetter( interfaces[i], propertyName);
                    }
                }
            }
            if (setter == null) {
                throw new PropertyNotFoundException( 
                    "Could not find a setter for property " + 
                    propertyName + " in class " + theClass.getName());
            }
        }
        return setter;
    }

    // The actual work is done here
    private static Setter doGetSetter(Class<?> resultClass, String propertyName) {

        Method[] methods = resultClass.getDeclaredMethods();
        for (int i = 0; i < methods.length; i++) {
            // only carry on if the method has 1 parameter
            if ( methods[i].getParameterTypes().length == 1 ) {
                String methodName = methods[i].getName();

                if (methodName.startsWith("set")) {
                    String testStdMethod = methodName.substring(3);
                    if (testStdMethod.equalsIgnoreCase(propertyName)) {
                        Setter result = new CustomSetter(
                            resultClass, methods[i], propertyName);
                        return result;
                    }
                }
            }
        }
        return null;
    }
}

The source of this is based on the BaseSetter class that comes with Hibernate, but is changed to support case-insensitive matching. Still, this one, and the original class that Hibernate uses, lacks performance because of the heavy usage of reflection.

Also, keep in mind that if the result class contains different properties with names that would be equal in case-insensitive comparison, then only one of them will be picked by the current code and it might not work as expected.

Upvotes: 1

Ken Chan
Ken Chan

Reputation: 90447

Actually don't need to implement another AliasToBeanResultTransformer , you can use addScalar(String columnAlias, Type type) to explicitly alias the columns of the native SQL:

String nativeSQL = "select o.id as id, o.name as objectName from MyObject"; 
List<MyObject> resultList = session.createSQLQuery(nativeSQL)
        .addScalar("id" ,StandardBasicTypes.INTEGER)
        .addScalar("objectName",StandardBasicTypes.STRING)
        .setResultTransformer(new AliasToBeanResultTransformer(MyObject.class))
        .list();

The transformer will then look for a MyObject class and expect it having the setters setId() and setObjectName() in order to populate the returned values to the MyObject instance

Upvotes: 7

Related Questions