Bug 474956 RepeatableUnitOfWork linked by Embeddable in shared cache in specific scenario
(2.3.x branch checkin)
- Resolves an issue in AggregateObjectMapping buildAggregateFromRow() by passing in the CacheKey (within buildCloneFromRow()) and checking if the aggregate is referenced within the target object and the CacheKey is null before instantiating a new aggregate instance.
- Added AdvancedJPAJunitTest test: testInvalidateAndRefreshEmbeddableParent within the JPA advanced suite, with new, specialized Entities: HockeyPuck, HockeyRink and HockeySponsor (and associated table creation to AdvancedTableCreator)

Signed-off-by: David Minsky <david.minsky@oracle.com>
diff --git a/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/mappings/AggregateObjectMapping.java b/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/mappings/AggregateObjectMapping.java
index ab814b9..e1655f8 100644
--- a/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/mappings/AggregateObjectMapping.java
+++ b/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/mappings/AggregateObjectMapping.java
@@ -1,5 +1,5 @@
 /*******************************************************************************

- * Copyright (c) 1998, 2013 Oracle and/or its affiliates. All rights reserved.

+ * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved.

  * This program and the accompanying materials are made available under the 

  * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 

  * which accompanies this distribution. 

@@ -317,7 +317,10 @@
             }

         }

 

-        if (aggregate == null) {

+        // Build a new aggregate if the target object does not reference an existing aggregate.
+        // EL Bug 474956 - build a new aggregate if the the target object references an existing aggregate, and 
+        // the passed cacheKey is null from the invalidation of the target object in the IdentityMap.
+        if (aggregate == null || (aggregate != null && cacheKey == null)) {
             aggregate = descriptor.getObjectBuilder().buildNewInstance();

             refreshing = false;

         }

@@ -668,17 +671,15 @@
      * be able to populate working copies directly from the row.

      */

     public void buildCloneFromRow(AbstractRecord databaseRow, JoinedAttributeManager joinManager, Object clone, CacheKey sharedCacheKey, ObjectBuildingQuery sourceQuery, UnitOfWorkImpl unitOfWork, AbstractSession executionSession) {

-        // This method is a combination of buildggregateFromRow and

-        // buildClonePart on the super class.

-        // none of buildClonePart used, as not an orignal new object, nor

-        // do we worry about creating heavy clones for aggregate objects.

-        Object clonedAttributeValue = buildAggregateFromRow(databaseRow, clone, null, joinManager, sourceQuery, false, executionSession, true);

-        ClassDescriptor descriptor = getReferenceDescriptor(clonedAttributeValue, unitOfWork);

+        // This method is a combination of buildggregateFromRow and buildClonePart on the super class.
+        // None of buildClonePart used, as not an orignal new object, nor do we worry about creating heavy clones for aggregate objects.
+        // Ensure that the shared CacheKey is passed, as this will be set to null for a refresh of an invalid object.
+        Object clonedAttributeValue = buildAggregateFromRow(databaseRow, clone, sharedCacheKey, joinManager, sourceQuery, false, executionSession, true);
         if (clonedAttributeValue != null) {

+            ClassDescriptor descriptor = getReferenceDescriptor(clonedAttributeValue, unitOfWork);
             descriptor.getObjectChangePolicy().setAggregateChangeListener(clone, clonedAttributeValue, unitOfWork, descriptor, getAttributeName());

         }

         setAttributeValueInObject(clone, clonedAttributeValue);

-        return;

     }

 

     /**

diff --git a/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/models/jpa/advanced/AdvancedTableCreator.java b/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/models/jpa/advanced/AdvancedTableCreator.java
index 9656370..029f410 100644
--- a/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/models/jpa/advanced/AdvancedTableCreator.java
+++ b/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/models/jpa/advanced/AdvancedTableCreator.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 1998, 2012 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved.
  * This program and the accompanying materials are made available under the 
  * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 
  * which accompanies this distribution. 
@@ -86,6 +86,8 @@
         addTableDefinition(buildRABBITFOOTTable());
         addTableDefinition(buildCMP3_ROOMTable());
         addTableDefinition(buildCMP3_DOORTable());
+        addTableDefinition(buildHOCKEY_RINKTable());
+        addTableDefinition(buildHOCKEY_PUCKTable());
     }
     
     public TableDefinition buildADDRESSTable() {
@@ -2492,4 +2494,86 @@
  
         return table;
     }
+    
+    public TableDefinition buildHOCKEY_RINKTable() {
+        TableDefinition table = new TableDefinition();
+        table.setName("CMP3_HOCKEY_RINK");
+        
+        FieldDefinition fieldID = new FieldDefinition();
+        fieldID.setName("ID");
+        fieldID.setTypeName("NUMBER");
+        fieldID.setSize(18);
+        fieldID.setSubSize(0);
+        fieldID.setIsPrimaryKey(true);
+        fieldID.setIsIdentity(false);
+        fieldID.setUnique(false);
+        fieldID.setShouldAllowNull(false);
+        table.addField(fieldID);
+        
+        FieldDefinition fieldHOCKEY_PUCK_ID = new FieldDefinition();
+        fieldHOCKEY_PUCK_ID.setName("HOCKEY_PUCK_ID");
+        fieldHOCKEY_PUCK_ID.setTypeName("NUMBER");
+        fieldHOCKEY_PUCK_ID.setSize(18);
+        fieldHOCKEY_PUCK_ID.setSubSize(0);
+        fieldHOCKEY_PUCK_ID.setIsPrimaryKey(false);
+        fieldHOCKEY_PUCK_ID.setIsIdentity(false);
+        fieldHOCKEY_PUCK_ID.setUnique(false);
+        fieldHOCKEY_PUCK_ID.setShouldAllowNull(true);
+        fieldHOCKEY_PUCK_ID.setForeignKeyFieldName("CMP3_HOCKEY_PUCK.ID");
+        table.addField(fieldHOCKEY_PUCK_ID);
+        
+        return table;
+    }
+    
+    public TableDefinition buildHOCKEY_PUCKTable() {
+        TableDefinition table = new TableDefinition();
+        table.setName("CMP3_HOCKEY_PUCK");
+        
+        FieldDefinition fieldID = new FieldDefinition();
+        fieldID.setName("ID");
+        fieldID.setTypeName("NUMBER");
+        fieldID.setSize(18);
+        fieldID.setSubSize(0);
+        fieldID.setIsPrimaryKey(true);
+        fieldID.setIsIdentity(false);
+        fieldID.setUnique(false);
+        fieldID.setShouldAllowNull(false);
+        table.addField(fieldID);
+        
+        FieldDefinition fieldNAME = new FieldDefinition();
+        fieldNAME.setName("NAME");
+        fieldNAME.setTypeName("VARCHAR2");
+        fieldNAME.setSize(36);
+        fieldNAME.setSubSize(0);
+        fieldNAME.setIsPrimaryKey(false);
+        fieldNAME.setIsIdentity(false);
+        fieldNAME.setUnique(false);
+        fieldNAME.setShouldAllowNull(true);
+        table.addField(fieldNAME);
+        
+        FieldDefinition fieldSPONSOR_NAME = new FieldDefinition();
+        fieldSPONSOR_NAME.setName("SPONSOR_NAME");
+        fieldSPONSOR_NAME.setTypeName("VARCHAR2");
+        fieldSPONSOR_NAME.setSize(36);
+        fieldSPONSOR_NAME.setSubSize(0);
+        fieldSPONSOR_NAME.setIsPrimaryKey(false);
+        fieldSPONSOR_NAME.setIsIdentity(false);
+        fieldSPONSOR_NAME.setUnique(false);
+        fieldSPONSOR_NAME.setShouldAllowNull(true);
+        table.addField(fieldSPONSOR_NAME);
+        
+        FieldDefinition fieldSPONSOR_VALUE = new FieldDefinition();
+        fieldSPONSOR_VALUE.setName("SPONSOR_VALUE");
+        fieldSPONSOR_VALUE.setTypeName("NUMBER");
+        fieldSPONSOR_VALUE.setSize(18);
+        fieldSPONSOR_VALUE.setSubSize(0);
+        fieldSPONSOR_VALUE.setIsPrimaryKey(false);
+        fieldSPONSOR_VALUE.setIsIdentity(false);
+        fieldSPONSOR_VALUE.setUnique(false);
+        fieldSPONSOR_VALUE.setShouldAllowNull(true);
+        table.addField(fieldSPONSOR_VALUE);
+        
+        return table;
+    }
+    
 }
diff --git a/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/models/jpa/advanced/HockeyPuck.java b/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/models/jpa/advanced/HockeyPuck.java
new file mode 100644
index 0000000..c6cab11
--- /dev/null
+++ b/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/models/jpa/advanced/HockeyPuck.java
@@ -0,0 +1,69 @@
+/*******************************************************************************
+ * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
+ * which accompanies this distribution.
+ * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * Contributors:
+ *     dminsky - initial API and implementation
+ *     
+ ******************************************************************************/
+package org.eclipse.persistence.testing.models.jpa.advanced;
+
+import javax.persistence.Embedded;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+@Entity
+@Table(name="CMP3_HOCKEY_PUCK")
+public class HockeyPuck {
+
+    @Id
+    protected int id; 
+    protected String name;
+    @Embedded
+    protected HockeySponsor sponsor;
+    
+    public HockeyPuck() {
+        super();
+        // the Embeddable must be instantiated with real values
+        sponsor = new HockeySponsor();
+        sponsor.setName("none");
+        sponsor.setSponsorshipValue(1);
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+    
+    public String toString() {
+        return getClass().getSimpleName() + " id:[" + id + "] name:[" + name + "] hashcode:[" + System.identityHashCode(this) + "] embedded: " + sponsor;
+    }
+
+    public HockeySponsor getSponsor() {
+        return sponsor;
+    }
+
+    public void setSponsor(HockeySponsor sponsor) {
+        this.sponsor = sponsor;
+    }
+    
+}
diff --git a/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/models/jpa/advanced/HockeyRink.java b/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/models/jpa/advanced/HockeyRink.java
new file mode 100644
index 0000000..9498e69
--- /dev/null
+++ b/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/models/jpa/advanced/HockeyRink.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
+ * which accompanies this distribution.
+ * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * Contributors:
+ *     dminsky - initial API and implementation
+ *     
+ ******************************************************************************/
+package org.eclipse.persistence.testing.models.jpa.advanced;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.OneToOne;
+import javax.persistence.Table;
+
+@Entity
+@Table(name="CMP3_HOCKEY_RINK")
+public class HockeyRink {
+    
+    @Id
+    protected int id;
+    
+    @OneToOne
+    @JoinColumn(name="HOCKEY_PUCK_ID")
+    protected HockeyPuck puck;
+    
+    public HockeyRink() {
+        super();
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public HockeyPuck getPuck() {
+        return puck;
+    }
+
+    public void setPuck(HockeyPuck puck) {
+        this.puck = puck;
+    }
+
+    public String toString() {
+        return getClass().getSimpleName() + " id:[" + id + "] hashcode:[" + System.identityHashCode(this) + "]";
+    }
+    
+}
diff --git a/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/models/jpa/advanced/HockeySponsor.java b/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/models/jpa/advanced/HockeySponsor.java
new file mode 100644
index 0000000..899ebaa
--- /dev/null
+++ b/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/models/jpa/advanced/HockeySponsor.java
@@ -0,0 +1,68 @@
+/*******************************************************************************
+ * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
+ * which accompanies this distribution.
+ * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * Contributors:
+ *     dminsky - initial API and implementation
+ *     
+ ******************************************************************************/
+package org.eclipse.persistence.testing.models.jpa.advanced;
+
+import java.beans.PropertyChangeListener;
+
+import javax.persistence.Column;
+import javax.persistence.Embeddable;
+
+import org.eclipse.persistence.descriptors.changetracking.ChangeTracker;
+import org.eclipse.persistence.internal.descriptors.changetracking.AttributeChangeListener;
+import org.eclipse.persistence.sessions.UnitOfWork;
+
+@Embeddable
+public class HockeySponsor {
+    
+    @Column(name="SPONSOR_NAME")
+    protected String name;
+    @Column(name="SPONSOR_VALUE")
+    protected int sponsorshipValue;
+    
+    public HockeySponsor() {
+        super();
+    }
+    
+    public String toString() {
+        StringBuffer buffer = new StringBuffer();
+        buffer.append(getClass().getSimpleName() + " hashcode:[" + System.identityHashCode(this) + "]");
+        if (this instanceof ChangeTracker) {
+            PropertyChangeListener listener = ((ChangeTracker)this)._persistence_getPropertyChangeListener();
+            buffer.append(" listener:[" + listener + "]");
+            if (listener != null && listener instanceof AttributeChangeListener) {
+                UnitOfWork uow = ((AttributeChangeListener)listener).getUnitOfWork();
+                buffer.append(" uow:[" + uow + "] uow hashcode: " + System.identityHashCode(uow));
+                buffer.append("]");
+            }
+        }
+        return buffer.toString();
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public int getSponsorshipValue() {
+        return sponsorshipValue;
+    }
+
+    public void setSponsorshipValue(int sponsorshipValue) {
+        this.sponsorshipValue = sponsorshipValue;
+    }
+
+}
diff --git a/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/tests/jpa/advanced/AdvancedJPAJunitTest.java b/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/tests/jpa/advanced/AdvancedJPAJunitTest.java
index be5c465..c05006a 100644
--- a/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/tests/jpa/advanced/AdvancedJPAJunitTest.java
+++ b/jpa/eclipselink.jpa.test/src/org/eclipse/persistence/testing/tests/jpa/advanced/AdvancedJPAJunitTest.java
@@ -38,6 +38,7 @@
 import java.util.Map;

 import java.util.Set;

 import java.util.Vector;

+import java.beans.PropertyChangeListener;

 

 import javax.persistence.CacheStoreMode;

 import javax.persistence.EntityManager;

@@ -59,6 +60,8 @@
 import org.eclipse.persistence.descriptors.ClassDescriptor;

 import org.eclipse.persistence.descriptors.DescriptorEvent;

 import org.eclipse.persistence.descriptors.DescriptorEventAdapter;

+import org.eclipse.persistence.descriptors.changetracking.ChangeTracker;

+import org.eclipse.persistence.internal.descriptors.changetracking.AttributeChangeListener;

 import org.eclipse.persistence.descriptors.invalidation.CacheInvalidationPolicy;

 import org.eclipse.persistence.descriptors.invalidation.TimeToLiveCacheInvalidationPolicy;

 import org.eclipse.persistence.internal.helper.ClassConstants;

@@ -112,6 +115,9 @@
 import org.eclipse.persistence.testing.models.jpa.advanced.ViolationCode;

 import org.eclipse.persistence.testing.models.jpa.advanced.Violation.ViolationID;

 import org.eclipse.persistence.testing.models.jpa.advanced.ViolationCode.ViolationCodeId;

+import org.eclipse.persistence.testing.models.jpa.advanced.HockeyPuck;

+import org.eclipse.persistence.testing.models.jpa.advanced.HockeyRink;

+import org.eclipse.persistence.testing.models.jpa.advanced.HockeySponsor;

 import org.eclipse.persistence.testing.models.jpa.advanced.additionalcriteria.Bolt;

 import org.eclipse.persistence.testing.models.jpa.advanced.additionalcriteria.Eater;

 import org.eclipse.persistence.testing.models.jpa.advanced.additionalcriteria.Nut;

@@ -230,6 +236,7 @@
             

             // Run this test only when the JPA 2.0 specification is enabled on the server, or we are in SE mode with JPA 2.0 capability

             suite.addTest(new AdvancedJPAJunitTest("testMetamodelMinimalSanityTest"));

+            suite.addTest(new AdvancedJPAJunitTest("testInvalidateAndRefreshEmbeddableParent"));

         }

         

         return suite;

@@ -2628,6 +2635,104 @@
         }

     }

     

