|

Hibernate’s @Filter Annotation – Apply Dynamic Filters at Runtime


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


Hibernate provides 2 proprietary features that enable you to define additional filter criteria that Hibernate applies to every query that selects a specific entity class. This article will show you how to use the @FilterDef and @Filter annotations, which is the more flexible approach. You can activate and deactivate filter definitions for your current session and use dynamic parameters in your filter conditions. That enables you to adjust the filter conditions at runtime.

In a previous article on implementing soft deletes, I showed you the @Where annotation. It’s similar to Hibernate’s filter feature but less flexible. The condition defined in the @Where annotation is always active, and you can’t use any parameters to customize it. If you’re looking for a simpler, static approach to define filter conditions that will be added to your query, make sure to check that article.

Parameterized Filters With a Default Condition

The most powerful and complex filters use a parameterized filter definition with a default condition. These and the ones without a default condition are the most relevant in real-world applications. So, let’s take a look at them first.

Defining a Parameterized Filter With a Default Condition

Before you can use a filter, you need to define it. You do that using Hibernate’s @FilterDef annotation, which you can apply on the class or package level. The name attribute is its only mandatory attribute. Each filter definition requires a name that’s unique within your persistence unit. You will use this name when you apply a filter definition to an entity class or attribute.

In addition to the name, you can also define an array of parameters and a default condition. I set both in the following example to define a filter that limits the resultset to professional players.

@FilterDef(name = "proFilter", 
		   parameters = @ParamDef(name = "professional", type = "boolean"), 
		   defaultCondition = "pro = :professional")
		   
package com.thorben.janssen.sample.model;

import org.hibernate.annotations.FilterDef;
import org.hibernate.annotations.FilterDefs;
import org.hibernate.annotations.ParamDef;

The parameters attribute accepts an array of @ParamDef annotations. Each of them defines the name and type of a parameter that you can use in the defaultCondition of the @FilterDef or the condition of the @Filter annotation. In this example, I reference the professional parameter in the defaultCondition.

The defaultCondition attribute accepts the SQL snippet that Hibernate will add to the generated SQL statement.

Applying the Filter Definition to an Entity

After we defined the filter, it’s time to apply it to an entity. You can do that by annotating an entity class, attribute, or method with a @Filter annotation.

In the following example, I annotated the ChessPlayer entity class with a @Filter annotation to apply the filter definition to this entity class. This only connects the filter definition with the entity class, but it doesn’t activate it. This requires another step, which I will show you in the next section.

@Filter(name = "proFilter")
@Entity
public class ChessPlayer { ... }

As you can see in the code snippet, the name attribute of the @Filter annotation references the filter definition that we defined in the previous code snippet. That @FilterDef provides a defaultCondition, which Hibernate will apply when we activate this filter.

Activating a Parameterized Filter Definition

Hibernate’s filters are deactivated by default. If you want to use a filter, you need to activate it on your Hibernate Session. You can do that by calling the enableFilter method on your Session with the name of the @FilterDef you want to activate. The method returns a Filter object, which you can then use to set the filter parameters.

This activates the referenced @FilterDef for all entities that referenced it, and it stays active until the end of the current Session or until you call the disableFilter method with the name of the filter definition.

Let’s activate the previously defined filter proFilter and set the professional parameter to true.

// Enable filter and set parameter
Session session = em.unwrap(Session.class);
Filter filter = session.enableFilter("proFilter");
filter.setParameter("professional", true);

// Execute query with an enabled filter
List<ChessPlayer> chessPlayersAfterEnable = em.createQuery("select p from ChessPlayer p", ChessPlayer.class)
											  .getResultList();

When you execute this code and activate the logging of SQL statements, you can see in the log file that Hibernate added the SQL snippet provided by the @FilterDef annotation to the SQL SELECT statement.

17:59:00,949 DEBUG SQL:144 - select chessplaye0_.id as id1_1_, chessplaye0_.birthDate as birthdat2_1_, chessplaye0_.firstName as firstnam3_1_, chessplaye0_.lastName as lastname4_1_, chessplaye0_.pro as pro5_1_, chessplaye0_.version as version6_1_ from ChessPlayer chessplaye0_ where chessplaye0_.pro = ?

Parameterized Filters Without a Default Condition

In some situations, you might want to define a reusable filter that you can apply on different columns. You can’t specify a condition that matches different database tables and columns. But as mentioned earlier, the defaultCondition attribute of the @FilterDef annotation is optional. As I will show you in the next section, you can define a custom condition on the @Filter annotation.

In the following code snippet, I create a filter definition without a defaultCondition but with 2 parameters of type LocalDate.

@FilterDef(name = "dateFilter", 
		   parameters = {
                @ParamDef(name = "minDate", type = "java.time.LocalDate"),
                @ParamDef(name = "maxDate", type = "java.time.LocalDate")
		   })

Applying the Filter Definition to Multiple Entities

