In this chapter, you see many different strategies for filtering data as it passes through the Hibernate engine. When Hibernate loads data from the database, you can transparently restrict the data seen by the application with a filter. When Hibernate stores data in the database, you can listen to such an event and execute some secondary routines: for example, write an audit log or assign a tenant identifier to the record.
We explore the following data-filtering features and APIs:
We start with cascading options for transitive state changes.
When an entity instance changes state—for example, when it was transient and becomes persistent—associated entity instances may also be included in this state transition. This cascading of state transitions isn’t enabled by default; each entity instance has an independent life cycle. But for some associations between entities, you may want to implement fine-grained life cycle dependencies.
For example, in section 7.3, you created an association between the Item and Bid entity classes. In this case, not only did you make the bids of an Item automatically persistent when they were added to an Item, but they were also automatically deleted when the owning Item was deleted. You effectively made Bid an entity class that was dependent on another entity, Item.
The cascading settings you enabled in this association mapping were CascadeType.PERSIST and CascadeType.REMOVE. We also talked about the special switch orphanRemoval and how cascading deletion at the database level (with the foreign key ON DELETE option) affects your application.
You should review this association mapping and its cascading settings; we won’t repeat it here. In this section, we look at some other, rarely used cascading options.
Table 13.1 summarizes all available cascading options in Hibernate. Note how each is linked with an EntityManager or Session operation.
Option |
Description |
---|---|
CascadeType.PERSIST | When an entity instance is stored with EntityManager #persist(), at flush time any associated entity instance(s) are also made persistent. |
CascadeType.REMOVE | When an entity instance is deleted with EntityManager #remove(), at flush time any associated entity instance(s) are also removed. |
CascadeType.DETACH | When an entity instance is evicted from the persistence context with EntityManager#detach(), any associated entity instance(s) are also detached. |
CascadeType.MERGE | When a transient or detached entity instance is merged into a persistence context with EntityManager#merge(), any associated transient or detached entity instance(s) are also merged. |
CascadeType.REFRESH | When a persistent entity instance is refreshed with EntityManager#refresh(), any associated persistent entity instance(s) are also refreshed. |
org.hibernate.annotations.CascadeType.REPLICATE | When a detached entity instance is copied into a database with Session#replicate(), any associated detached entity instance(s) are also copied. |
CascadeType.ALL | Shorthand to enable all cascading options for the mapped association. |
If you’re curious, you’ll find more cascading options defined in the org.hibernate.annotations.CascadeType enumeration. Today, though, the only interesting option is REPLICATE and the Session#replicate() operation. All other Session operations have a standardized equivalent or alternative on the EntityManager API, so you can ignore these settings.
We’ve already covered the PERSIST and REMOVE options. Let’s look at transitive detachment, merging, refreshing, and replication.
Let’s say you want to retrieve an Item and its bids from the database and work with this data in detached state. The Bid class maps this association with an @ManyToOne. It’s bidirectional with this @OneToMany collection mapping in Item:
@Entity public class Item { <enter/> @OneToMany( mappedBy = "item", cascade = {CascadeType.DETACH, CascadeType.MERGE} ) protected Set<Bid> bids = new HashSet<Bid>(); <enter/> // ... }
Transitive detachment and merging is enabled with the DETACH and MERGE cascade types. Now you load the Item and initialize its bids collection:
The EntityManager#detach() operation is cascaded: it evicts the Item instance from the persistence context as well as all bids in the collection. If the bids aren’t loaded, they aren’t detached. (Of course, you could have closed the persistence context, effectively detaching all loaded entity instances.)
In detached state, you change the Item#name, create a new Bid, and link it with the Item:
item.setName("New Name"); <enter/> Bid bid = new Bid(new BigDecimal("101.00"), item); item.getBids().add(bid);
Because you’re working with detached entity state and collections, you have to pay extra attention to identity and equality. As explained in section 10.3, you should override the equals() and hashCode() methods on the Bid entity class:
@Entity public class Bid { <enter/> @Override public boolean equals(Object other) { if (this == other) return true; if (other == null) return false; if (!(other instanceof Bid)) return false; Bid that = (Bid) other; <enter/> if (!this.getAmount().equals(that.getAmount())) return false; if (!this.getItem().getId().equals(that.getItem().getId())) return false; return true; } <enter/> @Override public int hashCode() { int result = getAmount().hashCode(); result = 31 * result + getItem().getId().hashCode(); return result; } <enter/> // ... }
Two Bid instances are equal when they have the same amount and are linked with the same Item.
After you’re done with your modifications in detached state, the next step is to store the changes. Using a new persistence context, merge the detached Item and let Hibernate detect the changes:
In the previous example, we said that Hibernate is smart enough to load the Item#bids collection when you merge a detached Item. Hibernate always loads entity associations eagerly with a JOIN when merging, if CascadeType.MERGE is enabled for the association. This is smart in the previous case, where the Item#bids were initialized, detached, and modified. Hibernate loading the collection when merging with a JOIN is therefore necessary and optimal. But if you merge an Item instance with an uninitialized bids collection or an uninitialized seller proxy, Hibernate will fetch the collection and proxy with a JOIN when merging. The merge initializes these associations on the managed Item it returns. CascadeType.MERGE causes Hibernate to ignore and effectively override any FetchType.LAZY mapping (as allowed by the JPA specification). This behavior may not be ideal in some cases, and at the time of writing, it isn’t configurable.
Our next example is less sophisticated, enabling cascaded refreshing of related entities.
The User entity class has a one-to-many relationship with BillingDetails: each user of the application may have several credit cards, bank accounts, and so on. If you aren’t familiar with the BillingDetails class, review the mappings in chapter 6.
You can map the relationship between User and BillingDetails as a unidirectional one-to-many entity association (there is no @ManyToOne):
@Entity @Table(name = "USERS") public class User { <enter/> @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REFRESH}) @JoinColumn(name = "USER_ID", nullable = false) protected Set<BillingDetails> billingDetails = new HashSet<>(); <enter/> // ... }
The cascading options enabled for this association are PERSIST and REFRESH. The PERSIST option simplifies storing billing details; they become persistent when you add an instance of BillingDetails to the collection of an already persistent User.
In section 18.3, we’ll discuss an architecture where the persistence context may be open for a long time, leading to managed entity instances in the context becoming stale. Therefore, in some long-running conversations, you’ll want to reload them from the database. The REFRESH cascading option ensures that when you reload the state of a User instance, Hibernate will also refresh the state of each BillingDetails instance linked to the User:
You first saw replication in section 10.2.7. This nonstandard operation is available on the Hibernate Session API. The main use case is copying data from one database into another.
Consider this many-to-one entity association mapping between Item and User:
@Entity public class Item { <enter/> @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "SELLER_ID", nullable = false) @org.hibernate.annotations.Cascade( org.hibernate.annotations.CascadeType.REPLICATE ) protected User seller; <enter/> // ... }
Here, you enable the REPLICATE cascading option with a Hibernate annotation. Next, you load an Item and its seller from the source database:
After you close the persistence context, the Item and the User entity instances are in detached state. Next, you connect to the database and write the detached data:
tx.begin(); EntityManager otherDatabase = // ... get EntityManager <enter/> otherDatabase.unwrap(Session.class) .replicate(item, ReplicationMode.OVERWRITE); // select ID from ITEM where ID = ? // select ID from USERS where ID = ? <enter/> tx.commit(); // update ITEM set NAME = ?, SELLER_ID = ?, ... where ID = ? // update USERS set USERNAME = ?, ... where ID = ? otherDatabase.close();
When you call replicate() on the detached Item, Hibernate executes SQL SELECT statements to find out whether the Item and its seller are already present in the database. Then, on commit, when the persistence context is flushed, Hibernate writes the values of the Item and the seller into the target database. In the previous example, these rows were already present, so you see an UPDATE of each, overwriting the values in the database. If the target database doesn’t contain the Item or User, two INSERTs are made.
The last cascading option we’re going to discuss is a global setting, enabling transitive persistence for all entity associations.
An object persistence layer is said to implement persistence by reachability if any instance becomes persistent whenever the application creates a reference to the instance from another instance that is already persistent. In the purest form of persistence by reachability, the database has some top-level or root object from which all persistent objects are reachable. Ideally, an instance should become transient and be deleted from the database if it isn’t reachable via references from the root persistent object.
Neither Hibernate nor any other ORM solutions implement this. In fact, there is no analogue of the root persistent object in any SQL database, and no persistent garbage collector can detect unreferenced instances. Object-oriented (network) data stores may implement a garbage-collection algorithm, similar to the one implemented for in-memory objects by the JVM; but this option isn’t available in the ORM world, and scanning all tables for unreferenced rows won’t perform acceptably.
Still, there is some value in the concept of persistence by reachability. It helps you make transient instances persistent and propagate their state to the database without many calls to the persistence manager.
You can enable cascaded persistence for all entity associations in your orm.xml mapping metadata, as a default setting of the persistence unit:
<persistence-unit-metadata> <persistence-unit-defaults> <cascade-persist/> </persistence-unit-defaults> </persistence-unit-metadata>
Hibernate now considers all entity associations in the domain model mapped by this persistence unit as CascadeType.PERSIST. Whenever you create a reference from an already persistent entity instance to a transient entity instance, Hibernate automatically makes that transient instance persistent.
Cascading options are effectively predefined reactions to life cycle events in the persistence engine. If you need to implement a custom procedure when data is stored or loaded, you can implement your own event listeners and interceptors.
In this section, we discuss three different APIs for custom event listeners and persistence life cycle interceptors available in JPA and Hibernate. You can
Let’s start with the standard JPA callbacks. They offer easy access to persist, load, and remove life cycle events.
Let’s say you want to send a notification email to a system administrator whenever a new entity instance is stored. First, write a life cycle event listener with a callback method, annotated with @PostPersist, as shown in the following listing.
You often need access to contextual information and APIs when implementing an event listener. The previous example needs the currently logged-in user and an email API. A simple solution based on thread-locals and singletons might not be sufficient in larger and more complex applications. JPA also standardizes integration with CDI, so an entity listener class may rely on injection and the @Inject annotation to access dependencies. The CDI container provides the contextual information when the listener class is called. Note that even with CDI, you can’t inject the current Entity-Manager to access the database in an event listener. We discuss a different solution for accessing the database in a (Hibernate) event listener later in this chapter.
You may only use each callback annotation once in an entity listener class; that is, only one method may be annotated @PostPersist. See table 13.2 for a summary of all available callback annotations.
Annotation |
Description |
---|---|
@PostLoad | Triggered after an entity instance is loaded into the persistence context, either by identifier lookup, through navigation and proxy/collection initialization, or with a query. Also called after refreshing an already-persistent instance. |
@PrePersist | Called immediately when persist() is called on an entity instance. Also called for merge() when an entity is discovered as transient, after the transient state is copied onto a persistent instance. Also called for associated entities if you enable CascadeType.PERSIST. |
@PostPersist | Called after the database operation for making an entity instance persistent is executed and an identifier value is assigned. This may be at the time when persist() or merge() is invoked, or later when the persistence context is flushed if your identifier generator is pre-insert (see section 4.2.5). Also called for associated entities if you enable CascadeType.PERSIST. |
@PreUpdate, @PostUpdate | Executed before and after the persistence context is synchronized with the database: that is, before and after flushing. Triggered only when the state of the entity requires synchronization (for example, because it’s considered dirty). |
@PreRemove, @PostRemove | Triggered when remove() is called or the entity instance is removed by cascading, and after deletion of the record in the database when the persistence context is flushed. |
An entity listener class must be enabled for any entity you’d like to intercept, such as this Item:
@Entity @EntityListeners( PersistEntityListener.class ) public class Item { <enter/> // ... }
The @EntityListeners annotation accepts an array of listener classes, if you have several interceptors. If several listeners define callback methods for the same event, Hibernate invokes the listeners in the declared order. Alternatively, you can bind listener classes to an entity in XML metadata with the <entity-listener> sub-element of <entity>.
You don’t have to write a separate entity listener class to intercept life cycle events. You can, for example, implement the notifyAdmin() method on the User entity class:
@Entity @Table(name = "USERS") public class User { <enter/> @PostPersist public void notifyAdmin(){ User currentUser = CurrentUser.INSTANCE.get(); Mail mail = Mail.INSTANCE; mail.send( "Entity instance persisted by " + currentUser.getUsername() + ": " + this ); } <enter/> // ... }
Note that callback methods on an entity class don’t have any arguments: the “current” entity involved in the state changes is this. Duplicate callbacks for the same event aren’t allowed in a single class. But you can intercept the same event with callback methods in several listener classes or in a listener and an entity class.
You can also add callback methods on an entity superclass for the entire hierarchy. If, for a particular entity subclass, you want to disable the superclass’s callbacks, annotate the subclass with @ExcludeSuperclassListeners or map it in XML metadata with <exclude-superclass-listeners>.
You can declare default entity listener classes, enabled for all entities in your persistence unit, in XML metadata:
<persistence-unit-metadata> <persistence-unit-defaults> <entity-listeners> <entity-listener class="org.jpwh.model.filtering.callback.PersistEntityListener"/> </entity-listeners> </persistence-unit-defaults> </persistence-unit-metadata>
If you want to disable a default entity listener for a particular entity, either map it with <exclude-default-listeners> in XML metadata or mark it with the @Exclude-Default-Listeners annotation:
@Entity @Table(name = "USERS") @ExcludeDefaultListeners public class User { <enter/> // ... }
Be aware that enabling entity listeners is additive. If you enable and/or bind entity listeners in XML metadata and annotations, Hibernate will call them all in the following order:
1. Default listeners for the persistence unit, in the order as declared in XML metadata.
2. Listeners declared on an entity with @EntityListeners, in the given order.
3. Callback methods declared in entity superclasses are first, starting with the most generic superclass. Callback methods on the entity class are last.
JPA event listeners and callbacks provide a rudimentary framework for reacting to life cycle events with your own procedures. Hibernate also has a more fine-grained and powerful alternative API: org.hibernate.Interceptor.
Let’s assume that you want to write an audit log of data modifications in a separate database table. For example, you may record information about creation and update events for each Item. The audit log includes the user, the date and time of the event, what type of event occurred, and the identifier of the Item that was changed.
Audit logs are often handled using database triggers. On the other hand, it’s sometimes better for the application to take responsibility, especially if portability between different databases is required.
You need several elements to implement audit logging. First, you have to mark the entity classes for which you want to enable audit logging. Next, you define what information to log, such as the user, date, time, and type of modification. Finally, you tie it all together with an org.hibernate.Interceptor that automatically creates the audit trail.
First, create a marker interface, Auditable:
public interface Auditable { public Long getId(); }
This interface requires that a persistent entity class expose its identifier with a getter method; you need this property to log the audit trail. Enabling audit logging for a particular persistent class is then trivial. You add it to the class declaration, such as for Item:
@Entity public class Item implements Auditable { <enter/> // ... }
Now, create a new persistent entity class, AuditLogRecord, with the information you want to log in your audit database table:
@Entity public class AuditLogRecord { <enter/> @Id @GeneratedValue(generator = "ID_GENERATOR") protected Long id; <enter/> @NotNull protected String message; <enter/> @NotNull protected Long entityId; <enter/> @NotNull protected Class entityClass; <enter/> @NotNull protected Long userId; <enter/> @NotNull @Temporal(TemporalType.TIMESTAMP) protected Date createdOn = new Date(); <enter/> // ... }
You want to store an instance of AuditLogRecord whenever Hibernate inserts or updates an Item in the database. A Hibernate interceptor can handle this automatically. Instead of implementing all methods in org.hibernate.Interceptor, extend the EmptyInterceptor and override only the methods you need, as shown next.
EntityManagerFactory emf = JPA.getEntityManagerFactory(); <enter/> Map<String, String> properties = new HashMap<String, String>(); properties.put( org.hibernate.jpa.AvailableSettings.SESSION_INTERCEPTOR, AuditLogInterceptor.class.getName() ); <enter/> EntityManager em = emf.createEntityManager(properties);
If you want to enable an interceptor by default for any EntityManager, you can set the property hibernate.ejb.interceptor in your persistence.xml to a class that implements org.hibernate.Interceptor. Note that unlike a session-scoped interceptor, Hibernate shares this default interceptor, so it must be thread-safe! The example AuditLogInterceptor is not thread-safe.
This EntityManager now has an enabled AuditLogInterceptor, but the interceptor must also be configured with the current Session and logged-in user identifier. This involves some typecasts to access the Hibernate API:
Session session = em.unwrap(Session.class); AuditLogInterceptor interceptor = (AuditLogInterceptor) ((SessionImplementor) session).getInterceptor(); interceptor.setCurrentSession(session); interceptor.setCurrentUserId(CURRENT_USER_ID);
The EntityManager is now ready for use, and an audit trail will be written whenever you store or modify an Item instance with it.
Hibernate interceptors are flexible, and, unlike JPA event listeners and callback methods, you have access to much more contextual information when an event occurs. Having said that, Hibernate allows you to hook even deeper into its core with the extensible event system it’s based on.
The Hibernate core engine is based on a model of events and listeners. For example, if Hibernate needs to save an entity instance, it triggers an event. Whoever listens to this kind of event can catch it and handle saving the data. Hibernate therefore implements all of its core functionality as a set of default listeners, which can handle all Hibernate events.
Hibernate is open by design: you can write and enable your own listeners for Hibernate events. You can either replace the existing default listeners or extend them and execute a side effect or additional procedure. Replacing the event listeners is rare; doing so implies that your own listener implementation can take care of a piece of Hibernate core functionality.
Essentially, all the methods of the Session interface (and its narrower cousin, the EntityManager) correlate to an event. The find() and load() methods trigger a LoadEvent, and by default this event is processed with the DefaultLoadEvent-Listener.
A custom listener should implement the appropriate interface for the event it wants to process and/or extend one of the convenience base classes provided by Hibernate, or any of the default event listeners. Here’s an example of a custom load event listener.
public class SecurityLoadListener extends DefaultLoadEventListener { <enter/> public void onLoad(LoadEvent event, LoadEventListener.LoadType loadType) throws HibernateException { <enter/> boolean authorized = MySecurity.isAuthorized( event.getEntityClassName(), event.getEntityId() ); <enter/> if (!authorized) throw new MySecurityException("Unauthorized access"); <enter/> super.onLoad(event, loadType); } <enter/> }
This listener performs custom authorization code. A listener should be considered effectively a singleton, meaning it’s shared between persistence contexts and thus shouldn’t save any transaction-related state as instance variables. For a list of all events and listener interfaces in native Hibernate, see the API Javadoc of the org.hibernate.event package.
You enable listeners for each core event in your persistence.xml, in a <persistence-unit>:
<properties> <property name="hibernate.ejb.event.load" value="org.jpwh.test.filtering.SecurityLoadListener"/> </properties>
The property name of the configuration setting always starts with hibernate.ejb.event, followed by the type of event you want to listen to. You can find a list of all event types in org.hibernate.event.spi.EventType. The value of the property can be a comma-separated list of listener class names; Hibernate will call each listener in the specified order.
You rarely have to extend the Hibernate core event system with your own functionality. Most of the time, an org.hibernate.Interceptor is flexible enough. It helps to have more options and to be able to replace any piece of the Hibernate core engine in a modular fashion.
The audit-logging implementation you saw in the previous section was very simple. If you need to log more information for auditing, such as the actual changed property values of an entity, consider Hibernate Envers.
Envers is a project of the Hibernate suite dedicated to audit logging and keeping multiple versions of data in the database. This is similar to version control systems you may already be familiar with, such as Subversion and Git.
With Envers enabled, a copy of your data is automatically stored in separate database tables when you add, modify, or delete data in the main tables of the application. Envers internally uses the Hibernate event SPI you saw in the previous section. Envers listens to Hibernate events, and when Hibernate stores changes in the database, Envers creates a copy of the data and logs a revision in its own tables.
Envers groups all data modifications in a unit of work—that is, in a transaction—as a change set with a revision number. You can write queries with the Envers API to retrieve historical data given a revision number or timestamp: for example, “find all Item instances as they were last Friday.” First you have to enable Envers in your application.
Envers is available without further configuration as soon as you put its JAR file on your classpath (or, as shown in the example code of this book, include it as a Maven dependency). You enable audit logging selectively for an entity class with the @org.hibernate.envers.Audited annotation.
@Entity @org.hibernate.envers.Audited public class Item { <enter/> @NotNull protected String name; <enter/> @OneToMany(mappedBy = "item") @org.hibernate.envers.NotAudited protected Set<Bid> bids = new HashSet<Bid>(); <enter/> @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "SELLER_ID", nullable = false) protected User seller; <enter/> // ... }
You’ve now enabled audit logging for Item instances and all properties of the entity. To disable audit logging for a particular property, annotate it with @NotAudited. In this case, Envers ignores the bids but audits the seller. You also have to enable auditing with @Audited on the User class.
Hibernate will now generate (or expect) additional database tables to hold historical data for each Item and User. Figure 13.1 shows the schema for these tables.
The ITEM_AUD and USERS_AUD tables are where the modification history of Item and User instances is stored. When you modify data and commit a transaction, Hibernate inserts a new revision number with a timestamp into the REVINFO table. Then, for each modified and audited entity instance involved in the change set, a copy of its data is stored in the audit tables. Foreign keys on revision number columns link the change set together. The REVTYPE column holds the type of change: whether the entity instance was inserted, updated, or deleted in the transaction. Envers never automatically removes any revision information or historical data; even after you remove() an Item instance, you still have its previous versions stored in ITEM_AUD.
Let’s run through some transactions to see how this works.
In the following code examples, you see several transactions involving an Item and its seller, a User. You create and store an Item and User, then modify both, and then finally delete the Item.
You should already be familiar with this code. Envers automatically creates an audit trail when you work with the EntityManager:
tx.begin(); EntityManager em = JPA.createEntityManager(); <enter/> User user = new User("johndoe"); em.persist(user); <enter/> Item item = new Item("Foo", user); em.persist(item); <enter/> tx.commit(); em.close();
<enter/> tx.begin(); EntityManager em = JPA.createEntityManager(); <enter/> Item item = em.find(Item.class, ITEM_ID); item.setName("Bar"); item.getSeller().setUsername("doejohn"); <enter/> tx.commit(); em.close();
<enter/> tx.begin(); EntityManager em = JPA.createEntityManager(); <enter/> Item item = em.find(Item.class, ITEM_ID); em.remove(item); <enter/> tx.commit(); em.close();
Envers transparently writes the audit trail for this sequence of transactions by logging three change sets. To access this historical data, you first have to obtain the number of the revision, representing the change set you’d like to access.
With the Envers AuditReader API, you can find the revision number of each change set:
In listing 13.5, we assumed that either you know the (approximate) timestamp for a transaction or you have the identifier value of an entity so you can obtain its revisions. If you have neither, you may want to explore the audit log with queries. This is also useful if you have to show a list of all change sets in the user interface of your application.
The following code discovers all revisions of the Item entity class and loads each Item version and the audit log information for that change set:
With a revision number, you can access different versions of the Item and its seller.
The AuditReader#find() operation retrieves only a single entity instance, like Entity-Manager#find(). But the returned entity instances are not in persistent state: the persistence context doesn’t manage them. If you modify an older version of Item, Hibernate won’t update the database. Consider the entity instances returned by the AuditReader API to be detached, or read-only.
AuditReader also has an API for execution of arbitrary queries, similar to the native Hibernate Criteria API (see section 16.3).
Envers supports projection. The following query retrieves only the Item#name of a particular version:
AuditQuery query = auditReader.createQuery() .forEntitiesAtRevision(Item.class, revisionUpdate); <enter/> query.addProjection( AuditEntity.property("name") ); <enter/> assertEquals(query.getResultList().size(), 1); String result = (String)query.getSingleResult(); assertEquals(result, "Bar");
Finally, you may want to roll back an entity instance to an older version. This can be accomplished with the Session#replicate() operation and overwriting an existing row. The following example loads the User instance from the first change set and then overwrites the current User in the database with the older version:
User user = auditReader.find(User.class, USER_ID, revisionCreate); <enter/> em.unwrap(Session.class) .replicate(user, ReplicationMode.OVERWRITE); em.flush(); em.clear(); <enter/> user = em.find(User.class, USER_ID); assertEquals(user.getUsername(), "johndoe");
Envers will also track this change as an update in the audit log; it’s just another new revision of the User instance.
Temporal data is a complex subject, and we encourage you to read the Envers reference documentation for more information. Adding details to the audit log, such as the user who made a change, isn’t difficult. The documentation also shows how you can configure different tracking strategies and customize the database schema used by Envers.
Next, imagine that you don’t want to see all the data in your database. For example, the currently logged-in application user may not have the rights to see everything. Usually, you add a condition to your queries and restrict the result dynamically. This becomes difficult if you have to handle a concern such as security, because you’d have to customize most of the queries in your application. You can centralize and isolate these restrictions with Hibernate’s dynamic data filters.
The first use case for dynamic data filtering relates to data security. A User in Caveat-Emptor may have a ranking property, which is a simple integer:
@Entity @Table(name = "USERS") public class User { <enter/> @NotNull protected int rank = 0; <enter/> // ... }
Now assume that users can only bid on items that other users offer with an equal or lower rank. In business terms, you have several groups of users that are defined by an arbitrary rank (a number), and users can trade only with people who have the same or lower rank.
To implement this requirement, you’d have to customize all queries that load Item instances from the database. You’d check whether the Item#seller you want to load has an equal or lower rank than the currently logged-in user. Hibernate can do this work for you with a dynamic filter.
First, you define your filter with a name and the dynamic runtime parameters it accepts. You can place the Hibernate annotation for this definition on any entity class of your domain model or in a package-info.java metadata file:
@org.hibernate.annotations.FilterDef( name = "limitByUserRank", parameters = { @org.hibernate.annotations.ParamDef( name = "currentUserRank", type = "int" ) } )
This example names this filter limitByUserRank; note that filter names must be unique in a persistence unit. It accepts one runtime argument of type int. If you have several filter definitions, declare them within @org.hibernate.annotations.FilterDefs.
The filter is inactive now; nothing indicates that it’s supposed to apply to Item instances. You must apply and implement the filter on the classes or collections you want to filter.
You want to apply the defined filter on the Item class so that no items are visible if the logged-in user doesn’t have the necessary rank:
@Entity @org.hibernate.annotations.Filter( name = "limitByUserRank", condition = ":currentUserRank >= (" + "select u.RANK from USERS u " + "where u.ID = SELLER_ID" + ")" ) public class Item { <enter/> // ... }
The condition is an SQL expression that’s passed through directly to the database system, so you can use any SQL operator or function. It must evaluate to true if a record should pass the filter. In this example, you use a subquery to obtain the rank of the seller of the item. Unqualified columns, such as SELLER_ID, refer to the table mapped to the entity class. If the currently logged-in user’s rank isn’t greater than or equal to the rank returned by the subquery, the Item instance is filtered out. You can apply several filters by grouping them in an @org.hibernate.annotations.Filters.
A defined and applied filter, if enabled for a particular unit of work, filters out any Item instance that doesn’t pass the condition. Let’s enable it.
You’ve defined a data filter and applied it to a persistent entity class. It’s still not filtering anything—it must be enabled and parameterized in the application for a particular unit of work, with the Session API:
org.hibernate.Filter filter = em.unwrap(Session.class) .enableFilter("limitByUserRank"); <enter/> filter.setParameter("currentUserRank", 0);
You enable the filter by name; the method returns a Filter on which you set the runtime arguments dynamically. You must set the parameters you’ve defined; here it’s set to rank 0. This example then filters out Items sold by a User with a higher rank in this Session.
Other useful methods of the Filter are getFilterDefinition() (which allows you to iterate through the parameter names and types) and validate() (which throws a HibernateException if you forget to set a parameter). You can also set a list of arguments with setParameterList(); this is mostly useful if your SQL restriction contains an expression with a quantifier operator (the IN operator, for example).
Now, every JPQL or criteria query that you execute on the filtered persistence context restricts the returned Item instances:
List<Item> items = em.createQuery("select i from Item i").getResultList(); // select * from ITEM where 0 >= // (select u.RANK from USERS u where u.ID = SELLER_ID)
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery criteria = cb.createQuery(); criteria.select(criteria.from(Item.class)); List<Item> items = em.createQuery(criteria).getResultList(); // select * from ITEM where 0 >= // (select u.RANK from USERS u where u.ID = SELLER_ID)
Note how Hibernate dynamically appends the SQL restriction conditions to the statement generated.
When you first experiment with dynamic filters, you’ll most likely run into an issue with retrieval by identifier. You might expect that em.find(Item.class, ITEM_ID) will be filtered as well. This is not the case, though: Hibernate doesn’t apply filters to retrieval by identifier operations. One of the reasons is that data-filter conditions are SQL fragments, and lookup by identifier may be resolved completely in memory, in the first-level persistence context cache. Similar reasoning applies to filtering of many-to-one or one-to-one associations. If a many-to-one association was filtered (for example, by returning null if you called anItem.getSeller()), the multiplicity of the association would change! You won’t know if the item has a seller or if you aren’t allowed to see it.
But you can dynamically filter collection access. Remember that persistent collections are shorthand for a query.
Until now, calling someCategory.getItems() has returned all Item instances that are referenced by that Category. This can be restricted with a filter applied to a collection:
@Entity public class Category { <enter/> @OneToMany(mappedBy = "category") @org.hibernate.annotations.Filter( name = "limitByUserRank", condition = ":currentUserRank >= (" + "select u.RANK from USERS u " + "where u.ID = SELLER_ID" + ")" ) protected Set<Item> items = new HashSet<Item>(); <enter/> // ... }
If you now enable the filter in a Session, all iteration through a collection of -Category-#items is filtered:
filter.setParameter("currentUserRank", 0); Category category = em.find(Category.class, CATEGORY_ID); assertEquals(category.getItems().size(), 1);
If the current user’s rank is 0, only one Item is loaded when you access the collection. Now, with a rank of 100, you see more data:
filter.setParameter("currentUserRank", 100); category = em.find(Category.class, CATEGORY_ID); assertEquals(category.getItems().size(), 2);
You probably noticed that the SQL condition for both filter applications is the same. If the SQL restriction is the same for all filter applications, you can set it as the default condition when you define the filter, so you don’t have to repeat it:
@org.hibernate.annotations.FilterDef( name = "limitByUserRankDefault", defaultCondition= ":currentUserRank >= (" + "select u.RANK from USERS u " + "where u.ID = SELLER_ID" + ")", parameters = { @org.hibernate.annotations.ParamDef( name = "currentUserRank", type = "int" ) } )
There are many other excellent use cases for dynamic data filters. You’ve seen a restriction of data access given an arbitrary security-related condition. This can be the user rank, a particular group the user must belong to, or a role the user has been assigned. Data might be stored with a regional code (for example, all business contacts of a sales team). Or perhaps each salesperson works only on data that covers their region.
3.15.29.119