Chapter 13. Filtering data

In this chapter

  • Cascading state transitions
  • Listening to and intercepting events
  • Auditing and versioning with Hibernate Envers
  • Filtering data dynamically

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:

  • In section 13.1, you learn to react to state changes of an entity instance and cascade the state change to associated entities. For example, when a User is saved, Hibernate can transitively and automatically save all related BillingDetails. When an Item is deleted, Hibernate can delete all Bid instances associated with that Item. You can enable this standard JPA feature with special attributes in your entity association and collection mappings.
  • The Java Persistence standard includes life cycle callbacks and event listeners. An event listener is a class you write with special methods, called by Hibernate when an entity instance changes state: for example, after Hibernates loads it or is about to delete it from the database. These callback methods can also be on your entity classes and marked with special annotations. This gives you an opportunity to execute custom side effects when a transition occurs. Hibernate also has several proprietary extension points that allow interception of life cycle events at a lower level within its engine, which we discuss in section 13.2.
  • A common side effect is writing an audit log; such a log typically contains information about the data that was changed, when the change was made, and who made the modification. A more sophisticated auditing system might require storing several versions of data and temporal views; you might want to ask Hibernate to load data “as it was last week.” This being a complex problem, we introduce Hibernate Envers in section 13.3, a subproject dedicated to versioning and auditing in JPA applications.
  • In section 13.4, you see that data filters are also available as a proprietary Hibernate API. These filters add custom restrictions to SQL SELECT statements executed by Hibernate. Hence, you can effectively define a custom limited view of the data in the application tier. For example, you could apply a filter that restricts loaded data by sales region, or any other authorization criteria.

We start with cascading options for transitive state changes.

Major new feature in JPA 2

  • Injection of dependencies through CDI is now supported in JPA entity event listener classes.

13.1. Cascading state transitions

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.

13.1.1. Available cascading options

Table 13.1 summarizes all available cascading options in Hibernate. Note how each is linked with an EntityManager or Session operation.

Table 13.1. Cascading options for entity association mappings

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.

13.1.2. Transitive detachment and merging

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:

Path: /model/src/main/java/org/jpwh/model/filtering/cascade/Item.java

@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:

Path: /examples/src/test/java/org/jpwh/test/filtering/Cascade.java

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:

Path: /examples/src/test/java/org/jpwh/test/filtering/Cascade.java

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:

Path: /model/src/main/java/org/jpwh/model/filtering/cascade/Bid.java

@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:

Path: /examples/src/test/java/org/jpwh/test/filtering/Cascade.java

  1. Hibernate merges the detached item. First it checks whether the persistence context already contains an Item with the given identifier value. In this case, there isn’t any, so the Item is loaded from the database. Hibernate is smart enough to know that it will also need the bids during merging, so it fetches them right away in the same SQL query. Hibernate then copies the detached item values onto the loaded instance, which it returns to you in persistent state. The same procedure is applied to every Bid, and Hibernate will detect that one of the bids is new.
  2. Hibernate made the new Bid persistent during merging. It now has an identifier value assigned.
  3. When you flush the persistence context, Hibernate detects that the name of the Item changed during merging. The new Bid will also be stored. Cascaded merging with collections is a powerful feature; consider how much code you would have to write without Hibernate to implement this functionality.
Eagerly fetching associations when merging

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.

13.1.3. Cascading refresh

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):

Path: /model/src/main/java/org/jpwh/model/filtering/cascade/User.java

@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:

Path: /examples/src/test/java/org/jpwh/test/filtering/Cascade.java

  1. An instance of User is loaded from the database.
  2. Its lazy billingDetails collection is initialized when you iterate through the elements or when you call size().
  3. When you refresh() the managed User instance, Hibernate cascades the operation to the managed BillingDetails and refreshes each with an SQL SELECT. If none of these instances remain in the database, Hibernate throws an EntityNotFoundException. Then, Hibernate refreshes the User instance and eagerly loads the entire billing-Details collection to discover any new BillingDetails. This is a case where Hibernate isn’t as smart as it could be. First it executes an SQL SELECT for each BillingDetails instance in the persistence context and referenced by the collection. Then it loads the entire collection again to find any added BillingDetails. Hibernate could obviously do this with one SELECT. The last cascading option is for the Hibernate-only replicate() operation.

13.1.4. Cascading replication

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:

Path: /model/src/main/java/org/jpwh/model/filtering/cascade/Item.java

@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:

Path: /examples/src/test/java/org/jpwh/test/filtering/Cascade.java

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:

Path: /examples/src/test/java/org/jpwh/test/filtering/Cascade.java

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.

13.1.5. Enabling global transitive persistence

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:

Path: /model/src/main/resources/filtering/DefaultCascadePersist.xml

<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.

13.2. Listening to and intercepting events

