Freitag, 11. Dezember 2009

Generic Aliases/Joins with Hibernate Criteria

hibernate

In my last project I had the demand to create generic hibernate queries for any associations paths through the entity objects graph. The next code is an example where I want to filter users by street name and by role name. The association paths are "addresses.street" and "role.name" as you can see in the UserManager.

public class User {
   private String id;

   private Set<Address>; addresses = new HashSet<Address>();

   private Role role;

}

public class Address {
    private String id;

    private String street;

    ...
}

public class Role {
    private String id;

    private String name;

    ...
}

public UserDao {
    public List<User> get(String filterPath, String value) {
        Query query = new Query("from User u where u." + filterPath + "="+value)
        return query.list();      
    }
}

public UserManager {
    /** Dosen't work because addresses is a collection **/
    public List<User> getByStreet(String street) {
        return userDao.get("addresses.street", "Longstreet");
    }
    /** This works **/
    public List<User> getByRoleName(String roleName) {
        return userDao.get("role.name", "roleName");
    }
}

The problem is we have to insert joins and create aliases to make restrictions work via collections. Doing this in a generic way is pretty error-prone. Let's try this with Criteria instead of Query.

With criteria we have to create "subcriterias" on the root criteria for every join. We also have to consider not create multiple subcriterias for the same node in the object graph because this ends in an exception. The public "createAlias" methods of the following code adds subcriterias on a criteria a returns the alias for it. If you don't want to make a inner join you can specify it with CriteriaSpecification.

public static String createAlias ( Criteria criteria, String associationPath )
    {
        return createAlias( criteria, criteria, criteria.getAlias(),
                associationPath, CriteriaSpecification.INNER_JOIN );
    }

    public static String createAlias ( Criteria criteria, String associationPath,
                                       int joinType )
    {
        return createAlias( criteria, criteria, criteria.getAlias(),
                associationPath, joinType );
    }

    private static String createAlias ( Criteria rootCriteria,
                                        Criteria currentSubCriteria, String alias,
                                        String associationPath, int joinType )
    {
        StringTokenizer st = new StringTokenizer( associationPath, "." );
        if ( st.countTokens() == 1 )
        {
            return alias + "." + associationPath;
        }
        else
        {
            String associationPathToken = st.nextToken();
            String newAlias = alias + "_" + associationPathToken;
            Criteria criteriaForAlias = findCriteriaForAlias( rootCriteria,
                    newAlias );
            if ( criteriaForAlias == null )
            {
                currentSubCriteria = currentSubCriteria.createCriteria(
                        associationPathToken, newAlias, joinType );
            }
            else
            {
                currentSubCriteria = criteriaForAlias;
            }

            String newKey = associationPath.substring( associationPathToken
                    .length() + 1, associationPath.length() );
            return createAlias( rootCriteria, currentSubCriteria, newAlias,
                    newKey, joinType );
        }
    }

    public static Criteria findCriteriaForAlias ( Criteria criteria, String alias )
    {
        Iterator subcriterias = ( ( CriteriaImpl ) criteria )
                .iterateSubcriteria();
        while ( subcriterias.hasNext() )
        {
            Criteria subcriteria = ( Criteria ) subcriterias.next();
            if ( subcriteria.getAlias().equals( alias ) )
            {
                return subcriteria;
            }
        }
        return null;
    }

Now it's possible to make queries with association paths via collections into to the depths of our object graph.

