Persistent entity criteria API

Construct persistent entity criteria with advanced filtering, sorting and paging capabilities and pass them to DAO methods to query for desired results.

External resources

Overview

  • Project: daofusion-core
  • Package reference: com.anasoft.os.daofusion.criteria

Introduction

Having your domain model along with associated DAO classes implemented brings you to a question that might have arisen when inspecting PersistentEntityDao query methods: what is actually the PersistentEntityCriteria interface? This section explains the persistent entity criteria concept in detail so that you can bring the standard DAO functionality to a whole new level.

Persistent entity criteria API overview

If you look at the generic AbstractHibernateEntityDao implementation you will notice that it merely delegates its DAO operations to the underlying Hibernate Session. Things should be easy, right? One of the main benefits of having a standard DAO interface is the ability to define a custom criteria API that should cover 90% of the most typical use cases out there. This is exactly the purpose of the PersistentEntityCriteria interface and associated classes - to shield an average user from any Hibernate-specific implementation details, while allowing advanced users to use the underlying Hibernate Criteria API directly if necessary at the same time. In fact, using the Hibernate Criteria API directly in your DAO classes should be the last resort for all but the most complex cases where a custom PersistentEntityCriteria implementation is not possible (remember that even a custom PersistentEntityCriteria implementation can be efficiently and consistently reused between multiple DAOs as opposed to the direct Hibernate Criteria API approach).

PersistentEntityCriteria interface defines the general contract for persistent entity query constraints bound to the underlying Hibernate Criteria instance. DAO Fusion enforces the use of Hibernate Criteria API under the hood to ensure database portability (no dynamic HQL / JPQL / SQL generation involved). This makes DAO Fusion generic and reusable across multiple database vendors, assuming the provided dialect is up to date for the given database.

Let's take a brief look at this interface to get a basic understanding of the criteria API concept:

public interface PersistentEntityCriteria {

    /**
     * Applies query constraints defined by the persistent
     * entity criteria implementation to the targetCriteria.
     */
    void apply(Criteria targetCriteria);

}

As you can see, there is no rocket science going on here - implementations of the PersistentEntityCriteria interface are merely responsible for updating the Hibernate Criteria instance according to any query constraints they define. The simplicity of this interface is intentional so that users can write their own implementations with full control over Hibernate Criteria processing. Note that the AbstractHibernateEntityDao uses this interface when constructing the Hibernate Criteria instance via BaseHibernateDataAccessor's getCriteria method before setting any projections and requesting results via the Hibernate Criteria API.

AbstractCriterionGroup represents the base class for PersistentEntityCriteria implementations that manage PersistentEntityCriterion instances to define individual query constraints. AbstractCriterionGroup defines the contract for paging criteria (firstResult and maxResults) as well as the application of such criteria to the target Hibernate Criteria instance via the applyPagingCriteria method. This class also provides a basic implementation of PersistentEntityCriteria's apply method:

public void apply(Criteria targetCriteria) {
    // obtain the list of managed PersistentEntityCriterion instances
    List<T> criterionList = getObjectList();

    // obtain the criterion visitor
    V visitor = getCriterionVisitor(targetCriteria);

    // visit all PersistentEntityCriterion instances
    // (targetCriteria is updated for each visited criterion)
    for (T criterion : criterionList) {
        criterion.accept(visitor);
    }

    applyPagingCriteria(targetCriteria);
}

AbstractCriterionGroup uses the visitor pattern to impose centralized PersistentEntityCriterion processing in terms of applying individual query constraints (PersistentEntityCriterion instances) to the target Hibernate Criteria instance. The getCriterionVisitor method is declared as abstract so that arbitrary visitor implementations can be used by AbstractCriterionGroup subclasses.

So, what is this PersistentEntityCriterion interface all about? PersistentEntityCriterion represents an abstraction of a single constraint that uses the visitor pattern as shown above for applying its query constraints into the target Hibernate Criteria instance:

public interface PersistentEntityCriterion<V> {

    /**
     * Accepts the given visitor to visit this criterion.
     */
    void accept(V visitor);

}

Note that you can implement this interface on your own if you want to plug in your custom criterion implementations in conjunction with an appropriate AbstractCriterionGroup and a corresponding criterion visitor.

So far, this has been all too generic. The reason why we built such abstractions above the default PersistentEntityCriteria implementation is to make DAO Fusion entity criteria API as flexible and extensible as possible. We encourage users to provide their own PersistentEntityCriteria implementations in special or complex cases in favor of using the Hibernate Criteria API directly.

NestedPropertyCriteria is the default general purpose PersistentEntityCriteria implementation based on the AbstractCriterionGroup in conjunction with NestedPropertyCriterion instances. NestedPropertyCriteria implements the query constraint application logic regarding specific NestedPropertyCriterion subclasses via default NestedPropertyCriterionVisitor implementation (returned as the result of the getCriterionVisitor method):