+    /**

+     * Bug 474956 RepeatableUnitOfWork linked by Embeddable in shared cache in specific scenario

+     * Test the invalidation and refresh of a parent object with an Embeddable instantiated

+     * with non-null values. After associating the parent with another object, the Embeddable

+     * should not reference a UnitOfWork (via a change listener) within the shared cache.

+     */

+    public void testInvalidateAndRefreshEmbeddableParent() {

+        // test depends on weaving

+        if (!isWeavingEnabled()) {

+            warning("Test depends on weaving and change tracking");

+            return;

+        }

+        

+        HockeyPuck puck = null;

+        HockeyRink rink = null;

+        

+        // setup

+        clearCache();

+        

+        EntityManager em = createEntityManager();

+        try {

+            beginTransaction(em);

+            

+            puck = new HockeyPuck();

+            puck.setId(1);

+            puck.setName("MrWraparound");

+            puck.getSponsor().setName("ACME Cloud Computing Company, Inc.");

+            puck.getSponsor().setSponsorshipValue(1000000);

+            em.persist(puck);

+            

+            commitTransaction(em);

+        } finally {

+            closeEntityManager(em);

+        }

+        

+        // test

+        em = createEntityManager();

+        try {

+            // 1. invalidate existing Entity in the cache

+            JpaHelper.getDatabaseSession(getEntityManagerFactory()).getIdentityMapAccessor().invalidateObject(puck.getId(), HockeyPuck.class);

+            assertFalse("Existing cached HockeyPuck should not be valid", 

+                JpaHelper.getDatabaseSession(getEntityManagerFactory()).getIdentityMapAccessor().isValid(puck.getId(), HockeyPuck.class));

+            

+            beginTransaction(em);

+            

+            // 2. create new Entity and persist

+            rink = new HockeyRink();

+            rink.setId(1);

+            em.persist(rink);

+            

+            // 3. load existing Entity

+            puck = em.createQuery("select object(p) from HockeyPuck p where p.id = " + puck.getId(), HockeyPuck.class).getSingleResult();

+            assertNotNull("HockeyPuck loaded should not be null", puck);

+            

+            // 4. associate loaded existing Entity with persisted new Entity

+            rink.setPuck(puck);

+            

+            commitTransaction(em);

+        } finally {

+            closeEntityManager(em);

+        }

+        

+        // verify, go directly to the shared cache for the parent Entity

+        try {

+            HockeyPuck cachedPuck = (HockeyPuck)JpaHelper.getDatabaseSession(getEntityManagerFactory()).getIdentityMapAccessor().getFromIdentityMap(puck.getId(), HockeyPuck.class);

+            HockeySponsor sponsor = cachedPuck.getSponsor();

+            if (sponsor instanceof ChangeTracker) {

+                PropertyChangeListener listener = ((ChangeTracker)sponsor)._persistence_getPropertyChangeListener();

+                // listener can be null

+                if (listener != null && listener instanceof AttributeChangeListener) {

+                    assertNull("UnitOfWork referenced in Embeddable referenced by an object in the shared cache", ((AttributeChangeListener)listener).getUnitOfWork());

+                }

+            } else {

+                fail("Config error: HockeyPuck/HockeySponsor is not change tracked");

+            }

+        } finally {

+            // reset

+            em = createEntityManager();

+            beginTransaction(em);

+            

+            if (puck != null) {

+                puck = em.find(HockeyPuck.class, puck.getId());

+                if (puck != null) {

+                    em.remove(puck);

+                }

+            }

+            if (rink != null) {

+                rink = em.find(HockeyRink.class, rink.getId());

+                if (rink != null) {

+                    em.remove(rink);

+                }

+            }

+            

+            commitTransaction(em);

+            closeEntityManager(em);

+        }

+    }

+    

     protected int getVersion(EntityManager em, Dealer dealer) {

         Vector pk = new Vector(1);

         pk.add(dealer.getId());