In this section, we discuss three different APIs for custom event listeners and persistence life cycle interceptors available in JPA and Hibernate. You can

  • Use the standard JPA life cycle callback methods and event listeners.
  • Write a proprietary org.hibernate.Interceptor and activate it on a Session.
  • Use extension points of the Hibernate core engine with the org.hibernate.event SPI.

Let’s start with the standard JPA callbacks. They offer easy access to persist, load, and remove life cycle events.

13.2.1. JPA event listeners and callbacks

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.

Listing 13.1. Notifying an admin when an entity instance was stored

Path: /model/src/main/java/org/jpwh/model/filtering/callback/PersistEntityListener.java

  1. An entity listener class must have either no constructor or a public no-argument constructor. It doesn’t have to implement any special interfaces. An entity listener is stateless; the JPA engine automatically creates and destroys it.
  2. You may annotate any method of an entity listener class as a callback method for persistence life cycle events. The notifyAdmin() method is invoked after a new entity instance is stored in the database.
  3. Because event listener classes are stateless, it can be difficult to get more contextual information when you need it. Here, you want the currently logged-in user and access to the email system to send a notification. A primitive solution is to use thread-local variables and singletons; you can find the source for CurrentUser and Mail in the example code. A callback method of an entity listener class has a single Object parameter: the entity instance involved in the state change. If you only enable the callback for a particular entity type, you may declare the argument as that specific type. The callback method may have any kind of access; it doesn’t have to be public. It must not be static or final and return nothing. If a callback method throws an unchecked RuntimeException, Hibernate will abort the operation and mark the current transaction for rollback. If a callback method declares and throws a checked Exception, Hibernate will wrap and treat it as a RuntimeException.
Injection in event listener classes

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.

Table 13.2. Life cycle 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:

Path: /model/src/main/java/org/jpwh/model/filtering/callback/Item.java

@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:

Path: /model/src/main/java/org/jpwh/model/filtering/callback/User.java

@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:

Path: /model/src/main/resources/filtering/EventListeners.xml

<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:

Path: /model/src/main/java/org/jpwh/model/filtering/callback/User.java

@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.

13.2.2. Implementing Hibernate interceptors

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:

Path: /model/src/main/java/org/jpwh/model/filtering/interceptor/Auditable.java

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:

Path: /model/src/main/java/org/jpwh/model/filtering/interceptor/Item.java

@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:

Path: /model/src/main/java/org/jpwh/model/filtering/interceptor/AuditLogRecord.java

@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.

Listing 13.2. Hibernate interceptor logging modification events

Path: /examples/src/test/java/org/jpwh/test/filtering/AuditLogInterceptor.java

  1. You need to access the database to write the audit log, so this interceptor needs a Hibernate Session. You also want to store the identifier of the currently logged-in user in each audit log record. The inserts and updates instance variables are collections where this interceptor will hold its internal state.
  2. This method is called when an entity instance is made persistent.
  3. This method is called when an entity instance is detected as dirty during flushing of the persistence context. The interceptor collects the modified Auditable instances in inserts and updates. Note that in onSave(), there may not be an identifier value assigned to the given entity instance. Hibernate guarantees to set entity identifiers during flushing, so the actual audit log trail is written in the postFlush() callback, which isn’t shown in listing 13.2:

Path: /examples/src/test/java/org/jpwh/test/filtering/AuditLogInterceptor.java

  1. This method is called after flushing of the persistence context is complete. Here, you write the audit log records for all insertions and updates you collected earlier.
  2. You can’t access the original persistence context: the Session that is currently executing this interceptor. The Session is in a fragile state during interceptor calls. Hibernate lets you create a new Session that inherits some information from the original Session with the sessionWithOptions() method. The new temporary Session works with the same transaction and database connection as the original Session.
  3. You store a new AuditLogRecord for each insertion and update using the temporary Session.
  4. You flush and close the temporary Session independently from the original Session. You’re now ready to enable this interceptor with a Hibernate property when creating an EntityManager:

Path: /examples/src/test/java/org/jpwh/test/filtering/AuditLogging.java

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);
Enabling default interceptors

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:

Path: /examples/src/test/java/org/jpwh/test/filtering/AuditLogging.java

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.

13.2.3. The core event system

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.

Listing 13.3. Custom load event listener

Path: /examples/src/test/java/org/jpwh/test/filtering/SecurityLoadListener.java

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>:

Path: /model/src/main/resources/META-INF/persistence.xml

<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.

13.3. Auditing and versioning with 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.

13.3.1. Enabling audit logging

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.

Listing 13.4. Enabling audit logging for the Item entity

Path: /model/src/main/java/org/jpwh/model/filtering/envers/Item.java

@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.

Figure 13.1. Audit logging tables for the Item and User entities

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.

13.3.2. Creating an audit trail

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:

Path: /examples/src/test/java/org/jpwh/test/filtering/Envers.java

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();

