-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 5 posts ] 
Author Message
 Post subject: onFlushDirty not called when modifying CollectionOfElements
PostPosted: Tue Feb 02, 2010 12:47 pm 
Newbie

Joined: Fri Sep 05, 2008 10:27 am
Posts: 4
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.


Top
 Profile  
 
 Post subject: Re: onFlushDirty not called when modifying CollectionOfElements
PostPosted: Tue Feb 02, 2010 1:29 pm 
Newbie

Joined: Fri Sep 05, 2008 10:27 am
Posts: 4
Still poking around, noticed some additional detail around the dirty check before calling onFlushDirty():

Code:
   private boolean isUpdateNecessary(final FlushEntityEvent event, final boolean mightBeDirty) {
      final Status status = event.getEntityEntry().getStatus();
      if ( mightBeDirty || status==Status.DELETED ) {
         // compare to cached state (ignoring collections unless versioned)
         dirtyCheck(event);
         if ( isUpdateNecessary(event) ) {
            return true;
         }
         else {
            FieldInterceptionHelper.clearDirty( event.getEntity() );
            return false;
         }
      }
      else {
         return hasDirtyCollections( event, event.getEntityEntry().getPersister(), status );
      }
   }
....
   private boolean hasDirtyCollections(FlushEntityEvent event, EntityPersister persister, Status status) {
      if ( isCollectionDirtyCheckNecessary(persister, status) ) {
         DirtyCollectionSearchVisitor visitor = new DirtyCollectionSearchVisitor(
               event.getSession(),
               persister.getPropertyVersionability()
            );
         visitor.processEntityPropertyValues( event.getPropertyValues(), persister.getPropertyTypes() );
         boolean hasDirtyCollections = visitor.wasDirtyCollectionFound();
         event.setHasDirtyCollection(hasDirtyCollections);
         return hasDirtyCollections;
      }
      else {
         return false;
      }
   }

   private boolean isCollectionDirtyCheckNecessary(EntityPersister persister, Status status) {
      return status==Status.MANAGED &&
            persister.isVersioned() &&
            persister.hasCollections();
   }


In our case mightBeDirty is always true and status is always Status.MANAGED. Thus, hasDirtyCollections never gets called. It would return false anyway as it turns out (according to my debugger), since the entity in question is not versioned...

