© Joseph B. Ottinger, Jeff Linwood and Dave Minter 2016

Joseph B. Ottinger, Jeff Linwood and Dave Minter, Beginning Hibernate, 10.1007/978-1-4842-2319-2_11

11. Filtering the Results of Searches

Joseph B. Ottinger, Jeff Linwood2 and Dave Minter3

(1)Youngsville, North Carolina, USA

(2)Austin, Texas, USA

(3)London, UK

Your application will often need to process only a subset of the data in the database tables. In these cases, you can create a Hibernate filter to eliminate the unwanted data. Filters provide a way for your application to limit the results of a query to data that passes the filter’s criteria. Filters are not a new concept — you can achieve much the same effect using SQL database views or, well, named queries — but Hibernate offers a centralized management system for them.

Unlike database views, Hibernate filters can be enabled or disabled during a Hibernate session. In addition, Hibernate filters are parameterized, which is particularly useful when you are building applications on top of Hibernate that use security roles or personalization.

Filters are particularly useful when you have many similar queries with generalizable selection clauses. Filters allow you to use a generalized query, adding criteria for the query as needed.

When to Use Filters

As an example, consider an application that uses users and groups. A user has a status indicating whether that user is active or inactive, and a set of memberships; if your application needs to manage users based on status and/or group membership, you’re looking at four separate queries (or a wildcard query, which seems silly). The wildcard query could indeed work, but it would add a burden on the database that shouldn’t be there, especially if one set of wildcards was very common.

If we were to use four different queries (“all users,” “all users with this status,” “all users in this group,” and “all users with this status and in this group”), not only would we have four queries to test and maintain, but we’d also have to have a way of keeping track of which query we were supposed to be using at any given time.

We could also use custom queries for each execution (building the query as needed, instead of storing a set of queries). It’s doable, but not entirely efficient, and it pollutes your services with query data.1

Filters allow us to define sets of restrictions. Instead of custom queries or sets of similar queries, we can create a filter and apply it when we query the database, such that our actual query doesn’t change, even though the data set does.

The advantage to using Hibernate filters is that you can programmatically turn filters on or off in your application code, and your filters are defined in consistent locations for easy maintainability. The major disadvantage of filters is that you cannot create new filters at runtime. Instead, any filters your application requires need to be specified in the proper Hibernate annotations or mapping documents. Although this may sound somewhat limiting, the fact that filters can be parameterized makes them pretty flexible. For our user status filter example, only one filter would need to be defined in the mapping document (albeit in two parts). That filter would specify that the status column must match a named parameter. You would not need to define the possible values of the status column in the Hibernate annotations or mapping documents — the application can specify those parameters at runtime.

Although it is certainly possible to write applications with Hibernate that do not use filters, we find them to be an excellent solution to certain types of problems — notably security and personalization.

Defining and Attaching Filters

Your first step is to create a filter definition. A filter definition is like metadata for a filter, including parameter definitions; it’s not the filter itself, but it serves as a starting point.2 After you have the filter definition, you create the filter itself; this contains the actual filter specification. Using the filter is a simple matter of enabling the filter by name and populating the parameters, if any.

What’s the reason for having a filter definition separate from the filter itself? It’s that the filter definitions are often written for a specific database, and thus they trend to being nonportable. If the filters and their definitions were unified, it’d be much more difficult to keep them portable; with a separated definition, it’s easy to put a filter in a separately included resource.

Let’s look at some filters, to make their usage clear.

Filters with Annotations

To use filters with annotations, you will need to use the @FilterDef, @ParamDef, and @Filter annotations. The @FilterDef annotation defines the filter and belongs to either the class or the package. To define a filter on a class, add an @FilterDef annotation after the @Entity annotation.

After you have defined your filters, you can attach them to classes or collections with the @Filter annotation. The @Filter annotation takes two parameters: name and condition. The name references a filter definition that we have previously described in an annotation. The condition parameter is a HQL WHERE clause. The parameters in the condition are denoted with colons, similar to named parameters in HQL. The parameters have to be defined on the filter definition. Here is a skeleton example of the filter annotations:

