August 12, 2004
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.
Configuration
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!
Usage
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:
session.enableFilter("myFilter").setParameter("myFilterParam", "some-value");
Note that methods on the org.hibernate.Filter interface do allow the method-chaining
common to much of Hibernate.
Big Deal
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.
Security Example
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
Conclusion
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.
About the author
Steve Ebersole
Blog:
|