HistoryEnabled objects.
On the project I'm currently on we need a feature that enables the user to see what data look like at any point in time. To me this looked like a classic out of the book example where you just add the validFrom and validTo columns to the entities, and end up with 2 queries:
- get current data: select * from mytable where validTo is null
- get "historical" data: select * from mytable where givenDate between validFrom and validTo
I discussed this with some other programmers on the project an we agreed that this would probably be a good approach. However it would be nice to implement it as general and transparent as possible so that the cost of adding this feature to any entity would be as low as possible.
I came up with the following solution:
1 Entities have to extend the "HistoryEnabled" class (has validFrom and validTo properties).
2 EventListener plugged into Hibernate intercept save-update events. If object to be saved/update is instanceof HistoryEnabled: inserts a new row instead of overwriting the existing.
3 Use a filter to exclude "nonvalid" (date out of scope) objects when quering.
This was quite easy to implement, however I am a bit unsure how good step 2 is. The reason for that is that we actually change the primary key before saving the object to make Hibernate insert a new row instead of updating the existing row. We've tried to think about scenarious where this would cause issues and have only come up with one so far; If there is a relation between two entities that are both "HistoryEnabled" an "update" would make them inconsistent because they would both reference the old copy instead of the new. This is not a serious problem to us as our domain model is designed in a way that entities that need historical data are not directly associated with other historical objects.
Have anyone got comments on this solution to save historical data?
Here is how we implemented the save-update event listener:
Code:
public class HistoryEnabled{
private Long id;
private Date validFrom;
private Date validTo;
(...)
}
public class SaveHistoricalEventListener extends DefaultSavOrUpdateEventListener {
public void onSaveOrUpdate(SaveOrUpdateEvent evt) throws HibernateException {
boolean abortNormalSave = false;
/* the object we want to save */
Object toBeSaved = evt.getObject();
if (toBeSaved instanceof HistoryEnabled) {
HistoryEnabled dirty = (HistoryEnabled) toBeSaved;
if (dirty.getId() != null) {
Session session = evt.getSession();
/*
* To enable "historical" saving we have to load the original from
* database. Two object with the same primary key can not be
* within the same session, so we have to evict dirty before loading
* the original
*/
session.evict(dirty);
/* Get the original */
HistoryEnabled original = (HistoryEnabled) session
.get(dirty.getClass(), dirty.getId());
if (original != null && original.getValidTo() == null) {
/*
* Ok, we're here. this means that the object exist
* in the database and we are trying to overwrite the
* latest version of this object (validTo == null).
*
* We will change that so
* "dirty" gets a new primary key (doesn't overwrite original)
* "dirty.validFrom" - set value to "new Date()" (becomes the new "current"t)
* "original.validTo" - set value to "new Date()" (becomes "historical")
*/
/* remove primary key so a new row is inserted
* and original is not overwritten */
dirty.setId(null);
/* set correct validTo/From */
dirty.setValidFrom(new Date());
original.setValidTo(dirty.getValidFrom());
session.update(original); // update existing
session.save(dirty); // insert new
/* we've done the saving our way, stop hibernate from messing up */
abortNormalSave = true;
} else if( original != null
&& original.getValidTo() != null ) {
/* not allowed to overwrite historical objects */
abortNormalSave = true;
}
}
}
/* Continue "normal" save if "historical save" was not done */
if (!abortNormalSave) {
super.onSaveOrUpdate(evt);
}
}
}