If you don’t set the defaultCondition, you need to provide a condition when you apply the filter to an entity. You can do that by providing an SQL snippet to the condition attribute of the @Filter annotation. In this example, I use that when applying the dateFilter definition. For my ChessPlayer entity, I want to use that filter on the birthDate column.

@Filter(name = "dateFilter", condition = "birthDate >= :minDate and birthDate <= :maxDate")
@Entity
public class ChessPlayer { ... }

And I apply the same filter to the date column of the ChessGame entity. This time, I annotated the date attribute instead of the entity class with the @Filter annotation. But that doesn’t make any difference.

@Entity
public class ChessGame {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @Filter(name = "dateFilter", condition = "date >= :minDate and date <= :maxDate")
    private LocalDate date;
	
    ...
}

Activating the Filter Globally

In your business code, you need to activate the filter in the same way as in the previous example. You need to call the enableFilter method on your Session and set all parameter values on the Filter object.

Filter dateFilter = session.enableFilter("dateFilter");
dateFilter.setParameter("minDate", LocalDate.of(1990, 1, 1));
dateFilter.setParameter("maxDate", LocalDate.of(2000, 1, 1));

List<ChessPlayer> chessPlayers = em.createQuery("select p from ChessPlayer p", ChessPlayer.class)
		.getResultList();

Using the same filter definition on multiple entity attributes might look like a very comfortable way to reuse a filter. But please keep in mind that you activate the filter definition for the entire Session and not only the next query. After activating the dateFilter in the previous code snippet, Hibernate will apply it on all queries executed as part of the current Session that fetch ChessGame or ChessPlayer entities. Depending on the semantics of your filter, this can cause unexpected results.

Static Filters With a Default Condition

Another commonly used filter definition uses a static defaultCondition without any parameters. These usually check if a boolean flag is true or false or if a date is within the current month or year.

Here you can see a static version of the proFilter that I showed you in the 1st example. This time, it checks if the pro column contains the value true instead of comparing it with a provided parameter value. It’s, of course, less flexible than the previous filter definition, but especially for boolean flags, this is often good enough.

@FilterDef(name = "isProFilter", defaultCondition = "pro = 'true'")

Applying a Static Filter Definition to an Entity

You can apply this filter definition in the same way as any other definition that provides a defaultCondition. You only need to annotate your entity class or attribute with @Filter and reference the definition in the name attribute.

@Filter(name = "isProFilter")
@Entity
public class ChessPlayer { ... }

Activating a Parameterless Filter

This filter definition doesn’t use any parameters. Due to that, you only need to enable it in your business code by calling the enableFilter method.

Filter proFilter = session.enableFilter("isProFilter");
List<ChessPlayer> chessPlayers = em.createQuery("select p from ChessPlayer p", ChessPlayer.class).getResultList();

Filters on Association Tables

In all the previous examples, we applied the filter definition to a mapped column of the table mapped by the entity class. If you want to do the same for a column that’s part of an association table of a many-to-many or unidirectional one-to-many association, you need to annotate the attribute or method that defines the association mapping with a @FilterJoinTable annotation.

@FilterDef(name = "playerMinId", parameters = {
        @ParamDef(name = "minId", type = "integer")
})
@Entity
public class ChessTournament {

    @ManyToMany
    @FilterJoinTable(name = "playerMinId", condition = "players_id >= :minId")
    private Set<ChessPlayer> players = new HashSet<>();
	
    ...
}

This is the only difference to the filters and filter definitions that I showed you before. In this example, I added the @FilterDef annotation to the same entity class, specified a parameter, and defined the condition on the @FilterJoinTable annotation.

You can then use the same enableFilter and setParameter methods to activate the filter in your business code, as we used in the previous code samples.

Session session = em.unwrap(Session.class);
Filter filter = session.enableFilter("playerMinId");
filter.setParameter("minId", 101);
ChessTournament chessTournamentAfterEnable = em.find(ChessTournament.class, this.chessTournament.getId());

Limitations and Pitfall when Using Filters

Before you start using Hibernate’s filters in your application, you should be aware of 2 limitations that cause issues in many applications.

Filter and 2nd Level Cache

Hibernate’s 2nd Level Cache is independent of your current Session and its specific filter settings. To ensure that an activated filter doesn’t cause inconsistent results, the 2nd level cache always stores the unfiltered results, and you can’t use the @Filter and @Cache annotation together.

No Filtering on Direct Fetching

Hibernate only applies the filter to entity queries but not if you’re fetching an entity directly, e.g., by calling the find() method on the EntityManager. Due to that, you shouldn’t use a filter to implement any security feature, and you should check your application carefully for any direct fetch operations

Conclusion

Hibernate’s @FilterDef and @Filter annotations enable you to specify additional filter criteria, which Hibernate will apply to all queries that select a specific entity class. At runtime, you need to activate the filter, and you can customize them by providing different parameter values. That enables you to adjust the filter to the specific needs of each use case and session.