Implementing a Revision History Strategy

Hello,

I'm looking for advice on ways to keep revision history for all updates to entities in EJB3. Users of my application will need to interact with the historical data, so a simple audit log will not suffice.

As a proof of concept, I implemented a save() method in a Stateless facade that accepts a modified entity as an argument and persists a cloned entity instead of overwriting the original:

1. Date now = new Date();

2. newRecord = modified.clone(); //CLONE MODIFICATIONS

3. em.refresh(modified); //REVERT MODIFICATIONS

4. modified.setOutStamp(now);

5. newRecord.setInStamp(now);

6. newRecord.setOutStamp(null);

7. persist (or merge) both records and return newRecord.

The proof of concept works, but I'm still reading about managed/detached entities, persistence contexts, and transactions. I'm concerned that modifications to the original entity could be flushed to the databases whenever the entity is managed. Other developers would have to be careful about how they obtain the entity and remember to use myFacade.save() instead of em.persist() to avoid bypassing the revision history logic.

I learned of callback methods, and was thinking about using @PostLoad and @PrePersist/@PreUpdate to set/unset some sort of flag to enable persistence. It would be even better if I could schedule all of the revision cloning in a callback method so that it would execute regardless of when/how the entity gets flushed to the database.

One challenge I can think of will be to avoid cascading situations (e.g. making calls to persist a clone object from within a callback method that is itself listening for persist events).

I'd appreciate feedback from anyone who has implemented a similar requirement or can offer general advice on the subject.

Thanks.

[1851 byte] By [devusr4a] at [2007-11-26 21:37:20]
# 1

Hello.

I've been working on some way to keep a revision history in a way that is transparente to the people using the pesistence layer. I've just implemented a proof of concept using @PreUpdate entity listener that does just that. Here it goes:

--Car.java--

package ejb3.tests.entitylistener.persistence;

import javax.persistence.Column;

import javax.persistence.EmbeddedId;

import javax.persistence.Entity;

import javax.persistence.Table;

@Entity

@Table(name = "CAR")

public class Car {

@EmbeddedId

private CarId id;

@Column(name = "HP")

private int horsePower;

//Due to a stupid hibernate bug, when we call persist() on

//the EntityManager, the method @PrePersist, which should be

//called befoe any validation are made, is called after

//hibernate verifies if the non-generated id field is null,

//which result in an Exception. Therefore we must instantiate

//it inside the constructor.

public Car() {

this.id = new CarId();

}

public Car(long carIdId, int version, int horsePower) {

this.id = new CarId();

this.id.setId(carIdId);

this.id.setVersion(version);

this.horsePower = horsePower;

}

public CarId getId() {

return this.id;

}

public void setId(CarId id) {

this.id = id;

}

public int getHorsePower() {

return this.horsePower;

}

public void setHorsePower(int horsePower) {

this.horsePower = horsePower;

}

}

-CarId.java

package ejb3.tests.entitylistener.persistence;

import javax.persistence.Column;

import javax.persistence.Embeddable;

@Embeddable

public class CarId implements java.io.Serializable {

private static final long serialVersionUID = 1;

private long id;

private int version;

public CarId() {

}

public CarId(long id, int version) {

this.id = id;

this.version = version;

}

@Column(name="ID", nullable=false)

public long getId() {

return this.id;

}

public void setId(long id) {

this.id = id;

}

@Column(name="VERSION", nullable=false)

public int getVersion() {

return this.version;

}

public void setVersion(int version) {

this.version = version;

}

public boolean equals(Object other) {

if ( (this == other ) ) return true;

if ( (other == null ) ) return false;

if ( !(other instanceof CarId) ) return false;

CarId castOther = ( CarId ) other;

return (this.getId()==castOther.getId())

&& (this.getVersion()==castOther.getVersion());

}

public int hashCode() {

int result = 17;

result = 37 * result + (int) this.getId();

result = 37 * result + this.getVersion();

return result;

}

}

-CarListener.java

package ejb3.tests.entitylistener.persistence.listener;

import java.sql.Connection;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

import javax.naming.InitialContext;

import javax.naming.NamingException;

import javax.persistence.PrePersist;

import javax.persistence.PreUpdate;

import javax.sql.DataSource;

import ejb3.tests.entitylistener.persistence.Car;

import ejb3.tests.entitylistener.persistence.CarId;

