persistence@glassfish.java.net

RE: Re1: Fix for issue 1139 "cascade merge"

From: Gordon Yorke <gordon.yorke_at_oracle.com>
Date: Fri, 29 Sep 2006 10:59:37 -0400

Hello Wonseok,
    The updates look great, I'll get them checked in by Tuesday morning.
--Gordon
  -----Original Message-----
  From: Wonseok Kim [mailto:guruwons_at_gmail.com]
  Sent: Thursday, September 28, 2006 10:30 PM
  To: persistence_at_glassfish.dev.java.net
  Subject: Re: Re1: Fix for issue 1139 "cascade merge"


  Thanks Gordon for your kind answer.

  I modified the fix as we discussed and moved tests into EntityManagerJUnitTestSuite.
  There was no failure in entity-persistence-tests and quick look tests.
  Please review and check in if it's OK. I hope this is the final :-)

  Diffs are below and modified sources are in the attached file.
  Regards
  - Wonseok

  [entity-persistence]
  Index: src/java/oracle/toplink/essentials/internal/descriptors/ObjectBuilder.java
  ===================================================================
  RCS file: /cvs/glassfish/entity-persistence/src/java/oracle/toplink/essentials/internal/descriptors/ObjectBuilder.java,v
  retrieving revision 1.15
  diff -c -r1.15 ObjectBuilder.java
  *** src/java/oracle/toplink/essentials/internal/descriptors/ObjectBuilder.java 18 Aug 2006 17:59:26 -0000 1.15
  --- src/java/oracle/toplink/essentials/internal/descriptors/ObjectBuilder.java 29 Sep 2006 00:48:17 -0000
  ***************
  *** 2052,2061 ****
         * This merge also prevents the extra step of calculating the changes when it is not required.
         */
        public void mergeIntoObject(Object target, boolean isUnInitialized, Object source, MergeManager mergeManager) {
            // PERF: Avoid synchronized enumerator as is concurrency bottleneck.
            Vector mappings = getDescriptor().getMappings();
            for (int index = 0; index < mappings.size(); index++) {
  ! ((DatabaseMapping)mappings.get(index)).mergeIntoObject(target, isUnInitialized, source, mergeManager);
            }
    
            // PERF: Avoid events if no listeners.
  --- 2052,2077 ----
         * This merge also prevents the extra step of calculating the changes when it is not required.
         */
        public void mergeIntoObject(Object target, boolean isUnInitialized, Object source, MergeManager mergeManager) {
  + mergeIntoObject(target, isUnInitialized, source, mergeManager, false);
  + }
  +
  + /**
  + * INTERNAL:
  + * Merge the contents of one object into another, this merge algorthim is dependent on the merge manager.
  + * This merge also prevents the extra step of calculating the changes when it is not required.
  + * If 'cascadeOnly' is true, only foreign reference mappings are merged.
  + */
  + public void mergeIntoObject(Object target, boolean isUnInitialized, Object source, MergeManager mergeManager, boolean cascadeOnly) {
  + // cascadeOnly is introduced to optimize merge
  + // for GF#1139 Cascade merge operations to relationship mappings even if already registered
  +
            // PERF: Avoid synchronized enumerator as is concurrency bottleneck.
            Vector mappings = getDescriptor().getMappings();
            for (int index = 0; index < mappings.size(); index++) {
  ! DatabaseMapping mapping = (DatabaseMapping)mappings.get(index);
  ! if(!cascadeOnly || mapping.isForeignReferenceMapping()){
  ! mapping.mergeIntoObject(target, isUnInitialized, source, mergeManager);
  ! }
            }
    
            // PERF: Avoid events if no listeners.
  Index: src/java/oracle/toplink/essentials/internal/ejb/cmp3/base/EntityManagerImpl.java
  ===================================================================
  RCS file: /cvs/glassfish/entity-persistence/src/java/oracle/toplink/essentials/internal/ejb/cmp3/base/EntityManagerImpl.java,v
  retrieving revision 1.23
  diff -c -r1.23 EntityManagerImpl.java
  *** src/java/oracle/toplink/essentials/internal/ejb/cmp3/base/EntityManagerImpl.java 2 Sep 2006 00:15:37 -0000 1.23
  --- src/java/oracle/toplink/essentials/internal/ejb/cmp3/base/EntityManagerImpl.java 29 Sep 2006 00:48:17 -0000
  ***************
  *** 195,201 ****
                throw new IllegalArgumentException(ExceptionLocalization.buildMessage("cannot_merge_removed_entity", new Object[]{entity}));
            }
            try {
  ! return getActivePersistenceContext(checkForTransaction(!isExtended())).mergeCloneWithReferences(entity, MergeManager.CASCADE_BY_MAPPING);
            } catch (oracle.toplink.essentials.exceptions.OptimisticLockException ole) {
                throw new javax.persistence.OptimisticLockException(ole);
            }
  --- 195,201 ----
                throw new IllegalArgumentException(ExceptionLocalization.buildMessage("cannot_merge_removed_entity", new Object[]{entity}));
            }
            try {
  ! return getActivePersistenceContext(checkForTransaction(!isExtended())).mergeCloneWithReferences(entity, MergeManager.CASCADE_BY_MAPPING, true);
            } catch (oracle.toplink.essentials.exceptions.OptimisticLockException ole) {
                throw new javax.persistence.OptimisticLockException(ole);
            }
  Index: src/java/oracle/toplink/essentials/internal/sessions/MergeManager.java
  ===================================================================
  RCS file: /cvs/glassfish/entity-persistence/src/java/oracle/toplink/essentials/internal/sessions/MergeManager.java,v
  retrieving revision 1.6
  diff -c -r1.6 MergeManager.java
  *** src/java/oracle/toplink/essentials/internal/sessions/MergeManager.java 20 Apr 2006 20:32:05 -0000 1.6
  --- src/java/oracle/toplink/essentials/internal/sessions/MergeManager.java 29 Sep 2006 00:48:18 -0000
  ***************
  *** 82,87 ****
  --- 82,91 ----
        protected long systemTime = 0;// stored so that all objects merged by a merge manager can have the same readTime
        public static boolean LOCK_ON_MERGE = true;
    
  + /** Force cascade merge even if a clone is already registered */
  + // GF#1139 Cascade doesn't work when merging managed entity
  + protected boolean forceCascade;
  +
        public MergeManager(AbstractSession session) {
            this.session = session;
            this.objectsAlreadyMerged = new IdentityHashtable();
  ***************
  *** 387,402 ****
            ClassDescriptor descriptor = getSession().getDescriptor(rmiClone);
            Object registeredObject = registerObjectForMergeCloneIntoWorkingCopy(rmiClone);
    
  ! if (registeredObject == rmiClone) {
                //need to find better better fix. prevents merging into itself.
                return rmiClone;
            }
    
  - boolean changeTracked = false;
            try {
                ObjectBuilder builder = descriptor.getObjectBuilder();
                
  ! if ( descriptor.usesVersionLocking()) {
                    VersionLockingPolicy policy = (VersionLockingPolicy) descriptor.getOptimisticLockingPolicy();
                    if (policy.isStoredInObject()) {
                        Object currentValue = builder.extractValueFromObjectForField(registeredObject, policy.getWriteLockField(), session);
  --- 391,405 ----
            ClassDescriptor descriptor = getSession().getDescriptor(rmiClone);
            Object registeredObject = registerObjectForMergeCloneIntoWorkingCopy(rmiClone);
    
  ! if (registeredObject == rmiClone && !shouldForceCascade()) {
                //need to find better better fix. prevents merging into itself.
                return rmiClone;
            }
    
            try {
                ObjectBuilder builder = descriptor.getObjectBuilder();
                
  ! if (registeredObject != rmiClone && descriptor.usesVersionLocking()) {
                    VersionLockingPolicy policy = (VersionLockingPolicy) descriptor.getOptimisticLockingPolicy();
                    if (policy.isStoredInObject()) {
                        Object currentValue = builder.extractValueFromObjectForField(registeredObject, policy.getWriteLockField (), session);
  ***************
  *** 410,417 ****
                // Toggle change tracking during the merge.
                descriptor.getObjectChangePolicy().dissableEventProcessing(registeredObject);
                
                // Merge into the clone from the original, use clone as backup as anything different should be merged.
  ! builder.mergeIntoObject(registeredObject, false, rmiClone, this);
            } finally {
                descriptor.getObjectChangePolicy().enableEventProcessing(registeredObject);
            }
  --- 413,425 ----
                // Toggle change tracking during the merge.
                descriptor.getObjectChangePolicy ().dissableEventProcessing(registeredObject);
                
  + boolean cascadeOnly = false;
  + if(registeredObject == rmiClone){
  + // GF#1139 Cascade merge operations to relationship mappings even if already registered
  + cascadeOnly = true;
  + }
                // Merge into the clone from the original, use clone as backup as anything different should be merged.
  ! builder.mergeIntoObject (registeredObject, false, rmiClone, this, cascadeOnly);
            } finally {
                descriptor.getObjectChangePolicy().enableEventProcessing(registeredObject);
            }
  ***************
  *** 693,698 ****
  --- 701,710 ----
            this.mergePolicy = mergePolicy;
        }
    
  + public void setForceCascade(boolean forceCascade) {
  + this.forceCascade = forceCascade;
  + }
  +
        public void setObjectDescriptors(IdentityHashtable objectDescriptors) {
            this.objectDescriptors = objectDescriptors;
        }
  ***************
  *** 817,821 ****
  --- 829,840 ----
         */
        public boolean shouldRefreshRemoteObject() {
            return getMergePolicy() == REFRESH_REMOTE_OBJECT;
  + }
  +
  + /**
  + * This is used to cascade merge even if a clone is already registered.
  + */
  + public boolean shouldForceCascade() {
  + return forceCascade;
        }
    }
  Index: src/java/oracle/toplink/essentials/internal/sessions/UnitOfWorkImpl.java
  ===================================================================
  RCS file: /cvs/glassfish/entity-persistence/src/java/oracle/toplink/essentials/internal/sessions/UnitOfWorkImpl.java,v
  retrieving revision 1.14
  diff -c -r1.14 UnitOfWorkImpl.java
  *** src/java/oracle/toplink/essentials/internal/sessions/UnitOfWorkImpl.java 18 Jul 2006 20:23:02 -0000 1.14
  --- src/java/oracle/toplink/essentials/internal/sessions/UnitOfWorkImpl.java 29 Sep 2006 00:48:19 -0000
  ***************
  *** 2665,2671 ****
         * @see #deepMergeClone(Object)
         */
        public Object mergeCloneWithReferences(Object rmiClone, int cascadePolicy) {
  !
           if (rmiClone == null) {
                return null;
            }
  --- 2665,2689 ----
         * @see #deepMergeClone(Object)
         */
        public Object mergeCloneWithReferences(Object rmiClone, int cascadePolicy) {
  ! return mergeCloneWithReferences(rmiClone, cascadePolicy, false);
  ! }
  !
  ! /**
  ! * INTERNAL:
  ! * Merge the attributes of the clone into the unit of work copy.
  ! * This can be used for objects that are returned from the client through
  ! * RMI serialization (or another serialization mechanism), because the RMI object
  ! * will be a clone this will merge its attributes correctly to preserve object
  ! * identity within the unit of work and record its changes.
  ! *
  ! * The object and its private owned parts are merged. This will include references from
  ! * dependent objects to independent objects.
  ! *
  ! * @return the registered version for the clone being merged.
  ! * @see #shallowMergeClone(Object)
  ! * @see #deepMergeClone(Object)
  ! */
  ! public Object mergeCloneWithReferences(Object rmiClone, int cascadePolicy, boolean forceCascade) {
           if (rmiClone == null) {
                return null;
            }
  ***************
  *** 2686,2691 ****
  --- 2704,2710 ----
            MergeManager manager = new MergeManager(this);
            manager.mergeCloneWithReferencesIntoWorkingCopy();
            manager.setCascadePolicy(cascadePolicy);
  + manager.setForceCascade(forceCascade);
            Object mergedObject = manager.mergeChanges(implementation, null);
            if (isSmartMerge()) {
                return builder.wrapObject(mergedObject, this);

  [entity-persistence-tests]
  Index: src/java/oracle/toplink/essentials/testing/models/cmp3/advanced/Employee.java
  ===================================================================
  RCS file: /cvs/glassfish/entity-persistence-tests/src/java/oracle/toplink/essentials/testing/models/cmp3/advanced/Employee.java,v
  retrieving revision 1.10
  diff -c -r1.10 Employee.java
  *** src/java/oracle/toplink/essentials/testing/models/cmp3/advanced/Employee.java 8 Sep 2006 18:53:37 -0000 1.10
  --- src/java/oracle/toplink/essentials/testing/models/cmp3/advanced/Employee.java 29 Sep 2006 00:47:26 -0000
  ***************
  *** 205,211 ****
            this.manager = manager;
        }
    
  ! @ManyToMany(cascade=PERSIST)
        @JoinTable(
            name="CMP3_EMP_PROJ",
            // Default for the project side and specify for the employee side
  --- 205,211 ----
            this.manager = manager;
        }
    
  ! @ManyToMany(cascade={PERSIST, MERGE})
        @JoinTable(
            name="CMP3_EMP_PROJ",
            // Default for the project side and specify for the employee side
  Index: src/java/oracle/toplink/essentials/testing/models/cmp3/advanced/Project.java
  ===================================================================
  RCS file: /cvs/glassfish/entity-persistence-tests/src/java/oracle/toplink/essentials/testing/models/cmp3/advanced/Project.java,v
  retrieving revision 1.3
  diff -c -r1.3 Project.java
  *** src/java/oracle/toplink/essentials/testing/models/cmp3/advanced/Project.java 20 Jan 2006 15:57:15 -0000 1.3
  --- src/java/oracle/toplink/essentials/testing/models/cmp3/advanced/Project.java 29 Sep 2006 00:47:26 -0000
  ***************
  *** 111,117 ****
            this.description = description;
        }
    
  ! @OneToOne
        @JoinColumn(name="LEADER_ID")
        public Employee getTeamLeader() {
            return teamLeader;
  --- 111,117 ----
            this.description = description;
        }
    
  ! @OneToOne(cascade = {CascadeType.MERGE})
        @JoinColumn(name="LEADER_ID")
        public Employee getTeamLeader() {
            return teamLeader;
  Index: src/java/oracle/toplink/essentials/testing/tests/cmp3/advanced/EntityManagerJUnitTestSuite.java
  ===================================================================
  RCS file: /cvs/glassfish/entity-persistence-tests/src/java/oracle/toplink/essentials/testing/tests/cmp3/advanced/EntityManagerJUnitTestSuite.java,v
  retrieving revision 1.30
  diff -c -r1.30 EntityManagerJUnitTestSuite.java
  *** src/java/oracle/toplink/essentials/testing/tests/cmp3/advanced/EntityManagerJUnitTestSuite.java 18 Sep 2006 20:48:29 -0000 1.30
  --- src/java/oracle/toplink/essentials/testing/tests/cmp3/advanced/EntityManagerJUnitTestSuite.java 29 Sep 2006 00:47:27 -0000
  ***************
  *** 2556,2561 ****
  --- 2556,2697 ----
            fail("No exception thrown when primary key update attempted.");
        }
        
  + // Test cascade merge on a detached entity
  + public void testCascadeMergeDetached() {
  + // setup
  + Project p1 = new Project();
  + p1.setName("Project1");
  + Project p2 = new Project();
  + p1.setName("Project2");
  + Employee e1 = new Employee();
  + e1.setFirstName("Employee1");
  + Employee e2 = new Employee();
  + e2.setFirstName("Employee2");
  +
  + EntityManager em = createEntityManager();
  + em.getTransaction().begin();
  + try {
  + em.persist(p1);
  + em.persist(p2);
  + em.persist(e1);
  + em.persist(e2);
  +
  + em.getTransaction().commit();
  + } catch (RuntimeException re){
  + em.getTransaction().rollback();
  + throw re;
  + }
  + em.close();
  + // end of setup
  +
  + //p1,p2,e1,e2 are detached
  +
  + // associate relationships
  + //p1 -> e1 (one-to-one)
  + p1.setTeamLeader(e1);
  + //e1 -> e2 (one-to-many)
  + e1.addManagedEmployee(e2);
  + //e2 -> p2 (many-to-many)
  + e2.addProject(p2);
  + p2.addTeamMember(e2);
  +
  + em = createEntityManager();
  + em.getTransaction().begin();
  + try {
  + Project mp1 = em.merge(p1); // cascade merge
  + assertTrue(em.contains(mp1));
  + assertTrue("Managed instance and detached instance must not be same", mp1 != p1);
  +
  + Employee me1 = mp1.getTeamLeader();
  + assertTrue("Cascade merge failed", em.contains(me1));
  + assertTrue("Managed instance and detached instance must not be same", me1 != e1);
  +
  + Employee me2 = me1.getManagedEmployees().iterator().next();
  + assertTrue("Cascade merge failed", em.contains(me2));
  + assertTrue("Managed instance and detached instance must not be same", me2 != e2);
  +
  + Project mp2 = me2.getProjects().iterator().next();
  + assertTrue("Cascade merge failed", em.contains(mp2));
  + assertTrue("Managed instance and detached instance must not be same", mp2 != p2);
  +
  + em.getTransaction().commit();
  + } catch (RuntimeException re){
  + em.getTransaction().rollback();
  + throw re;
  + }
  + em.close();
  + }
  +
  + // Test cascade merge on a managed entity
  + // Test for GF#1139 - Cascade doesn't work when merging managed entity
  + public void testCascadeMergeManaged() {
  + // setup
  + Project p1 = new Project();
  + p1.setName("Project1");
  + Project p2 = new Project();
  + p1.setName("Project2");
  + Employee e1 = new Employee();
  + e1.setFirstName("Employee1");
  + Employee e2 = new Employee();
  + e2.setFirstName("Employee2");
  +
  + EntityManager em = createEntityManager();
  + em.getTransaction().begin();
  + try {
  + em.persist(p1);
  + em.persist(p2);
  + em.persist(e1);
  + em.persist(e2);
  +
  + em.getTransaction().commit();
  + } catch (RuntimeException re){
  + em.getTransaction().rollback();
  + throw re;
  + }
  + em.close();
  + // end of setup
  +
  + //p1,p2,e1,e2 are detached
  + em = createEntityManager();
  + em.getTransaction().begin();
  + try {
  + Project mp1 = em.merge(p1);
  + assertTrue(em.contains(mp1));
  + assertTrue("Managed instance and detached instance must not be same", mp1 != p1);
  +
  + //p1 -> e1 (one-to-one)
  + mp1.setTeamLeader(e1);
  + mp1 = em.merge(mp1); // merge again - trigger cascade merge
  +
  + Employee me1 = mp1.getTeamLeader ();
  + assertTrue("Cascade merge failed", em.contains(me1));
  + assertTrue("Managed instance and detached instance must not be same", me1 != e1);
  +
  + //e1 -> e2 (one-to-many)
  + me1.addManagedEmployee(e2);
  + me1 = em.merge(me1); // merge again - trigger cascade merge
  +
  + Employee me2 = me1.getManagedEmployees().iterator().next();
  + assertTrue("Cascade merge failed", em.contains(me2));
  + assertTrue("Managed instance and detached instance must not be same", me2 != e2);
  +
  + //e2 -> p2 (many-to-many)
  + me2.addProject(p2);
  + p2.addTeamMember(me2);
  + me2 = em.merge(me2); // merge again - trigger cascade merge
  +
  + Project mp2 = me2.getProjects().iterator().next();
  + assertTrue("Cascade merge failed", em.contains(mp2));
  + assertTrue("Managed instance and detached instance must not be same", mp2 != p2);
  +
  + em.getTransaction ().commit();
  + } catch (RuntimeException re){
  + em.getTransaction().rollback();
  + throw re;
  + }
  + em.close();
  + }
  +
        public static void main(String[] args) {
            // Now run JUnit.
            junit.swingui.TestRunner.main(args);



  On 9/29/06, Gordon Yorke < gordon.yorke_at_oracle.com> wrote:
    Hello Wonseok,
        Perhaps this...
        public void mergeIntoObject(Object target, boolean isUnInitialized, Object source, MergeManager mergeManager, boolean cascadeOnly) {
            // PERF: Avoid synchronized enumerator as is concurrency bottleneck.
            Vector mappings = getDescriptor().getMappings();
                for (int index = 0; index < mappings.size(); index++) {
                    DatabaseMapping mapping = (DatabaseMapping)mappings.get(index);
                    if( !cascadeOnly || mapping.isForeignReferenceMapping()){
                        //cascadeOnly = F, frm = X, merge = true;
                        //cascadeOnly = T, frm = T, merge = true;
                        //cascadeOnly = T, frm = F, merge = false;
                        mapping.mergeIntoObject (target, isUnInitialized, source, mergeManager);
                    }
               }
           }
    This is a much cleaner method that performs optimally without the need for duplication of code. Calls to mergeIntoObject that should not be cascaded are already checked in mergeIntoObject code before any major work is attempted by these methods.
    --Gordon


    -----Original Message-----
    From: Wonseok Kim [mailto:guruwons_at_gmail.com]
    Sent: Wednesday, September 27, 2006 6:39 PM
    To: persistence_at_glassfish.dev.java.net
    Subject: Re1: Fix for issue 1139 "cascade merge"


      Gordon,
      Please review below message so I could submit the final fix.
      If you agree with adding a new method "cascadeMergeIntoObject()", I think there is no more remained issue.

      Thanks
      - Wonseok


      On 9/27/06, Wonseok Kim <guruwons_at_gmail.com> wrote:
        Hello Gordon,

        Removing the code block will work well, but I thought calling mergeIntoObject() for non-cascade mappings is unnecessary overhead - it does some comparison and invokes reflection call. If an entity has many mappings but there are a few or no cascade mappings the unnecessary cost will increase.

        How about making a new method "cascadeMergeIntoObject()" which I mentioned in the second fix. Because adding a new flag to mergeIntoObject() something like below does not look good. It looks like making the method ...dirty. :-) Do you have a good idea?

            public void mergeIntoObject(Object target, boolean isUnInitialized, Object source, MergeManager mergeManager, boolean cascadeOnly) {
                // PERF: Avoid synchronized enumerator as is concurrency bottleneck.
                Vector mappings = getDescriptor().getMappings();
                if(cascadeOnly){
                    for (int index = 0; index < mappings.size(); index++) {
                        DatabaseMapping mapping = (DatabaseMapping)mappings.get(index);
                        if(mapping instanceof ForeignReferenceMapping
                                && ((ForeignReferenceMapping)mapping).shouldMergeCascadeParts(mergeManager)){
                            mapping.mergeIntoObject (target, isUnInitialized, source, mergeManager);
                        }
                    }
                } else {
                    for (int index = 0; index < mappings.size(); index++) {
                        ((DatabaseMapping)mappings.get(index)).mergeIntoObject(target, isUnInitialized, source, mergeManager);
                    }
                }

        Thanks
        -Wonseok



        On 9/27/06, Gordon Yorke <gordon.yorke_at_oracle.com > wrote:
          Hello Wonseok,
              Looks good, I would remove the code block :
                      if(registeredObject == rmiClone){
                          // GF#1139 Cascade merge operations even if it's already registered
                          Vector mappings = descriptor.getMappings();
                          for (int index = 0; index < mappings.size(); index++) {
                              DatabaseMapping mapping = (DatabaseMapping)mappings.get(index);
                              if(mapping instanceof ForeignReferenceMapping
                                      && ((ForeignReferenceMapping)mapping).shouldMergeCascadeParts(this)){
                                  mapping.mergeIntoObject(registeredObject, false, rmiClone, this);
                              }
                          }
          As this is almost identical to what mergeIntoObject does now. I do not think there is a lot of value in limiting the merge to only foreign reference mappings but if you prefer to do that then it would be better to add that behaviour to the ObjectBuilder (perhaps through another flag).
          --Gordon
            -----Original Message-----
            From: Wonseok Kim [mailto:guruwons_at_gmail.com]
            Sent: Tuesday, September 26, 2006 1:51 AM
            To: persistence_at_glassfish.dev.java.net
            Subject: Re: Fix for issue 1139 "cascade merge"


            Hello Gordon,

            Overriding UoW.mergeCloneWithReferences() causes a code duplication, and introducing a new merge policy CLONE_INTO_WORKING_COPY_FORCE_CASCADE makes another complexity to maintain this policy.

            Instead of the approach I think introducing a new force-cascade flag is cleaner solution. Also when I reconsider moving the force-cascade logic into ObjectBuilder.mergeIntoObject(), it seems not a good idea because mergeIntoObject() is called by several different places. So I left the logic in MergeManager.

            Please review below.

            MergeManager:
                /** Force cascade merge even if a clone is already registered */
                // GF#1139 Cascade doesn't work when merging managed entity
                protected boolean forceCascade;
            ...
                protected Object mergeChangesOfCloneIntoWorkingCopy(Object rmiClone) {
                    ClassDescriptor descriptor = getSession().getDescriptor(rmiClone);
                    Object registeredObject = registerObjectForMergeCloneIntoWorkingCopy(rmiClone);

                    if (registeredObject == rmiClone && !shouldForceCascade()) {
                        //need to find better better fix. prevents merging into itself.
                        return rmiClone;
                    }

                    try {
                        ObjectBuilder builder = descriptor.getObjectBuilder();
                        
                        if (registeredObject != rmiClone && descriptor.usesVersionLocking()) {
                            VersionLockingPolicy policy = (VersionLockingPolicy) descriptor.getOptimisticLockingPolicy();
                            if (policy.isStoredInObject()) {
                                Object currentValue = builder.extractValueFromObjectForField(registeredObject, policy.getWriteLockField(), session);
                            
                                if (policy.isNewerVersion(currentValue, rmiClone, session.keyFromObject(rmiClone), session)) {
                                    throw OptimisticLockException.objectChangedSinceLastMerge (rmiClone);
                                }
                            }
                        }
                        
                        // Toggle change tracking during the merge.
                        descriptor.getObjectChangePolicy().dissableEventProcessing(registeredObject);
                        
                        if(registeredObject == rmiClone){
                            // GF#1139 Cascade merge operations even if it's already registered
                            Vector mappings = descriptor.getMappings();
                            for (int index = 0; index < mappings.size(); index++) {
                                DatabaseMapping mapping = (DatabaseMapping)mappings.get(index);
                                if(mapping instanceof ForeignReferenceMapping
                                        && ((ForeignReferenceMapping)mapping).shouldMergeCascadeParts(this)){
                                    mapping.mergeIntoObject(registeredObject, false, rmiClone, this);
                                }
                            }
                        } else {
                            // Merge into the clone from the original, use clone as backup as anything different should be merged.
                            builder.mergeIntoObject(registeredObject, false, rmiClone, this);
                        }
                    } finally {
                        descriptor.getObjectChangePolicy().enableEventProcessing(registeredObject);
                    }

                    return registeredObject;
                }
            ...

            To set forceCascade flag, UnitOfWorkImpl and EntityManagerImpl are modified as follows.

            UnitOfWorkImpl:
                public Object mergeCloneWithReferences(Object rmiClone, int cascadePolicy) {
                    return mergeCloneWithReferences(rmiClone, cascadePolicy, false);
                }
                
            ...
                public Object mergeCloneWithReferences(Object rmiClone, int cascadePolicy, boolean forceCascade) {
                   if (rmiClone == null) {
                        return null;
                    }
                    ClassDescriptor descriptor = getDescriptor(rmiClone);
                    if ((descriptor == null) || descriptor.isAggregateDescriptor() || descriptor.isAggregateCollectionDescriptor()) {
                        if (cascadePolicy == MergeManager.CASCADE_BY_MAPPING){
                            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("not_an_entity", new Object[]{rmiClone}));
                        }
                        return rmiClone;
                    }

                    //CR#2272
                    logDebugMessage(rmiClone, "merge_clone_with_references");

                    ObjectBuilder builder = descriptor.getObjectBuilder ();
                    Object implementation = builder.unwrapObject(rmiClone, this);

                    MergeManager manager = new MergeManager(this);
                    manager.mergeCloneWithReferencesIntoWorkingCopy();
                    manager.setCascadePolicy (cascadePolicy);
                    manager.setForceCascade(forceCascade);
                    Object mergedObject = manager.mergeChanges(implementation, null);
                    if (isSmartMerge()) {
                        return builder.wrapObject(mergedObject, this);
                    } else {
                        return mergedObject;
                    }
                }

            EntityManagerImpl:
                protected Object mergeInternal(Object entity){
                    verifyOpen();
                    if (entity == null){
                        throw new IllegalArgumentException(ExceptionLocalization.buildMessage("not_an_entity", new Object[] {entity}));
                    }
                    //gf830 - merging a removed entity should throw exception
                    if (getActivePersistenceContext(checkForTransaction(!isExtended())).getDeletedObjects().contains(entity)){
                        throw new IllegalArgumentException(ExceptionLocalization.buildMessage("cannot_merge_removed_entity", new Object[]{entity}));
                    }
                    try {
                        return getActivePersistenceContext(checkForTransaction(!isExtended())).mergeCloneWithReferences(entity, MergeManager.CASCADE_BY_MAPPING, true) ;
                    } catch (oracle.toplink.essentials.exceptions.OptimisticLockException ole) {
                        throw new javax.persistence.OptimisticLockException(ole);
                    }
                }


            Thanks
            -Wonseok


            On 9/26/06, Wonseok Kim <guruwons_at_gmail.com > wrote:
              Hello Gordon, thanks for review.

              Currently EntityManagerImpl is using UoW.mergeCloneWithReferences() instead of mergeClone(). So mergeCloneWithReferences() should be overrided as follows. right?

                  public Object mergeCloneWithReferences(Object rmiClone, int cascadePolicy) {
                      if (rmiClone == null) {
                           return null;
                       }
                       ClassDescriptor descriptor = getDescriptor(rmiClone);
                       if ((descriptor == null) || descriptor.isAggregateDescriptor() || descriptor.isAggregateCollectionDescriptor()) {
                           if (cascadePolicy == MergeManager.CASCADE_BY_MAPPING){
                               throw new IllegalArgumentException(ExceptionLocalization.buildMessage("not_an_entity", new Object[]{rmiClone}));
                           }
                           return rmiClone;
                       }

                       //CR#2272
              // logDebugMessage(rmiClone, "merge_clone_with_references");

                       ObjectBuilder builder = descriptor.getObjectBuilder();
                       Object implementation = builder.unwrapObject(rmiClone, this);

                       MergeManager manager = new MergeManager(this);
                       manager.mergeCloneIntoWorkingCopyForceCascade();
                       manager.setCascadePolicy(cascadePolicy);
                       Object mergedObject = manager.mergeChanges(implementation, null);
                       if (isSmartMerge()) {
                           return builder.wrapObject(mergedObject, this);
                       } else {
                           return mergedObject;
                       }
                      
                  }

              I'm curious, Why mergeCloneWithReferences() code is different from mergeClone()?
              It doesn't use SessionProfiler and handleException() as you see.
              Also, to override this private logDebugMessage() should have protected access.

              And the force-cascade logic which was in MergeManager can go to ObjectBuilder for avoding a duplicate like below.

                  public void mergeIntoObject(Object target, boolean isUnInitialized, Object source, MergeManager mergeManager) {
                      // PERF: Avoid synchronized enumerator as is concurrency bottleneck.
                      Vector mappings = getDescriptor().getMappings();
                      for (int index = 0; index < mappings.size(); index++) {
                          DatabaseMapping mapping = (DatabaseMapping)mappings.get(index);
                          if(target != source
                                  || (mergeManager.shouldMergeCloneIntoWorkingCopyForceCascade()
                                  && mapping instanceof ForeignReferenceMapping
                                  && ((ForeignReferenceMapping)mapping).shouldMergeCascadeParts(mergeManager))){
                              // GF#1139 Cascade merge operations even if it's already managed
                              mapping.mergeIntoObject(target, isUnInitialized, source, mergeManager);
                          }
                      }

              ...
                  }
               
              What do you think?

              -Wonseok



              On 9/25/06, Gordon Yorke < gordon.yorke_at_oracle.com> wrote:
                Hello Wonseok,
                    A slight change will have to be made to your code as it is not backward compatible with current TopLink behaviour. Instead of altering the MergeManger to always cascade the merge and duplicating code already found within the ObjectBuilder the RepeatableWriteUnitOfWork (which is specific to EJB3.0) should request the cascaded merge from the MergeManager while the UnitOfWork continues to request the abortive merge.

                The following method should be added to the RepeatableWriteUnitOfWork :
                    /**
                     * INTERNAL:
                     * Merge the attributes of the clone into the unit of work copy.
                     */
                    public Object mergeClone(Object rmiClone, int cascadeDepth) {
                        if (rmiClone == null) {
                            return null;
                        }

                        //CR#2272
                        logDebugMessage(rmiClone, "merge_clone");

                        startOperationProfile(SessionProfiler.Merge);
                        ObjectBuilder builder = getDescriptor(rmiClone).getObjectBuilder();
                        Object implementation = builder.unwrapObject(rmiClone, this);

                        MergeManager manager = new MergeManager(this);
                        manager.mergeCloneIntoWorkingCopyForceCascade();
                        manager.setCascadePolicy(cascadeDepth);

                        Object merged = null;
                        try {
                            merged = manager.mergeChanges(implementation, null);
                        } catch (RuntimeException exception) {
                            merged = handleException(exception);
                        }
                        endOperationProfile(SessionProfiler.Merge);

                        return merged;
                    }

                Then the MergeManager should have the following method and corresponding property added:
                    /**
                     * This can be used by the user for merging clones from RMI into the unit of work.
                     */
                    public void mergeCloneIntoWorkingCopyForceCascade() {
                        setMergePolicy(CLONE_INTO_WORKING_COPY_FORCE_CASCADE);
                    }

                Then update the MergeManager to check for this setting in two places:
                mergeClone():
                        } else if (shouldMergeCloneIntoWorkingCopy() || shouldMergeCloneWithReferencesIntoWorkingCopy() || shouldMergeCloneIntoWorkingCopyForceCascade()) {
                            mergedObject = mergeChangesOfCloneIntoWorkingCopy(object);

                and mergeChangesOfCloneIntoWorkingCopy() :
                         if (registeredObject== rmiClone && ! shouldMergeCloneIntoWorkingCopyForceCascade() ){
                             return rmiClone;
                         }


                These changes preserve existing TopLink behaviour while updating the functionality for EJB 3.0 support and the code continues to use the current TopLink patterns for method call sequence.
                --Gordon