Java Development News:
Hibernate 3 Fliters
By Steve Ebersole
12 Aug 2004 | TheServerSide.com
Hibernate3 adds the ability to pre-define filter criteria and attach those filters at both a class and a collection level. What's a "pre-defined filter criteria"? Well, it's the ability to define a limit clause very similiar to the existing "where" attribute available on the class and various collection elements. Except these filter conditions can be parameterized! The application can then make the decision at runtime whether given filters should be enabled and what their parameter values should be.
In order to use filters, they must first be defined and then attached to the appropriate mapping elements. To define a filter, use the new <filter-def/> element within a <hibernate-mapping/> element:
<filter-def name="myFilter"> <filter-param name="myFilterParam" type="string"/> </filter-def>
Then, this filter can be attched to a class:
<class name="myClass" ...> ... <filter name="myFilter" condition=":myFilterParam = my_filtered_column"/> </class>
or, to a collection:
<set ...> <filter name="myFilter" condition=":myFilterParam = my_filtered_column"/> </set>
or, even to both (or multiples of each) at the same time!
In support of this, a new interface was added to Hibernate3, org.hibernate.Filter, and some new methods added to org.hibernate.Session. The new methods on Session are: enableFilter(String filterName), getEnabledFilter(String filterName), and disableFilter(String filterName). By default, filters are not enabled for a given session; they must be explcitly enabled through use of the Session.enabledFilter() method, which returns an instance of the new Filter interface. Using the simple filter defined above, this would look something like:
Note that methods on the org.hibernate.Filter interface do allow the method-chaining common to much of Hibernate.
This is all functionality that was available in Hibernate before version 3, right? Of course. But before version 3, this was all manual processes by application code. To filter a collection you'd need to load the entity containing the collection and then apply the collection to the Session.filter() method. And for entity filtration you'd have to write stuff that manually modified the HQL string by hand or a custom Interceptor.
This new feature provides a clean and consistent way to apply these types of constraints. The Hibernate team envisions the usefulness of this feature in everything from internationalization to temporal data to security considerations (and even combinations of these at the same time) and much more. Of course it's hard to envision the potential power of this feature given the simple example used so far, so let's look at some slightly more in depth usages.
Temporal Data Example
Say you have an entity that follows the "effective record" database pattern. This entity has multiple rows each varying based on the date range during which that record was effective (possibly even maintained via a Hibernate Interceptor). An employment record might be a good example of such data, since employees might come and go and come back again. Further, say you are developing a UI which always needs to deal in current records of employment data. To use the new filter feature to acheive these goals, we would first need to define the filter and then attach it to our Employee class:
<filter-def name="effectiveDate"> <filter-param name="asOfDate" type="date"/> </filter-def> <class name="Employee" ...> ... <many-to-one name="department" column="dept_id" class="Department"/> <property name="effectiveStartDate" type="date" column="eff_start_dt"/> <property name="effectiveEndDate" type="date" column="eff_end_dt"/> ... <!-- Note that this assumes non-terminal records have an eff_end_dt set to a max db date for simplicity-sake --> <filter name="effectiveDate" condition=":asOfDate BETWEEN eff_start_dt and eff_end_dt"/> </class> <class name="Department" ...> ... <set name="employees" lazy="true"> <key column="dept_id"/> <one-to-many class="Employee"/> <filter name="effectiveDate" condition=":asOfDate BETWEEN eff_start_dt and eff_end_dt"/> </set> </class>
Then, in order to ensure that you always get back currently effective records, simply enable the filter on the session prior to retrieving employee data:
Session session = ...; session.enabledFilter("effectiveDate").setParameter("asOfDate", new Date()); List results = session.createQuery("from Employee as e where e.salary > :targetSalary") .setLong("targetSalary", new Long(1000000)) .list();
In the HQL above, even though we only explicitly mentioned a salary constraint on the results, because of the enabled filter the query will return only currently active employees who have a salary greater than a million dollars (lucky stiffs).
Even further, if a given department is loaded from a session with the "effectiveDate" filter enabled, its employee collection will only contain active employees.
Imagine we have an application that assigns each user an access level, and that some sensitive entities in the system are assigned access levels (way simplistic, I understand, but this is just illustration). So a user should be able to see anything where their assigned access level is greater than that assigned to the entity they are trying to see. Again, first we need to define the filter and apply it:
<filter-def name="accessLevel"> <filter-param name="userLevel" type="int"/> </filter-def> <class name="Opportunity" ...> ... <many-to-one name="region" column="region_id" class="Region"/> <property name="amount" type="Money"> <column name="amt"/> <cloumn name="currency"/> </property> <property name="accessLevel" type="int" column="access_lvl"/> ... <filter name="accessLevel">= access_lvl]]> </class> <class name="Region" ...> ... <set name="opportunities" lazy="true"> <key column="region_id"/> <one-to-many class="Opportunity"/> <filter name="accessLevel">= access_lvl]]> </set> ... </class>
Next, our application code would need to enable the filter:
User user = ...; Session session = ...; session.enableFilter("accessLevel").setParameter("userLevel", user.getAccessLevel());
At this point, loading a Region would filter its opportunities collection based on the current user's access level:
Region region = (Region) session.get(Region.class, "EMEA"); region.getOpportunities().size(); // <- limited to those accessible by the user's level
These were some pretty simple examples. But hopefully, they'll give you a glimpse of how powerful these filters can be and maybe sparked some thoughts as to where you might be able to apply such constraints within your own application. This can become even more powerful in combination with various interception methods, like web filters, etc.
Also a note: if you plan on using filters with outer joining (either through HQL or load fetching) be careful of the direction of the condition expression. Its safest to set this up for left outer joining; in general, place the parameter first followed by the column name(s) after the operator.