@Entity
@FilterDef(name = "byStatus", parameters = @ParamDef(name = "status", type = "boolean"))
@Filter(name = "byStatus", condition = "active = :status")
public class User {
   // other fields removed for brevity’s sake
   boolean status;
}
Note

Defining filters on each class is simple, but if you use filters in multiple classes, you will have a lot of duplication. To define any annotation at a package level, you will need to create a Java source file named package-info.java in the package. The package-info.java class should only include the package-level annotations and then declare the package immediately afterward. It is not meant to be a Java class. You will also need to tell Hibernate to map the package when you configure Hibernate, either through the addPackage() method on AnnotationConfiguration or in your Hibernate configuration XML.

Filters with XML Mapping Documents

For the XML mapping documents , use the <filter-def> XML element. These filter definitions must contain the name of the filter and the names and types of any filter parameters. You specify filter parameters with the <filter-param> XML element. Here is an excerpt from a mapping document with a filter called latePaymentFilter defined:

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping
   PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
   "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">


<hibernate-mapping>
  <class ...


  </class>
  <filter-def name="latePaymentFilter">
    <filter-param name="dueDate" type="date"/>
  </filter-def>
</hibernate-mapping>

Once you have created the filter definitions, you need to attach the filters to a class or a collection of mapping elements. You can attach a single filter to more than one class or collection. To do this, you add a <filter> XML element to each class and/or collection. The <filter> XML element has two attributes: name and condition. The name references a filter definition (for instance: latePaymentFilter). The condition represents a WHERE clause in HQL. Here’s an example:

  <class ...
    <filter name="latePaymentFilter" condition=":dueDate = paymentDate"/>
  </class>

Each <filter> XML element must correspond to a <filter-def> element.

Using Filters in Your Application

Your application programmatically determines which filters to activate or deactivate for a given Hibernate session. Each session can have a different set of filters with different parameter values. By default, sessions do not have any active filters — you must explicitly enable filters programmatically for each session. The Session interface contains several methods for working with filters, as follows:

  • public Filter enableFilter(String filterName)

  • public Filter getEnabledFilter(String filterName)

  • public void disableFilter(String filterName)

These are pretty self-explanatory — the enableFilter(String filterName) method activates the specified filter; the disableFilter(String filterName) method deactivates the method; and if you have already activated a named filter, getEnabledFilter(String filterName) retrieves that filter.

The org.hibernate.Filter interface has six methods. You are unlikely to use validate(); Hibernate uses that method when it processes the filters. The other five methods are as follows:

  • public Filter setParameter(String name, Object value)

  • public Filter setParameterList(String name, Collection values)

  • public Filter setParameterList(String name, Object[] values)

  • public String getName()

  • public FilterDefinition getFilterDefinition()

The setParameter() method is the most useful. You can substitute any Java object for the parameter, although its type should match the type you specified for the parameter when you defined the filter. The two setParameterList() methods are useful for using IN clauses in your filters. If you want to use BETWEEN clauses, use two different filter parameters with different names. Finally, the getFilterDefinition() method allows you to retrieve a FilterDefinition object representing the filter metadata (its name, its parameters’ names, and the parameter types).

Once you have enabled a particular filter on the session, you do not have to do anything else to your application to take advantage of filters, as we demonstrate in the following example.

A Basic Filtering Example

Because filters are very straightforward, a basic example allows us to demonstrate most of the filter functionality, including activating filters and defining filters in mapping documents.

We’re going to create a User entity, with an active status and membership in groups. We’re going to define three filters: one very simple filter with no parameters (just to show how), and then two parameterized filters, which we’ll apply in various combinations.

We’re going to stick with the annotation configuration, as we’re using a single database (H2), and it simplifies our example drastically.3 We’re going to also revert to using Lombok , because that will shorten our example code by getting rid of a lot of boilerplate methods, as shown in Listing 11-1.