In the end, the change is marked to be flushed after skipping the scheduleUpdate() call (which if I'm following things correctly is what would eventually call onFlushDirty()) during processing of the FlushVisitor:

Collections.prepareCollectionForUpdate(PersistentCollection, CollectionEntry, EntityMode, SessionFactoryImplementor) line: 270
Collections.processReachableCollection(PersistentCollection, CollectionType, Object, SessionImplementor) line: 208
FlushVisitor.processCollection(Object, CollectionType) line: 60
FlushVisitor(AbstractVisitor).processValue(Object, Type) line: 124
FlushVisitor(AbstractVisitor).processValue(int, Object[], Type[]) line: 84
FlushVisitor(AbstractVisitor).processEntityPropertyValues(Object[], Type[]) line: 78
DefaultFlushEntityEventListener.onFlushEntity(FlushEntityEvent) line: 161

This eventually triggers the flush during DefaultFlushEntityEventListener.flushCollections().


Top
 Profile  
 
 Post subject: Re: onFlushDirty not called when modifying CollectionOfElements
PostPosted: Thu Feb 04, 2010 3:32 pm 
Newbie

Joined: Fri Sep 05, 2008 10:27 am
Posts: 4
I have produced a modification to the InterceptorTest that replicates this issue. It adds a collection of elements to the User class (a set of roles) and the necessary test case to replicate the problem. Doesn't seem like I can attach a zip file here, and the JIRA guidelines state to refrain from submitting a JIRA ticket without some response here, so I'm putting the details in-line for review. I'll let this sit for a few days, but I'll probably open a JIRA ticket with the test case if I don't hear anything.

I'd really appreciate any thoughts on straightforward workarounds to implement other than versioning all the objects that have collections of element mappings (which seems inappropriate in many cases) or implementing findDirty() (which would likely introduce additional errors into our codebase in ways that would be difficult to track down when identified).

New test case in org.hibernate.test.interceptor.InterceptorTest (fails on the second assertNotNull in the onFlushDirty verification block):
Code:
        public void testCollectionOfElementsFlushDirty() {
                // Create the basic data
                Session s = openSession( new PropertyInterceptor() );
                Transaction t = s.beginTransaction();
                User u = new User("Gavin", "nivag");
                s.persist(u);
                t.commit();
                s.close();

                // Verify onSave got called but not onFlushDirty (since we created something but didn't update it)
                s = openSession();
                t = s.beginTransaction();
                u = (User) s.get(User.class, "Gavin");
                assertEquals( 0, u.getRoles().size() ); // Verify that there are no roles yet
                assertNotNull( u.getCreated() ); // We've created something
                assertNull( u.getLastUpdated() ); // But not updated it yet
                t.rollback();
                s.close();

                // Modify the entity
                s = openSession( new PropertyInterceptor() );
                t = s.beginTransaction();
                u = (User) s.get(User.class, "Gavin");
                u.getRoles().add("admin");
                t.commit();
                s.close();

                // Verify onFlushDirty was called
                s = openSession();
                t = s.beginTransaction();
                u = (User) s.get(User.class, "Gavin");
                assertEquals( 1, u.getRoles().size() ); // Verify we saved the new role
                assertNotNull( u.getCreated() ); // We had already created it, so this should still be set
                assertNotNull( u.getLastUpdated() ); // We've now updated it, so this should be set
                t.rollback();
                s.close();

                // Cleanup after ourselves so we don't cause problems for other cases
                s = openSession();
                t = s.beginTransaction();
                u = (User) s.get(User.class, "Gavin");
                s.delete(u);
                t.commit();
                s.close();
        }


New org.hibernate.test.interceptor.User (only the roles property is actually new):
Code:
//$Id: User.java 7700 2005-07-30 05:02:47Z oneovthafew $
package org.hibernate.test.interceptor;

import java.util.Calendar;
import java.util.HashSet;
import java.util.Set;

public class User {
        private String name;
        private String password;
        private Set roles = new HashSet();
        private Set actions = new HashSet();
        private Calendar lastUpdated;
        private Calendar created;
       
        public User(String name, String password) {
                super();
                this.name = name;
                this.password = password;
        }
        public User() {
                super();
        }
        public Calendar getLastUpdated() {
                return lastUpdated;
        }
        public void setLastUpdated(Calendar lastUpdated) {
                this.lastUpdated = lastUpdated;
        }
        public String getName() {
                return name;
        }
        public void setName(String name) {
                this.name = name;
        }
        public String getPassword() {
                return password;
        }
        public void setPassword(String password) {
                this.password = password;
        }
        public Set getRoles() {
                return this.roles;
        }
        public void setRoles(Set roles) {
                this.roles = roles;
        }
        public Set getActions() {
                return actions;
        }
        public void setActions(Set actions) {
                this.actions = actions;
        }
        public Calendar getCreated() {
                return created;
        }
        public void setCreated(Calendar created) {
                this.created = created;
        }
}


New User.hbm.xml (only the roles property definition is new):
Code:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<!--

     
-->

<hibernate-mapping
        package="org.hibernate.test.interceptor">
       
        <class name="User" table="users">
                <id name="name"/>
                <property name="password"/>
                <property name="lastUpdated"/>
                <property name="created"/>
                <set name="actions" lazy="false" fetch="join">
                        <key column="user_name"/>
                        <element column="action" type="string"/>
                </set>
                <set name="roles" lazy="false" fetch="join">
                        <key column="user_name"/>
                        <element column="rolename" type="string"/>
                </set>
        </class>
       
        <class name="Log" table="log_entries">
                <id name="id">
                        <generator class="increment"/>
                </id>
                <property name="entityName" column="entity_name"/>
                <property name="entityId" column="entity_id"/>
                <property name="action"/>
                <property name="time" column="action_time"/>
        </class>
       
</hibernate-mapping>


Top
 Profile  
 
 Post subject: Re: onFlushDirty not called when modifying CollectionOfElements
PostPosted: Fri Sep 16, 2011 6:09 pm 
Newbie

Joined: Fri Sep 16, 2011 6:05 pm
Posts: 1
Hi, robkid,

I'm facing now exactly the same problem as you did. Have you found any solution to that?

Thanks in advance!


Top
 Profile  
 
 Post subject: Re: onFlushDirty not called when modifying CollectionOfElements
PostPosted: Mon Sep 19, 2011 10:11 am 
Newbie

Joined: Fri Sep 05, 2008 10:27 am
Posts: 4
No, and I haven't been actively following up on this. For the moment, we've decided that the gaps caused by the bug are more acceptable than the potential problems that would arise from a theoretical findDirty() implementation or wide-spread implementation of versioned objects.

At the present time, seems like there hasn't been any movement on the bug ticket: https://hibernate.onjira.com/browse/HHH-4897


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 5 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.