We have an auditing framework built off of a HibernateInterceptor. The key portion of the interceptor is using onFlushDirty() to identify properties of objects we want to audit, saving the details, and then writing the audit records during postFlush(). However, we've run across an issue with CollectionOfElements.
One of our objects that uses CollectionOfElements is our user object. It uses a collection of strings stored in a roles table to track a user's roles. Code snippet (other attributes and methods snipped):
Code:
@Entity
@Table(name = "users")
@GenericGenerator(name = "users_seq", strategy = "sequence", parameters = {
@Parameter(name = "sequence", value = "users_seq")})
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@BatchSize(size=100)
public class SimpleUser implements SystemUser, IdentifiableEntity, Externalizable {
private Integer id;
....
private SortedSet<String> roles = new TreeSet<String>();
....
@CollectionOfElements(fetch = FetchType.EAGER)
@Column(name = "role")
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id")
)
@Sort(type = SortType.NATURAL)
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@BatchSize(size=100)
public SortedSet<String> getRoles() {
return roles;
}
public void addRole(String role) {
roles.add(role);
}
public void removeRole(String role) {
roles.remove(role);
}
public void setRoles(SortedSet<String> roles){
this.roles = roles;
}
public boolean hasRole(String... roleNames) {
for(String role: roleNames) {
if (getRoles().contains(role)) {
return true;
}
}
return false;
}
...
}
What we observe is that when the list of roles is modified along with some other attribute, the entity is passed to onFlushDirty(), our code properly observes that the list of roles has changed (it generally winds up being a PersistentSet, so we just call PersistentSet.isDirty()), and the changes all get audited. When only the list of roles is modified, Hibernate does not call onFlushDirty(), in spite of the fact that the PersistentSet's isDirty flag is set to true and Hibernate does indeed save the changes to the database.
In tracking this down through the debugger, I eventually chased the decision point in this case to CollectionType.isDirty():
Code:
public boolean isDirty(Object old, Object current, SessionImplementor session)
throws HibernateException {
// collections don't dirty an unversioned parent entity
// TODO: I don't really like this implementation; it would be better if
// this was handled by searchForDirtyCollections()
return isOwnerVersioned( session ) && super.isDirty( old, current, session );
// return false;
}
public boolean isDirty(Object old, Object current, boolean[] checkable, SessionImplementor session)
throws HibernateException {
return isDirty(old, current, session);
}
This method would seem to never return true unless the entity in question is a versioned entity (which SimpleUser is not). I'm assuming this reflects an assumption that the collection is a collection of Hibernate-mapped entities, which would trigger their own calls to onFlushDirty() if they were modified or their parent association was altered (which we do see and properly audit). However, in this case we have a collection of strings and the only persistence ownership is by the owner entity.
At this point, I feel sort of at a loss. I'm not familiar enough with Hibernate to know where else to try to trace through the source code. It feels to me like this is a bug, given that Hibernate is going to flush changes to this entity but does not call onFlushDirty() for this entity. The only workarounds I can see are to either implement findDirty() (which would be a lot of work and introduce complexity and potential for hard to trace bugs into our code) or mark the entity as versioned (this isn't the only place in the system we use CollectionOfElements though, and I'm hesitant to go through and mark everything versioned just for this). I'm pretty sure from all the documentation I've read through that we've defined the mapping for the roles attribute correctly.
I thought it best to post here rather than to submit a bug report directly. I'm also uncertain how to write a test case to detail this particular use case, and I hate to submit bug reports without a test case.