Listing 11-1. User.java
package chapter11.model;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.*;


import javax.persistence.*;
import javax.persistence.Entity;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;


@Entity
@Data
@NoArgsConstructor
@FilterDefs({
        @FilterDef(name = "byStatus", parameters = @ParamDef(name = "status", type = "boolean")),
        @FilterDef(name = "byGroup", parameters = @ParamDef(name = "group", type = "string")),
        @FilterDef(name = "userEndsWith1")
})
@Filters({
        @Filter(name = "byStatus", condition = "active = :status"),
        @Filter(name = "byGroup", condition = ":group in ( select ug.groups from user_groups ug where ug.user_id = id)"),
        @Filter(name = "userEndsWith1", condition = "name like '%1'")
})
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Integer id;
    @Column(unique = true)
    String name;
    boolean active;
    @ElementCollection
    Set<String> groups;


    public User(String name, boolean active) {
        this.name=name;
        this.active=active;
    }


    public void addGroups(String¼ groupSet) {
        if (getGroups() == null) {
            setGroups(new HashSet<>());
        }
        getGroups().addAll(Arrays.asList(groupSet));


    }
}

There are a few things about this that stand out, especially with regard to groups.

First, groups are defined as a Set, annotated with @ElementCollection. This will create a table, USER_GROUPS, that will contain a user ID and a single column, with the various group names in a column named after the collection (thus, "groups" and not "group").

Then, the filter that selects by group uses a subselect to limit the users returned. This condition is database specific and uses knowledge of the actual table structure; filters do some introspection, but not as much as they could. Be prepared to do some analysis to work out exactly what the filter condition should be.

We’re also going to add another method to SessionUtil, in our util package, called doWithSession(). This is a Hibernate-specific form of the doWithEntityManager() method we saw in Chapter 10; it accepts a lambda as an argument, and calls the lambda after creating a Session and starting a Transaction. This will eliminate all of the boilerplate around transaction and session management.

Here’s the method:

public static void doWithSession(Consumer<Session> command) {
    try(Session session = getSession()) {
        Transaction tx = session.beginTransaction();


        command.accept(session);
        if (tx.isActive() &&
                !tx.getRollbackOnly()) {
            tx.commit();
        } else {
            tx.rollback();
        }
    }
}

Now let’s create a test class in Listing 11-2, FilterTests.java, which runs a simple query to make sure we have a basic framework in place. We’ll add various test methods to this class to show the filters in action.

Listing 11-2. Our basic test framework, FilterTests.java
package chapter11;

import chapter11.model.User;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.query.Query;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;


import java.util.List;

import static org.testng.Assert.assertEquals;

public class FilterTests {
    @BeforeMethod
    public void setupTest() {
        SessionUtil.doWithSession((session) -> {
            User user = new User("user1", true);
            user.addGroups("group1", "group2");
            session.save(user);
            user = new User("user2", true);
            user.addGroups("group2", "group3");
            session.save(user);
            user = new User("user3", false);
            user.addGroups("group3", "group4");
            session.save(user);
            user = new User("user4", true);
            user.addGroups("group4", "group5");
            session.save(user);
        });
    }


    @AfterMethod
    public void endTest() {
        SessionUtil.doWithSession((session) -> {
            session.disableFilter("byGroup");
            session.disableFilter("byStatus");


            // need to manually delete all of the Users since
            // HQL delete doesn't cascade over element collections
            Query<User> query = session.createQuery("from User", User.class);
            for (User user : query.list()) {
                session.delete(user);
            }
        });
    }


    @Test
    public void testSimpleQuery() {
        SessionUtil.doWithSession((session) -> {
            Query<User> query = session.createQuery("from User", User.class);
            List<User> users = query.list();
            assertEquals(users.size(), 4);
        });
    }
}

This creates a set of users, some active, some inactive, and with membership in various groups. It also runs a basic query ("from User") and validates that it retrieved all of the users. (It also shows us how to use the doWithSession() method.)

