Spring-managed Hibernate Listeners with JPA

A standard use-case – you need an entity listener in order to execute some code on every update/insert/delete. For auditing, for example. But things are not straightforward if you need spring dependencies in your listeners and you are using JPA.

First of all, the JPA-only listeners are insufficient – you can annotate a method with @PreUpdate, but the most you can get as context is the entity it is about (documentation). But you may need the old values. Or the extracted ID of the entity. Or other metadata. All of that is not supported by JPA. So you need to implement hibernate interfaces like PreDeleteEventListener, PreUpdateEventListener, PostInsertEventListener, etc. and get their XEvent objects.

But you can’t easily have these listeners both spring-managed and registered if you are using JPA. You can list them as class names in some hibernate-specific property in persistence.xml, but that way hibernate will instantiate them. Below is the tweaks you need to make in order to get this working:

First, extend the persistence provider:

public class HibernateExtendedPersistenceProvider extends HibernatePersistence {

    private PostInsertEventListener[] postInsertEventListeners;
    private PreUpdateEventListener[] preUpdateEventListeners;
    private PreDeleteEventListener[] preDeleteEventListeners;

    @SuppressWarnings("rawtypes")
    @Override
    public EntityManagerFactory createEntityManagerFactory(String persistenceUnitName, Map properties) {
        Ejb3Configuration cfg = new Ejb3Configuration();
        setupConfiguration(cfg);
        Ejb3Configuration configured = cfg.configure( persistenceUnitName, properties );
        return configured != null ? configured.buildEntityManagerFactory() : null;
    }

    @SuppressWarnings("rawtypes")
    @Override
    public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map properties) {
        Ejb3Configuration cfg = new Ejb3Configuration();
        setupConfiguration(cfg);
        Ejb3Configuration configured = cfg.configure( info, properties );
        return configured != null ? configured.buildEntityManagerFactory() : null;
    }

    private void setupConfiguration(Ejb3Configuration cfg) {
        cfg.getEventListeners().setPostInsertEventListeners(postInsertEventListeners);
        cfg.getEventListeners().setPreDeleteEventListeners(preDeleteEventListeners);
        cfg.getEventListeners().setPreUpdateEventListeners(preUpdateEventListeners);
        //TODO if others are needed - add them
    }

    public void setPostInsertEventListeners(PostInsertEventListener[] postInsertEventListeners) {
        this.postInsertEventListeners = postInsertEventListeners;
    }

    public void setPreUpdateEventListeners(PreUpdateEventListener[] preUpdateEventListeners) {
        this.preUpdateEventListeners = preUpdateEventListeners;
    }

    public void setPreDeleteEventListeners(PreDeleteEventListener[] preDeleteEventListeners) {
        this.preDeleteEventListeners = preDeleteEventListeners;
    }
}

Then annotate your listener(s) with @Component (or declare them as spring beans the way you prefer). Then register them:

<bean id="hibernatePersistenceProvider" class="com.foo.bar.configuration.HibernateExtendedPersistenceProvider">
		<property name="postInsertEventListeners">
			<list>
				<ref bean="hibernateAuditLogListener" />
			</list>
		</property>
		<property name="preUpdateEventListeners">
			<list>
				<ref bean="hibernateAuditLogListener" />
			</list>
		</property>
		<property name="preDeleteEventListeners">
			<list>
				<ref bean="hibernateAuditLogListener" />
			</list>
		</property>
	</bean>

And finally, set the customized persistence provider to the entity manager factory bean:

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    	<property name="persistenceProvider" ref="hibernatePersistenceProvider" />

What you just did:

  • Made use of the fact that the PersistenceProvider allows you to obtain the hibernate Configuration object, which is not otherwise accessible when working with JPA
  • Registered your listeners as spring beans and added them to the extended persistence provider, which in turn registers them with hibernate
  • set the “persistenceProvider” property of spring’s LocalContainerEntityManagerFactoryBean. Normally you don’t set that, because it is inferred from the vendor adapter or from the classpath.

A standard use-case – you need an entity listener in order to execute some code on every update/insert/delete. For auditing, for example. But things are not straightforward if you need spring dependencies in your listeners and you are using JPA.

First of all, the JPA-only listeners are insufficient – you can annotate a method with @PreUpdate, but the most you can get as context is the entity it is about (documentation). But you may need the old values. Or the extracted ID of the entity. Or other metadata. All of that is not supported by JPA. So you need to implement hibernate interfaces like PreDeleteEventListener, PreUpdateEventListener, PostInsertEventListener, etc. and get their XEvent objects.

But you can’t easily have these listeners both spring-managed and registered if you are using JPA. You can list them as class names in some hibernate-specific property in persistence.xml, but that way hibernate will instantiate them. Below is the tweaks you need to make in order to get this working:

First, extend the persistence provider:

public class HibernateExtendedPersistenceProvider extends HibernatePersistence {

    private PostInsertEventListener[] postInsertEventListeners;
    private PreUpdateEventListener[] preUpdateEventListeners;
    private PreDeleteEventListener[] preDeleteEventListeners;

    @SuppressWarnings("rawtypes")
    @Override
    public EntityManagerFactory createEntityManagerFactory(String persistenceUnitName, Map properties) {
        Ejb3Configuration cfg = new Ejb3Configuration();
        setupConfiguration(cfg);
        Ejb3Configuration configured = cfg.configure( persistenceUnitName, properties );
        return configured != null ? configured.buildEntityManagerFactory() : null;
    }

    @SuppressWarnings("rawtypes")
    @Override
    public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map properties) {
        Ejb3Configuration cfg = new Ejb3Configuration();
        setupConfiguration(cfg);
        Ejb3Configuration configured = cfg.configure( info, properties );
        return configured != null ? configured.buildEntityManagerFactory() : null;
    }

    private void setupConfiguration(Ejb3Configuration cfg) {
        cfg.getEventListeners().setPostInsertEventListeners(postInsertEventListeners);
        cfg.getEventListeners().setPreDeleteEventListeners(preDeleteEventListeners);
        cfg.getEventListeners().setPreUpdateEventListeners(preUpdateEventListeners);
        //TODO if others are needed - add them
    }

    public void setPostInsertEventListeners(PostInsertEventListener[] postInsertEventListeners) {
        this.postInsertEventListeners = postInsertEventListeners;
    }

    public void setPreUpdateEventListeners(PreUpdateEventListener[] preUpdateEventListeners) {
        this.preUpdateEventListeners = preUpdateEventListeners;
    }

    public void setPreDeleteEventListeners(PreDeleteEventListener[] preDeleteEventListeners) {
        this.preDeleteEventListeners = preDeleteEventListeners;
    }
}

Then annotate your listener(s) with @Component (or declare them as spring beans the way you prefer). Then register them:

<bean id="hibernatePersistenceProvider" class="com.foo.bar.configuration.HibernateExtendedPersistenceProvider">
		<property name="postInsertEventListeners">
			<list>
				<ref bean="hibernateAuditLogListener" />
			</list>
		</property>
		<property name="preUpdateEventListeners">
			<list>
				<ref bean="hibernateAuditLogListener" />
			</list>
		</property>
		<property name="preDeleteEventListeners">
			<list>
				<ref bean="hibernateAuditLogListener" />
			</list>
		</property>
	</bean>

And finally, set the customized persistence provider to the entity manager factory bean:

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    	<property name="persistenceProvider" ref="hibernatePersistenceProvider" />

What you just did:

  • Made use of the fact that the PersistenceProvider allows you to obtain the hibernate Configuration object, which is not otherwise accessible when working with JPA
  • Registered your listeners as spring beans and added them to the extended persistence provider, which in turn registers them with hibernate
  • set the “persistenceProvider” property of spring’s LocalContainerEntityManagerFactoryBean. Normally you don’t set that, because it is inferred from the vendor adapter or from the classpath.