public interface NestedPropertyCriterionVisitor {

    // updates the target Criteria according to FilterCriterion
    void visit(FilterCriterion criterion);

    // updates the target Criteria according to SortCriterion
    void visit(SortCriterion criterion);

}

In addition to managing NestedPropertyCriterion instances and providing a visitor that is able to process them, the NestedPropertyCriteria contains features that are common to either all or certain NestedPropertyCriterion instances:

  • filter object as an optional source of filter values (shared by all FilterCriterion instances)
  • use of an AssociationPathRegister for preprocessing root Hibernate Criteria regarding nested subcriteria (defined by NestedPropertyCriterion via the AssociationPath)

NestedPropertyCriterion supports a nested property of a persistent entity, starting at the given persistent entity as the root object and navigating through associated objects as necessary. In other words, NestedPropertyCriterion enables you to write a criterion that spans over multiple entities, reaching the desired property of the desired entity from the root object.

NestedPropertyCriterion is essentially a combination of two factors:

  • AssociationPath which points to the given property of the target persistent entity
  • targetPropertyName denoting target property of the given persistent entity

In Hibernate, you would normally use Session's createCriteria method to create constraints for nested properties of the given root entity. However, you would have to manage nested Criteria on your own regarding their reuse across multiple criterion instances (two criterion instances can share a common association path "prefix"). This can ultimately bring you to the duplicate association path Hibernate Criteria API issue which has serious implications on the way of writing complex queries for nested entity properties.

Now this is the part when AssociationPathRegister kicks in. AssociationPathRegister is simply a map of AssociationPath instances and corresponding Hibernate Criteria to be reused when modifying the root Criteria instance. NestedPropertyCriteria uses this class to initialize Hibernate Subcriteria mappings in a safe way, avoiding the duplicate association path issue that might have arisen during the criterion visitor execution. AssociationPathRegister is essentially a thin wrapper around the given Criteria instance that gets initialized with existing Subcriteria mappings at construction time. It is therefore safe to create multiple AssociationPathRegister instances operating on the same Criteria. The best part is that you can use this class to modify Criteria instances in a safe way on your own as well:

AssociationPathRegister register = new AssociationPathRegister(targetCriteria);

AssociationPath associationPath = new AssociationPath(
    new AssociationPathElement("projectManager", NestedPropertyJoinType.LEFT_JOIN),
    new AssociationPathElement("contactInfo") // default join type is INNER_JOIN
);

// resulting criteria is cached within the register for subsequent calls
Criteria criteria = register.get(associationPath);

There are a few more classes mentioned in the sample code shown above:

  • AssociationPathElement - single element of an AssociationPath that contains the value corresponding to the given persistent entity property name, as well as the optional joinType which specifies the type of join to use when creating related Subcriteria rooted under the parent Criteria instance
  • AssociationPath - ordered list of association path elements which point to the given property of a persistent entity, starting at the given persistent entity as the root object and navigating through associated objects as necessary (note that the association path doesn't include the target property itself, it's just a path to that property)
  • NestedPropertyJoinType - enumeration of possible database join types applicable to AssociationPathElement instances

All of the classes listed above are immutable by design so that you can safely reuse their instances across your code.

Property criterion classes

Let's take a look at two NestedPropertyCriterion subclasses that are intended for direct use in conjunction with NestedPropertyCriteria: FilterCriterion and SortCriterion.

FilterCriterion defines filter constraints for the given entity property via the FilterCriterionProvider interface. A custom FilterCriterionProvider implementation is used to construct a Hibernate Criterion instance regarding the filter criterion as well as deciding whether to actually use the provided Criterion instance:

public interface FilterCriterionProvider {

    /**
     * Returns a Criterion instance corresponding to the given
     * property of the target persistent entity.
     */
    Criterion getCriterion(String targetPropertyName,
            Object[] filterObjectValues,
            Object[] directValues);

    /**
     * Returns a flag indicating whether to use this provider
     * during the FilterCriterion instance processing.
     */
    boolean enabled(Object[] filterObjectValues,
            Object[] directValues);

}

There are basically two ways to pass data to the custom FilterCriterionProvider implementation from within the FilterCriterion (both of which are optional):

  • by specifying filterObjectValuePaths - array of dot-separated logical paths pointing to values reachable from the root filter object (these paths are resolved against the filter object via Java reflection)
  • by providing filter values directly via the directValues object array

Note that filterObjectValuePaths will be resolved into actual filterObjectValues during the criterion instance processing within the default NestedPropertyCriterionVisitor implementation. The enabled method (called before each getCriterion method invocation) can be used for disabling the FilterCriterionProvider implementation from being used at all - this can be helpful in certain situations, such as:

  • filter value inconsistency (the FilterCriterionProvider is unable to build the corresponding Criterion instance due to missing or incorrect data)
  • manual filter switch (the user wants to have control over when this provider should be active)