First, let’s test the simplest filter we have in Listing 11-3, which is named "userEndsWith1" – a filter that is constructed solely for the sake of showing a non-parameterized filter.4

Listing 11-3. Using a non-parameterized filter
@Test
public void testNoParameterFilter() {
    SessionUtil.doWithSession((session) -> {
        Query<User> query = session.createQuery("from User", User.class);


        session.enableFilter("userEndsWith1");
        List<User> users = query.list();
        assertEquals(users.size(), 1);
        assertEquals(users.get(0).getName(), "user1");
    });
}

This test enables a filter for the session, and then runs a basic query (which is "from User," as you’ll recall, and we’ll be reusing this query over and over again). With no other configuration or specification, our data set returns only those users whose name ends with a “1” – and since we created only one user like that, that’s the user that’s returned.

Now let’s see how to use a parameter in a filter with two tests, shown in Listing 11-4.

Listing 11-4. A filter using a parameter
@Test
public void testActivesFilter() {
    SessionUtil.doWithSession((session) -> {
        Query<User> query = session.createQuery("from User", User.class);
        session.enableFilter("byStatus").setParameter("status", Boolean.TRUE);
        List<User> users = query.list();
        assertEquals(users.size(), 3);
    });
}


@Test
public void testInactivesFilter() {
    SessionUtil.doWithSession((session) -> {
        Query<User> query = session.createQuery("from User", User.class);


        session.enableFilter("byStatus").setParameter("status", Boolean.FALSE);
        List<User> users = query.list();
        assertEquals(users.size(), 1);
    });
}

Here, we try both of our status conditions, first enabling the filter and then setting a parameter on the filter reference. Again, we’re relying on our known data set.

We can combine filters, as well. We’re going to include two more tests, in Listing 11-5; the first will test our group filter, and the second will combine the two filters for a single query.

It’s worth noting again that our query has not changed for any of this code; we’re reusing the same query, over and over again, while getting different data sets from it.

Listing 11-5. Combining filters
@Test
public void testGroupFilter() {
    SessionUtil.doWithSession((session) -> {
        Query<User> query = session.createQuery("from User", User.class);


        session.enableFilter("byGroup").setParameter("group", "group4");
        List<User> users = query.list();
        assertEquals(users.size(), 2);
        session.enableFilter("byGroup").setParameter("group", "group1");
        users = (List<User>) query.list();
        assertEquals(users.size(), 1);
        // should be user 1
        assertEquals(users.get(0).getName(), "user1");
    });
}


@Test
public void testBothFilters() {
    SessionUtil.doWithSession((session) -> {
        Query<User> query = session.createQuery("from User", User.class);


        session.enableFilter("byGroup").setParameter("group", "group4");
        session.enableFilter("byStatus").setParameter("status", Boolean.TRUE);
        List<User> users = query.list();
        assertEquals(users.size(), 1);
        assertEquals(users.get(0).getName(), "user4");
    });
}

You can see here that it’s trivial to concatenate criteria via filters, allowing us to use simpler (and fewer) queries, and applying simple criteria.

Summary

Filters are a useful way to separate some database concerns from the rest of your code. A set of filters can cut back on the complexity of the HQL queries used in the rest of your application, at the expense of some runtime flexibility. Instead of using views (which must be created at the database level), your applications can take advantage of dynamic filters that can be activated as and when they are required.

Footnotes

1 Query data are the methods that retrieved users would have to keep track of the specific criteria in question. To some degree, this is going to be necessary anyway, but we’d prefer the impact be as small as possible. Of course, there’s always the Criteria API we mentioned in Chapter 10

2 In this, it’s like JNDI; you refer to a name in the filter definition, but the filter itself is defined elsewhere.

3 We could anticipate multiple database engines, but most environments use the same databases for testing and production, as they should. It’s a good habit to follow.

4 A better example might have been a constant status, instead of “name ends with a 1,” but the status is ideally parameterized as well. The data model would have been polluted without a contrived example, so that’s what we have.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset