EclipseLink JPA Tracking Changes

mare picture mare · Mar 28, 2013 · Viewed 8.4k times · Source

I try to log any changes of my JPA entities. For this reason each entity inherits from an abstract entity class which has a list of LogEntry objects.

AbstractEntity class:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@EntityListeners(ChangeListener.class)
public abstract class AbstractEntity implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Version
    private Long version;
    @Temporal(TemporalType.DATE)
    private Date validFrom;
    @Temporal(TemporalType.DATE)
    private Date validTo;
    private String name;
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "abstractEntity")
    private List<LogEntry> logEntry = new ArrayList<LogEntry>();
    //getter and setter
}

LogEntry class:

@Entity
public class LogEntry extends AbstractEntity {

    @ManyToOne
    @JoinColumn
    protected AbstractEntity abstractEntity;
    @ManyToOne
    @JoinColumn
    protected Person person; // creator or updater
    @Column(updatable=false, insertable=false, columnDefinition="TIMESTAMP DEFAULT   CURRENT_TIMESTAMP")
    @Temporal(TemporalType.TIMESTAMP)
    protected Date changeDate;
    protected String comment;
    //getter and setter
}

My approach for this was to create a new LogEntry object and add it to the entity's LogEntry list before an entity got updated or persisted.

I tried the following solutions:

  • Using callback annotations (@PreUpdate, @PrePersist etc.) directly in the entity class or separated in an entity listener connected with AbstractEntity
  • Using EclipsLink's DescriptorEvent and the corresponding callback methods in an entity listener. This was the most promising trial. In preUpdate I could add a new LogEntry to the affected object. The added LogEntry was even correctly persisted, but preUpdate will be invoked by any database operation (select also leads to invocation of preUpdate), so I can't differ between a changed object and an object with no changes. The provided changesets from the descriptor event, the related query or the unitOfWork are in each case null. A comparison of the current object and the old object (provided by the descriptor event) is IMHO too complex, isn't it? On the other hand, in preUpdateWithChanges I could easily detect a changed entity, but at this point it is apparently too late for adding a logentry. The logentry won't be persisted.

Nearly each of these trials enables me to change attributes (like name, or validTo) of the affected entity. But no solution provides the opportunity to create a new LogEntry instance, or rather to persist this LogEntry instance. I also tried to get an instance of a session bean via jndi lookup to persists the LogEntry manually. The jndi lookup works, but calling create or update methods of the session bean has no effect.

My current entity listener looks like this:

public class ChangeListener extends DescriptorEventAdapter {

    @Override
    public void preUpdate(DescriptorEvent event) {
        AbstractEntity entity = (AbstractEntity) event.getObject();
        if (!(entity instanceof LogEntry)) {
            LogEntry logEntry = new LogEntry();
            logEntry.setPerson(getSessionController().getCurrentUser());
            logEntry.setAbstractEntity(entity);
            entity.getLogEntries().add(logEntry);
        }
    }
}

Hibernate Envers is for various reasons no option.

EclipseLink Version is 2.3.2.

Answer

James picture James · Apr 1, 2013

Persisting new object during events from the commit process can be difficult.

You could obtain the changes before the commit and persist you logs (get change set from the UnitOfWork).

See, http://wiki.eclipse.org/EclipseLink/FAQ/JPA#How_to_access_what_changed_in_an_object_or_transaction.3F

Otherwise, it is possible to directly insert and object from inside an event.

i.e.

event.getSession().insertObject(logEntry);