11

I have an entity loaded by Hibernate (via EntityManager):

User u = em.load(User.class, id)

This class is audited by Hibernate Envers. How can I load the previous version of a User entity?

Brad Mace
  • 27,194
  • 17
  • 102
  • 148
razenha
  • 7,660
  • 6
  • 37
  • 53

5 Answers5

24

Here's another version that finds the previous revision relative to a "current" revision number, so it can be used even if the entity you're looking at isn't the latest revision. It also handles the case where there isn't a prior revision. (em is assumed to be a previously-populated EntityManager)

public static User getPreviousVersion(User user, int current_rev) {
    AuditReader reader = AuditReaderFactory.get(em);

    Number prior_revision = (Number) reader.createQuery()
    .forRevisionsOfEntity(User.class, false, true)
    .addProjection(AuditEntity.revisionNumber().max())
    .add(AuditEntity.id().eq(user.getId()))
    .add(AuditEntity.revisionNumber().lt(current_rev))
    .getSingleResult();

    if (prior_revision != null)
        return (User) reader.find(User.class, user.getId(), prior_revision);
    else
        return null
}

This can be generalized to:

public static T getPreviousVersion(T entity, int current_rev) {
    AuditReader reader = AuditReaderFactory.get(JPA.em());

    Number prior_revision = (Number) reader.createQuery()
    .forRevisionsOfEntity(entity.getClass(), false, true)
    .addProjection(AuditEntity.revisionNumber().max())
    .add(AuditEntity.id().eq(((Model) entity).id))
    .add(AuditEntity.revisionNumber().lt(current_rev))
    .getSingleResult();

    if (prior_revision != null)
        return (T) reader.find(entity.getClass(), ((Model) entity).id, prior_revision);
    else
        return null
}

The only tricky bit with this generalization is getting the entity's id. Because I'm using the Play! framework, I can exploit the fact that all entities are Models and use ((Model) entity).id to get the id, but you'll have to adjust this to suit your environment.

Brad Mace
  • 27,194
  • 17
  • 102
  • 148
  • Won't `forRevisionsOfEntity(clazz,false,true)` return an `Objec[]` with instance, revision info and revisionType? How can it be typecasted to `Number`? – Felipe Leão Feb 17 '17 at 19:19
  • the addProjection changes this to return the revision number, is there a way to have it return the entity directly? (Instead of running the find again afterwards) – Jay Jul 30 '18 at 14:27
11

maybe this then (from AuditReader docs)

AuditReader reader = AuditReaderFactory.get(entityManager);
User user_rev1 = reader.find(User.class, user.getId(), 1);

List<Number> revNumbers = reader.getRevisions(User.class, user_rev1);
User user_previous = reader.find(User.class, user_rev1.getId(),
  revNumbers.get(revNumbers.size()-1));

(I'm very new to this, not sure if I have all the syntax right, maybe the size()-1 should be size()-2?)

Jason S
  • 184,598
  • 164
  • 608
  • 970
  • 1
    It does need to be `size()-2` to get the second-to-last revision – Brad Mace Aug 11 '11 at 13:53
  • Why there is `user_rev1` at all? `reader.getRevisions` accepts primaryKey as second argument. Not entity. So all you need is `AuditReader reader = AuditReaderFactory.get(entityManager); List revNumbers = reader.getRevisions(User.class, user.getId()); User user_previous = reader.find(User.class, user.getId(), revNumbers.get(revNumbers.size()-1));` – Ruslan Stelmachenko Jan 13 '16 at 00:57
4

I think it would be this:

final AuditReader reader = AuditReaderFactory.get( entityManagerOrSession );

// This could probably be declared as Long instead of Object
final Object pk = userCurrent.getId();

final List<Number> userRevisions = reader.getRevisions( User.class, pk );

final int revisionCount = userRevision.size();

final Number previousRevision = userRevisions.get( revisionCount - 2 );

final User userPrevious = reader.find( User.class, pk, previousRevision );
Jamie Bisotti
  • 2,605
  • 19
  • 23
2

Building off of the excellent approach of @brad-mace, I have made the following changes:

  • You should pass in your EntityClass and Id instead of hardcoding and assuming the Model.
  • Don't hardcode your EntityManager.
  • There is no point setting selectDeleted, because a deleted record can never be returned as the previous revision.
  • Calling get single result with throw and exception if no results or more than 1 result is found, so either call resultlist or catch the exception (this solution calls getResultList with maxResults = 1)
  • Get the revision, type, and entity in one transaction (remove the projection, use orderBy and maxResults, and query for the Object[3] )

So here's another solution:

public static <T> T getPreviousRevision(EntityManager entityManager, Class<T> entityClass, Object entityId, int currentRev) {
    AuditReader reader = AuditReaderFactory.get(entityManager);
    List<Object[]> priorRevisions = (List<Object[]>) reader.createQuery()
            .forRevisionsOfEntity(entityClass, false, false)
            .add(AuditEntity.id().eq(entityId))
            .add(AuditEntity.revisionNumber().lt(currentRev))
            .addOrder(AuditEntity.revisionNumber().desc())
            .setMaxResults(1)
            .getResultList();

    if (priorRevisions.size() == 0) {
        return null;
    }
    // The list contains a single Object[] with entity, revinfo, and type 
    return (T) priorRevision.get(0)[0];
}
Steven Spungin
  • 27,002
  • 5
  • 88
  • 78
1

From the docs:

AuditReader reader = AuditReaderFactory.get(entityManager);
User user_rev1 = reader.find(User.class, user.getId(), 1);
Rich Kroll
  • 3,995
  • 3
  • 23
  • 28
  • 2
    I'm not sure this works for my, I want to get the previous version, your example get a mandatory version (in this case, the version number 1). – razenha Apr 27 '09 at 15:30