Path: /examples/src/test/java/org/jpwh/test/filtering/Envers.java

<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();

Path: /examples/src/test/java/org/jpwh/test/filtering/Envers.java

<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.

13.3.3. Finding revisions

With the Envers AuditReader API, you can find the revision number of each change set:

Listing 13.5. Obtaining the revision numbers of change sets

Path: /examples/src/test/java/org/jpwh/test/filtering/Envers.java

  1. The main Envers API is AuditReader. It can be accessed with an EntityManager.
  2. Given a timestamp, you can find the revision number of a change set made before or on that timestamp.
  3. If you don’t have a timestamp, you can get all revision numbers in which a particular audited entity instance was involved. This operation finds all change sets where the given Item was created, modified, or deleted. In our example, we created, modified, and then deleted the Item. Hence, we have three revisions.
  4. If you have a revision number, you can get the timestamp when Envers logged the change set.
  5. We created and modified the User, so there are two revisions.

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:

Path: /examples/src/test/java/org/jpwh/test/filtering/Envers.java

  1. If you don’t know modification timestamps or revision numbers, you can write a query with forRevisionsOfEntity() to obtain all audit trail details of a particular entity.
  2. This query returns the audit trail details as a List of Object[].
  3. Each result tuple contains the entity instance for a particular revision, the revision details (including revision number and timestamp), as well as the revision type.
  4. The revision type indicates why Envers created the revision, because the entity instance was inserted, modified, or deleted in the database. Revision numbers are sequentially incremented; a higher revision number is always a more recent version of an entity instance. You now have revision numbers for the three change sets in the audit trail, giving you access to historical data.

13.3.4. Accessing historical data

With a revision number, you can access different versions of the Item and its seller.

Listing 13.6. Loading historical versions of entity instances

Path: /examples/src/test/java/org/jpwh/test/filtering/Envers.java

  1. The find() method returns an audited entity instance version, given a revision. This operation loads the Item as it was after creation.
  2. This operation loads the Item after it was updated. Note how the modified seller of this change set is also retrieved automatically.
  3. In this revision, the Item was deleted, so find() returns null.
  4. The example didn’t modify the User in this revision, so Envers returns its closest historical revision.

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).

Listing 13.7. Querying historical entity instances

Path: /examples/src/test/java/org/jpwh/test/filtering/Envers.java

  1. This query returns Item instances restricted to a particular revision and change set.
  2. You can add further restrictions to the query; here the Item#name must start with “Ba”.
  3. Restrictions can include entity associations: for example, you’re looking for the revision of an Item sold by a particular User.
  4. You can order query results.
  5. You can paginate through large results.

Envers supports projection. The following query retrieves only the Item#name of a particular version:

Path: /examples/src/test/java/org/jpwh/test/filtering/Envers.java

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:

Path: /examples/src/test/java/org/jpwh/test/filtering/Envers.java

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.

13.4. 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:

Path: /model/src/main/java/org/jpwh/model/filtering/dynamic/User.java

@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.

13.4.1. Defining dynamic filters

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:

Path: /model/src/main/java/org/jpwh/model/filtering/dynamic/package-info.java

@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.

13.4.2. Applying the 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:

Path: /model/src/main/java/org/jpwh/model/filtering/dynamic/Item.java

@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.

13.4.3. Enabling the filter

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:

Path: /examples/src/test/java/org/jpwh/test/filtering/DynamicFilter.java

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:

Path: /examples/src/test/java/org/jpwh/test/filtering/DynamicFilter.java

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)

Path: /examples/src/test/java/org/jpwh/test/filtering/DynamicFilter.java

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.

13.4.4. Filtering collection access

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:

Path: /model/src/main/java/org/jpwh/model/filtering/dynamic/Category.java

@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:

Path: /examples/src/test/java/org/jpwh/test/filtering/DynamicFilter.java

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:

Path: /examples/src/test/java/org/jpwh/test/filtering/DynamicFilter.java

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:

Path: /model/src/main/java/org/jpwh/model/filtering/dynamic/package-info.java

@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.

13.5. Summary

  • Cascading state transitions are predefined reactions to life cycle events in the persistence engine.
  • You learned about listening to and intercepting events. You implement event listeners and interceptors to add custom logic when Hibernate loads and stores data. We introduced JPA’s event listener callbacks and Hibernate’s Interceptor extension point, as well as the Hibernate core event system.
  • You can use Hibernate Envers for audit logging and keeping multiple versions of data in the database (like the version control systems). Using Envers, a copy of your data is automatically stored in separate database tables when you add, modify, or delete data in application tables. Envers groups all data modifications as a change set, in a transaction, with a revision number. You can then query Envers to retrieve historical data.
  • Using dynamic data filters, Hibernate can automatically append arbitrary SQL restrictions to queries it generates.
..................Content has been hidden....................

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