public class CarListener {

private static long initialId = 0;

private Car getCar(long id, int version) throws NamingException, SQLException {

InitialContext ic = new InitialContext();

Object o = ic.lookup(

"java:/DefaultDS");

DataSource ds = (DataSource)o;

Connection conn = ds.getConnection();

PreparedStatement ps =

conn.prepareStatement("SELECT * FROM CAR WHERE ID = ? AND VERSION = ?");

ps.setObject(1, id);

ps.setObject(2, version);

ResultSet rs = ps.executeQuery();

rs.next();

long otherId = (Long)rs.getObject(1);

int otherVersion = (Integer)rs.getObject(2);

int otherHorsePower = (Integer)rs.getObject(3);

rs.close();

ps.close();

conn.close();

return new Car(otherId, otherVersion, otherHorsePower);

}

private void setCar(long id, int version, int horsePower) throws NamingException, SQLException {

InitialContext ic = new InitialContext();

Object o = ic.lookup(

"java:/DefaultDS");

DataSource ds = (DataSource)o;

Connection conn = ds.getConnection();

PreparedStatement ps =

conn.prepareStatement("INSERT INTO CAR VALUES (?, ?, ?)");

ps.setObject(1, id);

ps.setObject(2, version);

ps.setObject(3, horsePower);

ps.executeUpdate();

ps.close();

conn.close();

}

@PreUpdate

public void postUpdate(Car toUpdate) throws NamingException, SQLException {

synchronized(CarListener.class) {

Car c = this.getCar(toUpdate.getId().getId(), toUpdate.getId().getVersion());

boolean bool1 = c.getId().equals(toUpdate.getId());

boolean bool2 = c.getHorsePower() == toUpdate.getHorsePower();

if(!(bool1 && bool2)) {

CarId toUpdateId = toUpdate.getId();

long carIdId = toUpdateId.getId();

int currentVersion = toUpdateId.getVersion();

int newVersion = ++currentVersion;

int horsePower = toUpdate.getHorsePower();

setCar(carIdId, newVersion, horsePower);

toUpdateId.setVersion(newVersion);

}

}

}

@PrePersist

public void createNewId(Car toPersist) {

if(toPersist.getId().getId() == 0) {

synchronized(CarListener.class) {

toPersist.getId().setId(++initialId);

toPersist.getId().setVersion(1);

}

}

}

}

--

Tie the CarListener to the Car via orm.xml's <entity-listeners> node.

If you find a better way to do this, please advise.

Hugo Oliveira

hugom.oliveira.ext@siemens.com

Hugo_Oliveiraa at 2007-7-10 3:19:29 > top of Java-index,Enterprise & Remote Computing,Enterprise Technologies...
# 2

Sorry, but I left some debug code in there that by be congusing.

Here goes the good one

--CarListener.java-

package ejb3.tests.entitylistener.persistence.listener;

import java.sql.Connection;

import java.sql.PreparedStatement;

import java.sql.SQLException;

import javax.naming.InitialContext;

import javax.naming.NamingException;

import javax.persistence.PrePersist;

import javax.persistence.PreUpdate;

import javax.sql.DataSource;

import ejb3.tests.entitylistener.persistence.Car;

import ejb3.tests.entitylistener.persistence.CarId;

public class CarListener {

private static long initialId = 0;

public CarListener() {

}

private void setCar(long id, int version, int horsePower) throws NamingException, SQLException {

InitialContext ic = new InitialContext();

Object o = ic.lookup(

"java:/DefaultDS");

DataSource ds = (DataSource)o;

Connection conn = ds.getConnection();

PreparedStatement ps =

conn.prepareStatement("INSERT INTO CAR VALUES (?, ?, ?)");

ps.setObject(1, id);

ps.setObject(2, version);

ps.setObject(3, horsePower);

ps.executeUpdate();

ps.close();

conn.close();

}

@PreUpdate

public void preUpdate(Car toUpdate) throws NamingException, SQLException {

synchronized(CarListener.class) {

CarId toUpdateId = toUpdate.getId();

long carIdId = toUpdateId.getId();

int currentVersion = toUpdateId.getVersion();

int newVersion = ++currentVersion;

int horsePower = toUpdate.getHorsePower();

setCar(carIdId, newVersion, horsePower);

toUpdateId.setVersion(newVersion);

}

}

@PrePersist

public void createNewId(Car toPersist) {

if(toPersist.getId().getId() == 0) {

synchronized(CarListener.class) {

toPersist.getId().setId(++initialId);

toPersist.getId().setVersion(1);

}

}

}

}

Hugo_Oliveiraa at 2007-7-10 3:19:29 > top of Java-index,Enterprise & Remote Computing,Enterprise Technologies...