public UserDao {    
    public List<User> getStreetStartingWithA() {
        Criteria criteria = new Criteria(User.class);
        String streetAlias = createAlias(criteria, "addresses.street");
        criteria.add(Restrictions.like( streetAlias, "A", MatchMode.START);
        return criteria.list();
    }

    public List getStreetNamesOrderedAsc() {
        Criteria criteria = new Criteria(User.class);
        String streetAlias = createAlias(criteria, "addresses.street");
        criteria.add( Projections.distinct( streetAlias);
        criteria.addOrder( Order.asc( streetAlias ) );
        return criteria.list();
    }
}


The next thing is to add some classes to make adding restrictions more convenient.
With the "createCriteria" method it's possible create a criteria and add restrictions from FilterCriterias.

public UserDao {    
    public List<User> getStreetStartingWithA() {
        Filter filter = new Filter();
        filter.addFilterCriteria( "addresses.street", "A", Matcher.START);
        Criteria criteria = createCriteria( session, User.class, filter);
        return criteria.list();
    }
}

    public static Criteria createCriteria ( Session session, Class persistentClass, Filter filter )
    {
        String alias = persistentClass.getSimpleName().toLowerCase();
        Criteria criteria = session.createCriteria( persistentClass,
                alias );

        if ( filter != null )
        {
            for ( FilterCriteria filterCriteria : filter.getFilterCriterias() )
            {
                String restrictionPath = createAlias( criteria,
                        filterCriteria.getPath() );
                if ( filterCriteria.getValue() instanceof String )
                {
                    criteria.add( Restrictions.like( restrictionPath,
                            ( String ) filterCriteria.getValue(), filterCriteria
                                    .getMatcher().getMatchMode() ) );
                }
                else if ( Collection.class.isAssignableFrom( filterCriteria.getValue().getClass() ) )
                {
                    Collection coll = ( Collection ) filterCriteria.getValue();
                    criteria.add( Restrictions.in( restrictionPath,
                            coll ) );
                }else if ( Object[].class.isAssignableFrom( filterCriteria.getValue().getClass() ) )
                {
                    Object[] coll = ( Object[] ) filterCriteria.getValue();
                    criteria.add( Restrictions.in( restrictionPath,
                            coll ) );
                }
                else
                {
                    criteria.add( Restrictions.eq( restrictionPath,
                            filterCriteria.getValue() ) );
                }

            }
        }
        return criteria;
    }

public class Filter
{

    private List< FilterCriteria > filterCriterias = new ArrayList< FilterCriteria >();
    public Class getEntityClass()
    {
        return entityClass;
    }

    public List< FilterCriteria > getFilterCriterias()
    {
        return filterCriterias;
    }

    public void addFilterCriteria( String path, Object value )
    {
        filterCriterias.add( new FilterCriteria( path, value ) );
    }

    public void addFilterCriteria( String path, Object value, Matcher matcher )
    {
        filterCriterias.add( new FilterCriteria( path, value, matcher ) );
    }

}

public class FilterCriteria
{

    private Matcher matcher = Matcher.EXACT;

    private String path;

    private Object value;

    public FilterCriteria ( String path, Object value )
    {
        this.path = path;
        this.value = value;
    }

    public FilterCriteria ( String path, Object value, Matcher matcher )
    {
        this.path = path;
        this.value = value;
        this.matcher = matcher;
    }

    public Matcher getMatcher()
    {
        return matcher;
    }

    public Object getValue()
    {
        return value;
    }

    public String getPath()
    {
        return path;
    }

}

public enum Matcher
{
    EXACT( "filter.matcher.exact", MatchMode.EXACT ), ANYWHERE(
            "filter.matcher.anywhere", MatchMode.ANYWHERE ), START(
            "filter.matcher.start", MatchMode.START ), END(
            "filter.matcher.end", MatchMode.END );

    private String messageKey;

    private MatchMode matchMode;

    Matcher ( String messageKey, MatchMode matchMode )
    {
        this.messageKey = messageKey;
        this.matchMode = matchMode;
    }

    public String getMessageKey()
    {
        return messageKey;
    }

    public MatchMode getMatchMode()
    {
        return matchMode;
    }
}

1 Kommentar:

  1. Collection.class.isAssignableFrom( filterCriteria.getValue().getClass() )

    should be written as

    filterCriteria.getValue() instanceof Collection



    Object[].class.isAssignableFrom( filterCriteria.getValue().getClass() )

    should be written as

    filterCriteria.getValue() instanceof Object[]

    AntwortenLöschen