By having full control over Hibernate Criterion creation via the FilterCriterionProvider interface, the user is able to define even the most complex property criteria with ease, without having to worry about any nested subcriteria details. For example:

public static final FilterCriterionProvider LIKE_USING_FILTER_OBJECT = new FilterCriterionProvider() {

    public Criterion getCriterion(String targetPropertyName,
        Object[] filterObjectValues, Object[] directValues) {
        return Restrictions.like(targetPropertyName, filterObjectValues[0]);
    }

    public boolean enabled(Object[] filterObjectValues, Object[] directValues) {
        return (filterObjectValues.length == 1) && (filterObjectValues[0] != null);
    }

};

public static final FilterCriterionProvider BETWEEN_USING_DIRECT_VALUES = new FilterCriterionProvider() {

    public Criterion getCriterion(String targetPropertyName,
        Object[] filterObjectValues, Object[] directValues) {
        return Restrictions.between(targetPropertyName, directValues[0], directValues[1]);
    }

    public boolean enabled(Object[] filterObjectValues, Object[] directValues) {
        return (directValues.length == 2) && (directValues[0] != null) && (directValues[1] != null);
    }

};

The combination of filterObjectValuePaths and directValues allows you to define your own filter value strategy - a single filter object as the source of filter values, direct value approach or even a combination of the two. For simple cases that use one (or none) of these strategies, SimpleFilterCriterionProvider might suit your needs as well:

public static final FilterCriterionProvider LIKE_USING_FILTER_OBJECT = new SimpleFilterCriterionProvider(
        FilterDataStrategy.FILTER_OBJECT, 1) {

    public Criterion getCriterion(String targetPropertyName,
        Object[] filterObjectValues, Object[] directValues) {
        return Restrictions.like(targetPropertyName, filterObjectValues[0]);
    }

};

public static final FilterCriterionProvider BETWEEN_USING_DIRECT_VALUES = new SimpleFilterCriterionProvider(
        FilterDataStrategy.DIRECT, 2) {

    public Criterion getCriterion(String targetPropertyName,
        Object[] filterObjectValues, Object[] directValues) {
        return Restrictions.between(targetPropertyName, directValues[0], directValues[1]);
    }

};

Note that the SimpleFilterCriterionProvider implements the enabled method for you with an option to check for non-null values within the value array (depending on the chosen strategy).

SortCriterion, on the other hand, defines sort constraints for the given entity property using two boolean parameters: sortAscending and ignoreCase (the latter works only for string-based properties). Note that the SortCriterion is always "enabled" when present within the NestedPropertyCriteria.

FilterCriterion and SortCriterion classes feature builders for convenient instance creation: FilterCriterionBuilder and SortCriterionBuilder. For example:

FilterCriterion filterCriterion = new FilterCriterionBuilder(
        associationPath, targetPropertyName, LIKE_USING_FILTER_OBJECT)
    .filterObjectValuePaths("projectManager.contactInfo.email")
    .build();

Sample DAO using both filter value strategies

The following code shows an extended version of the sample DAO from the previous section, using filter object together with the direct value approach to employ custom constraints on the Order entity:

public class QueryDefinition<T extends Persistable<?>> {

    private Integer firstResult;
    private Integer maxResults;

    private T filterObject;
    private boolean filterEnabled;

    private boolean sortEnabled;
    private AssociationPath sortAssociationPath;
    private String sortTargetPropertyName;
    private boolean sortAscending;

    // getters and setters go here

}

public interface OrderDao extends PersistentEntityDao<Order, Long> {

    List<Order> getOrders(QueryDefinition<Order> query,
            Integer minOrderItemCount, final String customerNameFilter);

}

public class OrderDaoImpl extends EntityManagerAwareEntityDao<Order, Long> implements OrderDao {

    public Class<Order> getEntityClass() {
        return Order.class;
    }

