Hibernate Envers audit oneToMany side too, even if it has no changes

Have two entities Instrument and Definition.

When instrumentCode changed Envers create audited record only for Instrument.

I want that when instrumentCode changed Envers create audited records for both Instrument and Definition entities. How it is possible to do, and is it possible?

I’ve played with @AuditedJoinTable, @AuditedMappedBy but without luck.

@Audited
@Getter
@Entity
public class Instrument {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(name = "instrument_code")
    protected String instrumentCode;

    @ManyToOne(optional = false, fetch = FetchType.LAZY, targetEntity = Definition.class)
    @JoinColumn(name = "definition_id", nullable = false)
    private Definition definition;
}
//-----
@Audited
@Getter
@Entity
public class Definition {

    @Id
    @Column(nullable = false)
    protected String id;

    @OneToMany(mappedBy = "definition",
            orphanRemoval = true,
            cascade = CascadeType.ALL,
            targetEntity = Instrument.class)
    private Set<Instrument> instruments = Sets.newHashSet();
}

UPD.
Also I’ve debugged and found that in BaseEnversEventListener.generateBidirectionalCollectionChangeWorkUnits() it found my bidirectional relation but do nothing because entity had not been changed because of code:

				// Checking for changes
				final Object oldValue = oldState == null ? null : oldState[i];
				final Object newValue = newState == null ? null : newState[i];

				if ( !EntityTools.entitiesEqual( session, relDesc.getToEntityName(), oldValue, newValue ) ) {

Does it mean that only way to audit Definition when Instrument changed is only to completly rewrite envers onUpdate listener?

Not necessarily.

The easiest route albeit not the most elegant is to introduce a special field on your Definition entity and when you modify an Instrument that you want to have an accompanying Definition, you change the special field on Definition to force the check you referenced to see something changed.

The other route is supplying custom listeners.

The easiest way to supply listeners is to first disable the automatic registration that Envers does when it bootstraps by setting hibernate.envers.autoRegisterListeners=false in your configuration. If you take a look at the source inside EnversIntegrator, you’ll see near the bottom of #integrate where we fetch the listener registry and register the listeners. You simply want to register those all manually, replacing the ones you want to override with your extended implementations accordingly.

In 6.0 (time permitting), I hope to look at more creative ways to allow conditional auditing behavior using annotations rather than this listener override behavior. Something like:

@Audited
@Entity
@AuditListeners(InstrumentDefinitionListener.class)
public class Instrument {
  ..
}

The idea here is that the listener callback would provide a means and a hook into allowing users to inject their own behavior into the auditing of that entity class, where you could force Definition to be audited even though the default behavior isn’t; all without having to ever override/customize the listeners; but again that’s 6.0+ changes.

So, both you suggestions are very surfaced.
Problem with dummy field - is when and from where change it.
Let’s consider

@Entity
public class A {
    private Date lastModified;

    @OneToMany(mappedBy = "a", cascade = CascadeType.ALL)
    private List<B> blist;

    public void touch() {
        lastModified = new Date();
    }
}

public class B {
    @ManyToOne
    private A a; 

    @PreUpdate
    public void ensureParentUpdated() {
        if (a != null) {
            a.touch();
        }
    }
}

So, what if I have to audit not all but only few fields - in this case @PreUpdate is not suitable, exept we do not mind to have “false positive” revisions - where Definition will be audited even if Instrument - not.

About listeners:
At first, remark about registering, I think better way if I only need to override one listener not to set all listeners manually but remove standard and add custom listener, like this:

    private <T> void appendCustomEnversListener(EventListenerRegistry listenerRegistry, EventType<T> eventType,
                                                T listener) {
        removeEnversListenersForEventType(listenerRegistry, eventType);
        listenerRegistry.appendListeners(eventType, listener);
    }

    private <T> void removeEnversListenersForEventType(EventListenerRegistry listenerRegistry, EventType<T> eventType) {
        EventListenerGroup<T> listenerGroup = listenerRegistry.getEventListenerGroup(eventType);
        Iterator<T> listenerIterator = listenerGroup.listeners().iterator();
        while (listenerIterator.hasNext()) {
            T listener = listenerIterator.next();
            if (listener instanceof BaseEnversEventListener) {
                listenerIterator.remove();
            }
        }
    }

Then we can do this:

        appendCustomEnversListener(listenerRegistry, EventType.POST_UPDATE,
                new EnversPostUpdateEventListenerImpl(enversService) {
                    @Override
                    public void onPostUpdate(PostUpdateEvent event) {
                        if (isEntityVersioned(getEnversService(), event.getPersister())
                                && isAuditRequired(event)) {
                            super.onPostUpdate(event);
                            auditBuddyOnUpdate(getEnversService(), event);
                        }
                    }
                });

Where auditBuddy just place code for preparation and inserting ModWorkUnit .

This is how I solved my current issue.

But I have a new one - do the same, but when a collection inside Instrument changed, not Instrument itself. This is much harder to do as onPreCollectionUpdate listener logic is not so straightforward as onUpdate logic.

And I want to say that org.hibernate.envers.internal.revisioninfo.RevisionInfoGenerator#entityChanged give no info about actual changed entity.

Also I want to say that Envers classes breaks SOLID principles constantly. For example AuditProcess - we even not able to see what it store. Or, for example listeners - all methods are private or final protected and this is a PAIN to customize it. Only one way - copy paste all - and this is not good.


Hope this info will help make really helpful changes in 6.0

That can be challenging, particularly if you use an EntityManager or Session directly from your service or controller classes. If you have a set of Repository or DAO like classes, its probably much less of a problem trying to isolate where you’d need to call touch().

If you only audit a small number of fields on the entity, you could use a set of transient fields to control the calling of the touch() method inside the @PreUpdate handler, something like:

@Entity
@Audited
public class MyEntity {
  @Id
  private Integer id;
  private String value1;
  @Audited
  private String data;
  private transient String dataSnapshot;

  @PostLoad
  public void applyAuditSnapshot() {
    this.dataSnapshot = this.data;
  } 

  @PreUpdate
  public void ensureParentUpdated() {
    if ( !Objects.equals( this.dataSnapshot, this.data ) ) {
      a.touch();
    }
  }
}

That would work as well and I agree is much cleaner.

Yep, this kind of gets us full circle back to why I think its much easier to expose annotation hooks with a much higher level api for users to operate with that trying to dive into the logic the listeners themselves handle for various use cases.

What exactly are you referring to, the entity object that was changed?

What exactly do you mean by this, that you aren’t able to iterate the internal map where the class keeps all the AuditWorkUnit instances that have been added?

I certainly don’t disagree with you there.

My honest suggestion is as you’re exporing these routes, if you find that it would be easier to take path A but the way the code is written prevents that and forces path B, don’t hesitate to open a jira issue and explain why the code should be changed. I’d go a step farther and suggest then submit a PR for that jira, request a review from @Naros and we can discuss.

Thanks and please keep the feedback coming!