The ability to attach lifecycle events to an entity using simple annotations sometimes is a neat feature in the Java Persistence API.

The following short snippets demonstrate how to bind and trigger the different available lifecycle events using an embedded derby database and a bunch of annotations.

Dependencies

I’m using Hibernate as persistence manager here and Derby as an easy to setup database. In the last step we’ll be writing a test that’s why we’ve added JUnit and Hamcrest.

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <derby.version>10.9.1.0</derby.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.apache.derby</groupId>
        <artifactId>derbyclient</artifactId>
        <version>${derby.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.derby</groupId>
        <artifactId>derby</artifactId>
        <version>${derby.version}</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>4.1.9.Final</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-all</artifactId>
        <version>1.3</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Creating an Entity

First of all we’re creating the classical JPA entity .. a book entit that has and numeric ID, a title and a date field.

Please notice that we’ve registered two entity listeners for this entity using the @EntityListeners annotation for the class.

package com.hascode.tutorial.entity;

import java.util.Date;

import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import com.hascode.tutorial.entity.listener.BookEntityListener;
import com.hascode.tutorial.entity.listener.TitleValidator;

@Entity
@EntityListeners({ BookEntityListener.class, TitleValidator.class })
public class Book implements TitleEntity {
	@Id
	@GeneratedValue
	private Long id;

	private String title;

	@Temporal(TemporalType.DATE)
	private Date published;

	public final Long getId() {
		return id;
	}

	public final void setId(final Long id) {
		this.id = id;
	}

	@Override
	public final String getTitle() {
		return title;
	}

	public final void setTitle(final String title) {
		this.title = title;
	}

	public final Date getPublished() {
		return published;
	}

	public final void setPublished(final Date published) {
		this.published = published;
	}

	@Override
	public String toString() {
		return "Book [id=" + id + ", title=" + title + ", published="
				+ published + "]";
	}
}

Afterwards we should reference the entity in the persistence.xml in src/test/resources/META-INF

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
	version="1.0">
	<persistence-unit name="hascode-tutorial"
		transaction-type="RESOURCE_LOCAL">
		<provider>org.hibernate.ejb.HibernatePersistence</provider>
		<class>com.hascode.tutorial.entity.Book</class>
		<properties>
			<property name="javax.persistence.jdbc.driver" value="org.apache.derby.jdbc.EmbeddedDriver" />
			<property name="javax.persistence.jdbc.url" value="jdbc:derby:hascode_testdb;create=true" />
			<property name="hibernate.show_sql" value="true" />
			<property name="hibernate.dialect" value="org.hibernate.dialect.DerbyDialect" />
			<property name="hibernate.hbm2ddl.auto" value="create-drop" />
		</properties>
	</persistence-unit>
</persistence>

You might have noticed that our entity implements an interface so this is the TitleEntity interface .. it just got one getter method, getTitle()

package com.hascode.tutorial.entity;

public interface TitleEntity {
	String getTitle();
}

EntityListeners

Now the final part – our entity listeners: The first entity listener responds to every possible entity lifecycle listener and prints out a message.

The following lifecycle events are supported:

  • @PrePersist: triggered before a new entity is persisted (being added to the EntityManager)

  • @PostPersist: – triggered after storing a new entity in the database (during commit or flush).

  • @PostLoad: – triggered after an entity has been retrieved from the database.

  • @PreUpdate: – triggered when an entity is identified as modified by the EntityManager.

  • @PostUpdate: – triggered after updating an entity in the database (during commit or flush).

  • @PreRemove: – triggered when an entity is marked for removal in the EntityManager.

  • @PostRemove: – triggered after deleting an entity from the database (during commit or flush).

package com.hascode.tutorial.entity.listener;

import javax.persistence.PostLoad;
import javax.persistence.PostPersist;
import javax.persistence.PostRemove;
import javax.persistence.PostUpdate;
import javax.persistence.PrePersist;
import javax.persistence.PreRemove;
import javax.persistence.PreUpdate;

import com.hascode.tutorial.entity.Book;

public class BookEntityListener {

	@PrePersist
	public void prePersist(final Book book) {
		System.out.println("prePersist: " + book.toString());
	}

	@PostPersist
	public void postPersist(final Book book) {
		System.out.println("postPersist: " + book.toString());
	}

	@PreUpdate
	public void preUpdate(final Book book) {
		System.out.println("preUpdate: " + book.toString());
	}

	@PostUpdate
	public void postUpdate(final Book book) {
		System.out.println("postUpdate: " + book.toString());
	}

	@PostLoad
	public void postLoad(final Book book) {
		System.out.println("postLoad: " + book.toString());
	}

	@PreRemove
	public void preRemove(final Book book) {
		System.out.println("preRemove: " + book.toString());
	}

	@PostRemove
	public void postRemove(final Book book) {
		System.out.println("postRemove: " + book.toString());
	}

}

The second entity listener is activated before an entity gets persisted and validates the title of the book against a list of bad words.

Please notice that you really should not validate your entities this way – its just for demonstration purpose – if you’re interesting in an efficient way to validate your entities, please feel free to take a look at my tutorial “https://www.hascode.com/2010/12/bean-validation-with-jsr-303-and-hibernate-validator/[Bean Validation with JSR-303 and Hibernate Validator]“.

package com.hascode.tutorial.entity.listener;

import javax.persistence.PersistenceException;
import javax.persistence.PrePersist;

import com.hascode.tutorial.entity.TitleEntity;

public class TitleValidator {
	private final String[] badWords = { "shitty", "xxx", "..." };

	@PrePersist
	public void checkBadWords(final TitleEntity titleEntity) {
		for (String badWord : badWords) {
			if (titleEntity.getTitle().contains(badWord)) {
				System.err.println("bad word in title detected: " + badWord);
				throw new PersistenceException("bad word in title detected: "
						+ badWord);
				// should use ValidationException if bean validation is present
			}
		}
	}
}

Testing

Now that we’ve got everything we need – the last thing is to put it all together and persist some data in our embedded database.

You might be wondering why I’m flushing the entitymanager all the time – this is solely to demonstrate all possible life-cycle events.

package it;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;

import java.sql.SQLException;
import java.util.Date;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import javax.persistence.PersistenceException;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import com.hascode.tutorial.entity.Book;

public class BookIT {
	static EntityManagerFactory emf;
	static EntityManager em;
	static EntityTransaction tx;

	@BeforeClass
	public static void setupClass() throws Exception {
		emf = Persistence.createEntityManagerFactory("hascode-tutorial");
		em = emf.createEntityManager();
		tx = em.getTransaction();
	}

	@Before
	public void setUp() {
		tx.begin();
	}

	@AfterClass
	public static void teardownClass() throws SQLException {
		em.close();
		emf.close();
	}

	@After
	public void tearDown() {
		tx.rollback();
	}

	@Test(timeout = 10000)
	public void shouldTriggerBookLifecycles() throws Exception {
		Book book = new Book();
		book.setPublished(new Date());
		book.setTitle("Some book");
		em.persist(book);
		assertThat(book.getId(), is(notNullValue()));
		assertThat(book.getId(), is(1L));

		book.setPublished(new Date());
		em.persist(book);
		em.flush();
		em.refresh(book); // we just want to trigger postLoad

		Book book2 = em.find(Book.class, 1L);
		assertThat(book2, is(notNullValue()));
		assertThat(book2.getId(), is(book.getId()));
		assertThat(book2.getTitle(), is(book.getTitle()));

		em.remove(book2);
		em.flush();
	}

	@Test(timeout = 10000, expected = PersistenceException.class)
	public void testShouldValidateTitle() throws Exception {
		Book book = new Book();
		book.setPublished(new Date());
		book.setTitle("The shitty boook");
		em.persist(book);
	}
}

The first test produces some similar output:

prePersist: Book [id=null, title=Some book, published=Mon Feb 25 21:55:12 CET 2013]
postPersist: Book [id=1, title=Some book, published=Mon Feb 25 21:55:12 CET 2013]
postLoad: Book [id=1, title=Some book, published=2013-02-25]
preRemove: Book [id=1, title=Some book, published=2013-02-25]
postRemove: Book [id=1, title=Some book, published=2013-02-25]

The second test yields the following output:

bad word in title detected: shitty

Tutorial Sources

Please feel free to download the tutorial sources from my GitHub repository, fork it there or clone it using Git:

git clone https://github.com/hascode/jpa-entitylistener-tutorial.git

Article Updates

  • 2015-03-03: Links to my other JPA articles added.