    public List<Order> getOrders(final QueryDefinition<Order> query,
            Integer minOrderItemCount, final String customerNameFilter) {

        // initialize a NestedPropertyCriteria instance

        NestedPropertyCriteria criteria = new NestedPropertyCriteria();

        criteria.setFirstResult(query.getFirstResult());
        criteria.setMaxResults(query.getMaxResults());

        criteria.setFilterObject(query.getFilterObject());

        AssociationPath customerPath = new AssociationPath(new AssociationPathElement("customer"));


        // 1. filter object approach

        // "creationDate" is a direct property in relation to Order entity
        criteria.add(new FilterCriterion(AssociationPath.ROOT, "creationDate", "creationDate", true,
                new SimpleFilterCriterionProvider(FilterDataStrategy.FILTER_OBJECT, 1) {

                    public Criterion getCriterion(String targetPropertyName,
                            Object[] filterObjectValues, Object[] directValues) {
                        // filterObjectValues[0] comes from filterObject.getCreationDate()
                        return Restrictions.eq(targetPropertyName, filterObjectValues[0]);
                    }

                    @Override
                    public boolean enabled(Object[] filterObjectValues, Object[] directValues) {
                        return super.enabled(filterObjectValues, directValues)
                            && query.isFilterEnabled();
                        }

            }));

        // (Customer) "email" is a nested property in relation to Order entity
        criteria.add(new FilterCriterion(customerPath, "email", "customer.email", true,
                new SimpleFilterCriterionProvider(FilterDataStrategy.FILTER_OBJECT, 1) {

                    public Criterion getCriterion(String targetPropertyName,
                            Object[] filterObjectValues, Object[] directValues) {
                        // filterObjectValues[0] comes from filterObject.getCustomer().getEmail()
                        return Restrictions.like(targetPropertyName, filterObjectValues[0]);
                    }

                    @Override
                    public boolean enabled(Object[] filterObjectValues, Object[] directValues) {
                        return super.enabled(filterObjectValues, directValues)
                            && query.isFilterEnabled();
                    }

        }));


        // 2. direct value approach

        // "orderItems" is a direct property in relation to Order entity
        criteria.add(new FilterCriterion(AssociationPath.ROOT, "orderItems", minOrderItemCount, false,
                new SimpleFilterCriterionProvider(FilterDataStrategy.DIRECT, 1) {

                    public Criterion getCriterion(String targetPropertyName,
                            Object[] filterObjectValues, Object[] directValues) {
                        // directValues[0] is minOrderItemCount
                        return Restrictions.sizeGe(targetPropertyName, (Integer) directValues[0]);
                    }

                    @Override
                    public boolean enabled(Object[] filterObjectValues, Object[] directValues) {
                        return super.enabled(filterObjectValues, directValues)
                            && query.isFilterEnabled();
                    }

        }));


        // 3. bypassing filter value strategies altogether

        // (Customer) "name" is a nested property in relation to Order entity
        criteria.add(new FilterCriterion(customerPath, "name",
                new SimpleFilterCriterionProvider() {

                    public Criterion getCriterion(String targetPropertyName,
                            Object[] filterObjectValues, Object[] directValues) {
                        // customerNameFilter is the method parameter itself
                        return Restrictions.like(targetPropertyName, customerNameFilter);
                    }

                    @Override
                    public boolean enabled(Object[] filterObjectValues, Object[] directValues) {
                        return (customerNameFilter != null) && query.isFilterEnabled();
                    }

        }));


        if (query.isSortEnabled()) {
            criteria.add(new SortCriterion(
                query.getSortAssociationPath(),
                query.getSortTargetPropertyName(),
                query.isSortAscending()));
        }

        return query(criteria);
    }

}

In this example, QueryDefinition represents a generic persistent entity query supporting filtering, sorting and paging capabilities of the underlying NestedPropertyCriteria implementation (notice that a single sort property is supported in this case for simplicity). The user is responsible for initializing the filterObject as well as the associated objects properly according to filter constraints bound to it before passing it to the getOrders method. By modifying the state of the filterObject, the user is able to change the actual filter criteria bound to filter object values without touching the underlying NestedPropertyCriteria in any way. This is essentially the main purpose of the filter object approach - to separate criteria definition from the actual filter values accessible from a single object. Note that the AssociationPath.ROOT is just a fancy looking alias to an empty association path (intended for use with direct entity properties).

One important thing to note here is that our sample DAO implementation is stateless, which is generally the best way to go for most cases. Stateless DAOs can be efficiently managed as singletons (e.g. within a Spring context) and injected into service classes without having to worry about thread safety issues. This is especially true for web applications with each request being bound to a dedicated worker thread.

In general, the filter object approach is useful in applications where GUI changes can be seamlessly propagated to the server, modifying the filter object instance of the given server-side component (for example, a Java Server Faces managed bean bound to web page using server-side AJAX callback handlers). Note that the SimpleFilterCriterionProvider class ensures that our sample FilterCriterion instances won't be processed unless the filter data is consistent (filter criteria won't be applied unless the filter object or minOrderItemCount contain proper values).

In certain scenarios where the client side and server side connect more loosely to each other using a custom communication mechanism (e.g. GWT RPC for Google Web Toolkit or AMF for Flex), direct filter object changes are not possible since the client side typically uses some local data model bound to the GUI together with remote service calls. Although it's possible to change the filter object accordingly using some server-side request processor, it is much more convenient to use the criteria transfer object pattern such situations.