diff --git a/foundation/eclipselink.core.test/src/test/java/org/eclipse/persistence/testing/tests/junit/policies/NestedDetailRecord.java b/foundation/eclipselink.core.test/src/test/java/org/eclipse/persistence/testing/tests/junit/policies/NestedDetailRecord.java new file mode 100644 index 00000000000..23831b9ada9 --- /dev/null +++ b/foundation/eclipselink.core.test/src/test/java/org/eclipse/persistence/testing/tests/junit/policies/NestedDetailRecord.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 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 v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// Oracle - initial API and implementation +package org.eclipse.persistence.testing.tests.junit.policies; + +import java.time.Instant; +import java.util.Objects; + +public record NestedDetailRecord(float floatAttribute, Instant instantAttribute) { + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NestedDetailRecord that = (NestedDetailRecord) o; + return Float.compare(floatAttribute, that.floatAttribute) == 0 && Objects.equals(instantAttribute, that.instantAttribute); + } + + @Override + public int hashCode() { + return Objects.hash(floatAttribute, instantAttribute); + } +} diff --git a/foundation/eclipselink.core.test/src/test/java/org/eclipse/persistence/testing/tests/junit/policies/NestedMasterRecord.java b/foundation/eclipselink.core.test/src/test/java/org/eclipse/persistence/testing/tests/junit/policies/NestedMasterRecord.java new file mode 100644 index 00000000000..c11003e8aed --- /dev/null +++ b/foundation/eclipselink.core.test/src/test/java/org/eclipse/persistence/testing/tests/junit/policies/NestedMasterRecord.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 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 v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// Oracle - initial API and implementation +package org.eclipse.persistence.testing.tests.junit.policies; + +import java.util.Objects; + +public record NestedMasterRecord(int intAttribute, String stringAttribute, NestedDetailRecord nestedDetailRecord) { + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NestedMasterRecord that = (NestedMasterRecord) o; + return intAttribute == that.intAttribute && Objects.equals(stringAttribute, that.stringAttribute) && Objects.equals(nestedDetailRecord, that.nestedDetailRecord); + } + + @Override + public int hashCode() { + return Objects.hash(intAttribute, stringAttribute, nestedDetailRecord); + } +} diff --git a/foundation/eclipselink.core.test/src/test/java/org/eclipse/persistence/testing/tests/junit/policies/RecordCopyPolicyTest.java b/foundation/eclipselink.core.test/src/test/java/org/eclipse/persistence/testing/tests/junit/policies/RecordCopyPolicyTest.java new file mode 100644 index 00000000000..2157d778a2d --- /dev/null +++ b/foundation/eclipselink.core.test/src/test/java/org/eclipse/persistence/testing/tests/junit/policies/RecordCopyPolicyTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 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 v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// Oracle - initial API and implementation from Oracle TopLink +package org.eclipse.persistence.testing.tests.junit.policies; + +import org.eclipse.persistence.descriptors.copying.RecordCopyPolicy; +import org.junit.Test; + +import java.time.Instant; + +import static org.junit.Assert.assertEquals; + +public class RecordCopyPolicyTest { + + private final int INT_ATTR = 1; + private final String STRING_ATTR = "abcde"; + private final float FLOAT_ATTR = 1.1F; + private final Instant INSTANT_ATTR = Instant.parse("2024-05-27T10:15:30.00Z"); + + @Test + public void cloneNestedRecordTest() { + NestedDetailRecord nestedDetailRecord = new NestedDetailRecord(FLOAT_ATTR, INSTANT_ATTR); + NestedMasterRecord source = new NestedMasterRecord(INT_ATTR, STRING_ATTR, nestedDetailRecord); + RecordCopyPolicy recordCopyPolicy = new RecordCopyPolicy(); + NestedMasterRecord clone = (NestedMasterRecord)recordCopyPolicy.buildClone(source, null); + assertEquals(source, clone); + } + + @Test + public void cloneNestedRecordWithNullTest() { + NestedDetailRecord nestedDetailRecord = new NestedDetailRecord(FLOAT_ATTR, INSTANT_ATTR); + NestedMasterRecord source = new NestedMasterRecord(INT_ATTR, null, nestedDetailRecord); + RecordCopyPolicy recordCopyPolicy = new RecordCopyPolicy(); + NestedMasterRecord clone = (NestedMasterRecord)recordCopyPolicy.buildClone(source, null); + assertEquals(source, clone); + } +} diff --git a/foundation/eclipselink.core.test/src/test/java/org/eclipse/persistence/testing/tests/junit/policies/RecordInstantiationPolicyTest.java b/foundation/eclipselink.core.test/src/test/java/org/eclipse/persistence/testing/tests/junit/policies/RecordInstantiationPolicyTest.java new file mode 100644 index 00000000000..e66efffb44d --- /dev/null +++ b/foundation/eclipselink.core.test/src/test/java/org/eclipse/persistence/testing/tests/junit/policies/RecordInstantiationPolicyTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 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 v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// Oracle - initial API and implementation from Oracle TopLink +package org.eclipse.persistence.testing.tests.junit.policies; + +import org.eclipse.persistence.internal.descriptors.RecordInstantiationPolicy; +import org.junit.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class RecordInstantiationPolicyTest { + + private final float FLOAT_ATTR = 1.1F; + private final Instant INSTANT_ATTR = Instant.parse("2024-05-27T10:15:30.00Z"); + + @Test + public void newRecordTest() { + Class clazz = NestedDetailRecord.class; + //Values order is important. It must be same as order of Record attributes. + List values = new ArrayList<>(); + values.add(FLOAT_ATTR); + values.add(INSTANT_ATTR); + RecordInstantiationPolicy recordInstantiationPolicy = new RecordInstantiationPolicy<>(clazz); + recordInstantiationPolicy.setValues(values); + NestedDetailRecord nestedDetailRecord = (NestedDetailRecord) recordInstantiationPolicy.buildNewInstance(); + assertEquals(FLOAT_ATTR, nestedDetailRecord.floatAttribute(), 0); + assertEquals(INSTANT_ATTR, nestedDetailRecord.instantAttribute()); + } + + @Test + public void newRecordWithNullTest() { + Class clazz = NestedDetailRecord.class; + //Values order is important. It must be same as order of Record attributes. + List values = new ArrayList<>(); + values.add(FLOAT_ATTR); + values.add(null); + RecordInstantiationPolicy recordInstantiationPolicy = new RecordInstantiationPolicy<>(clazz); + recordInstantiationPolicy.setValues(values); + NestedDetailRecord nestedDetailRecord = (NestedDetailRecord) recordInstantiationPolicy.buildNewInstance(); + assertEquals(FLOAT_ATTR, nestedDetailRecord.floatAttribute(), 0); + assertNull(nestedDetailRecord.instantAttribute()); + } +} diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/ClassDescriptor.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/ClassDescriptor.java index e751a767c15..ffa86c6cb12 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/ClassDescriptor.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/ClassDescriptor.java @@ -54,6 +54,7 @@ import org.eclipse.persistence.descriptors.copying.CopyPolicy; import org.eclipse.persistence.descriptors.copying.InstantiationCopyPolicy; import org.eclipse.persistence.descriptors.copying.PersistenceEntityCopyPolicy; +import org.eclipse.persistence.descriptors.copying.RecordCopyPolicy; import org.eclipse.persistence.descriptors.invalidation.CacheInvalidationPolicy; import org.eclipse.persistence.descriptors.invalidation.NoExpiryCacheInvalidationPolicy; import org.eclipse.persistence.descriptors.partitioning.PartitioningPolicy; @@ -73,6 +74,7 @@ import org.eclipse.persistence.internal.descriptors.PersistenceObject; import org.eclipse.persistence.internal.descriptors.PersistenceObjectAttributeAccessor; import org.eclipse.persistence.internal.descriptors.PersistenceObjectInstantiationPolicy; +import org.eclipse.persistence.internal.descriptors.RecordInstantiationPolicy; import org.eclipse.persistence.internal.descriptors.SerializedObjectPolicyWrapper; import org.eclipse.persistence.internal.descriptors.VirtualAttributeMethodInfo; import org.eclipse.persistence.internal.dynamic.DynamicEntityImpl; @@ -2123,7 +2125,11 @@ public List> getConstraintDependencies() { public CopyPolicy getCopyPolicy() { // Lazy initialize for XML deployment. if (copyPolicy == null) { - setCopyPolicy(new InstantiationCopyPolicy()); + if (javaClass != null && javaClass.isRecord()) { + setCopyPolicy(new RecordCopyPolicy()); + } else { + setCopyPolicy(new InstantiationCopyPolicy()); + } } return copyPolicy; } @@ -2308,7 +2314,11 @@ public InheritancePolicy getInheritancePolicyOrNull() { public InstantiationPolicy getInstantiationPolicy() { // Lazy initialize for XML deployment. if (instantiationPolicy == null) { - setInstantiationPolicy(new InstantiationPolicy()); + if (javaClass != null && javaClass.isRecord()) { + setInstantiationPolicy(new RecordInstantiationPolicy(javaClass)); + } else { + setInstantiationPolicy(new InstantiationPolicy()); + } } return instantiationPolicy; } @@ -3994,12 +4004,20 @@ public void preInitialize(AbstractSession session) throws DescriptorException { } if (!isMethodAccess) { if (this.copyPolicy == null) { - setCopyPolicy(new PersistenceEntityCopyPolicy()); + if (javaClass != null && javaClass.isRecord()) { + setCopyPolicy(new RecordCopyPolicy()); + } else { + setCopyPolicy(new PersistenceEntityCopyPolicy()); + } } if (!isAbstract()) { try { if (this.instantiationPolicy == null) { - setInstantiationPolicy(new PersistenceObjectInstantiationPolicy((PersistenceObject)getJavaClass().getConstructor().newInstance())); + if (javaClass != null && javaClass.isRecord()) { + setInstantiationPolicy(new RecordInstantiationPolicy(javaClass)); + } else { + setInstantiationPolicy(new PersistenceObjectInstantiationPolicy((PersistenceObject)getJavaClass().getConstructor().newInstance())); + } } } catch (Exception ignore) { } } diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/copying/AbstractCopyPolicy.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/copying/AbstractCopyPolicy.java index 5fb241e1ed9..a30dfbbd505 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/copying/AbstractCopyPolicy.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/copying/AbstractCopyPolicy.java @@ -91,4 +91,8 @@ public void setDescriptor(ClassDescriptor descriptor) { this.descriptor = descriptor; } + @Override + public String toString() { + return getClass().getSimpleName() + "()"; + } } diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/copying/InstantiationCopyPolicy.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/copying/InstantiationCopyPolicy.java index 9037dc46de6..19455064959 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/copying/InstantiationCopyPolicy.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/copying/InstantiationCopyPolicy.java @@ -37,9 +37,4 @@ public Object buildClone(Object domainObject, Session session) throws Descriptor public boolean buildsNewInstance() { return true; } - - @Override - public String toString() { - return getClass().getSimpleName() + "()"; - } } diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/copying/PersistenceEntityCopyPolicy.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/copying/PersistenceEntityCopyPolicy.java index 7df247e9fa1..3d2db51b01a 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/copying/PersistenceEntityCopyPolicy.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/copying/PersistenceEntityCopyPolicy.java @@ -42,9 +42,4 @@ public Object buildClone(Object object, Session session) throws DescriptorExcept public boolean buildsNewInstance() { return false; } - - @Override - public String toString() { - return getClass().getSimpleName() + "()"; - } } diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/copying/RecordCopyPolicy.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/copying/RecordCopyPolicy.java new file mode 100644 index 00000000000..0eb55c075af --- /dev/null +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/copying/RecordCopyPolicy.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2024 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 v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// Oracle - initial API and implementation from Oracle TopLink +package org.eclipse.persistence.descriptors.copying; + +import org.eclipse.persistence.exceptions.DescriptorException; +import org.eclipse.persistence.sessions.Session; + +import java.lang.reflect.Constructor; +import java.lang.reflect.RecordComponent; +import java.util.ArrayList; + +/** + *

Purpose: This is the copy policy for {@code java.lang.Record}. + *

+ * It creates a copy by creating a new instance. As {@code java.lang.Record} is immutable + * all cloned attributes must be collected and passed to the constructor. + */ +public class RecordCopyPolicy extends AbstractCopyPolicy { + public RecordCopyPolicy() { + super(); + } + + @Override + public Object buildClone(Object domainObject, Session session) throws DescriptorException { + return cloneRecord((Record) domainObject); + } + + @Override + public boolean buildsNewInstance() { + return true; + } + + //There is limited functionality some complex nested objects shouldn't be cloned. + private R cloneRecord(R template) { + try { + ArrayList> types = new ArrayList<>(); + ArrayList values = new ArrayList<>(); + for (RecordComponent component : template.getClass().getRecordComponents()) { + types.add(component.getType()); + Object value = component.getAccessor().invoke(template); + if (value instanceof Record) { + value = cloneRecord((Record) value); + } + values.add(value); + } + Constructor canonical = template.getClass().getDeclaredConstructor(types.toArray(Class[]::new)); + @SuppressWarnings("unchecked") + var result = (R) canonical.newInstance(values.toArray(Object[]::new)); + return result; + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Record clone failed: " + e, e); + } + } +} diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/core/descriptors/CoreObjectBuilder.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/core/descriptors/CoreObjectBuilder.java index b820a0672de..67b2ea19932 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/core/descriptors/CoreObjectBuilder.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/core/descriptors/CoreObjectBuilder.java @@ -19,6 +19,8 @@ import org.eclipse.persistence.internal.core.sessions.CoreAbstractRecord; import org.eclipse.persistence.internal.core.sessions.CoreAbstractSession; +import java.util.List; + public abstract class CoreObjectBuilder< ABSTRACT_RECORD extends CoreAbstractRecord, ABSTRACT_SESSION extends CoreAbstractSession, diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/descriptors/ObjectBuilder.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/descriptors/ObjectBuilder.java index 3af7e8c9acf..74e759bbec3 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/descriptors/ObjectBuilder.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/descriptors/ObjectBuilder.java @@ -90,6 +90,7 @@ import org.eclipse.persistence.mappings.ContainerMapping; import org.eclipse.persistence.mappings.DatabaseMapping; import org.eclipse.persistence.mappings.DatabaseMapping.WriteType; +import org.eclipse.persistence.mappings.DirectToFieldMapping; import org.eclipse.persistence.mappings.ForeignReferenceMapping; import org.eclipse.persistence.mappings.ObjectReferenceMapping; import org.eclipse.persistence.mappings.foundation.AbstractColumnMapping; @@ -115,6 +116,7 @@ import org.eclipse.persistence.sessions.remote.DistributedSession; import java.io.Serializable; +import java.lang.reflect.RecordComponent; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; @@ -124,11 +126,14 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Vector; import java.util.concurrent.Semaphore; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; /** *

Purpose: Object builder is one of the behavior class attached to descriptor. @@ -188,6 +193,7 @@ public class ObjectBuilder extends CoreObjectBuilder clazz, List mappings, AbstractRecord databaseRow, AbstractSession session) { + class TypeValue { + public TypeValue(Class type) { + this.type = type; + } + Class type; + Object value; + } + Map typeValueMap = new LinkedHashMap<>(); + ReadObjectQuery query = new ReadObjectQuery(); + query.setSession(session); + for (RecordComponent component : clazz.getRecordComponents()) { + typeValueMap.put(component.getName(), new TypeValue(component.getType())); + } + for (DatabaseMapping mapping: mappings) { + Object value = null; + if (mapping instanceof DirectToFieldMapping) { + value = mapping.valueFromRow(databaseRow, null, query, true); + } if (mapping instanceof AggregateObjectMapping aggregateObjectMapping) { + ClassDescriptor descriptor = session.getClassDescriptor(aggregateObjectMapping.getReferenceClass()); + //Handle nested records + if (aggregateObjectMapping.getReferenceClass().isRecord()) { + value = descriptor.getObjectBuilder().buildNewRecordInstance((Class) aggregateObjectMapping.getReferenceClass(), descriptor.getMappings(), databaseRow, session); + } else { + value = descriptor.getObjectBuilder().buildNewInstance(); + } + } + TypeValue typeValue = typeValueMap.get(mapping.getAttributeName()); + typeValue.value = value; + } + RecordInstantiationPolicy recordInstantiationPolicy = ((RecordInstantiationPolicy)this.descriptor.getInstantiationPolicy()); + instanceLock.lock(); + try { + List values = new ArrayList<>(); + for (TypeValue typeValue: typeValueMap.values()) { + values.add(typeValue.value); + } + recordInstantiationPolicy.setValues(values); + return recordInstantiationPolicy.buildNewInstance(); + } catch (Throwable t) { + throw t; + } finally { + instanceLock.unlock(); + } + } + /** * Return an instance of the receivers javaClass. Set the attributes of an instance * from the values stored in the database row. diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/descriptors/RecordInstantiationPolicy.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/descriptors/RecordInstantiationPolicy.java new file mode 100644 index 00000000000..d40402fa51b --- /dev/null +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/descriptors/RecordInstantiationPolicy.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2024 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 v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// Oracle - initial API and implementation from Oracle TopLink +package org.eclipse.persistence.internal.descriptors; + +import org.eclipse.persistence.exceptions.DescriptorException; + +import java.lang.reflect.Constructor; +import java.lang.reflect.RecordComponent; +import java.util.ArrayList; +import java.util.List; + +/** + * Purpose: Allows customization of how an {@code java.lang.Record} is created/instantiated.

+ */ +public class RecordInstantiationPolicy extends InstantiationPolicy { + + /** + * Record class + */ + private Class clazz; + + /** + * Values passed to the record constructor + */ + private List values; + + /** + * Default constructor + */ + public RecordInstantiationPolicy() { + super(); + } + + /** + * Constructor + * + * @param clazz Record class + */ + public RecordInstantiationPolicy(Class clazz) { + this(); + this.clazz = clazz; + } + + /** + * Build and return a new instance, using the appropriate mechanism. + */ + @Override + public Object buildNewInstance() throws DescriptorException { + try { + //Handle ClassDecriptor validation/postInitialization + if (values == null) { + return null; + } else { + return newRecord(clazz, values); + } + } finally { + values = null; + } + } + + @Override + public void useFactoryInstantiationPolicy(String factoryClassName, String methodName) { + throw new UnsupportedOperationException(); + } + + private T newRecord(Class clazz, List values) { + List> types = new ArrayList<>(); + RecordComponent[] recordComponents = clazz.getRecordComponents(); + for (RecordComponent recordComponent: recordComponents) { + types.add(recordComponent.getType()); + } + try { + Constructor canonical = clazz.getDeclaredConstructor(types.toArray(Class[]::new)); + @SuppressWarnings("unchecked") + var result = (T) canonical.newInstance(values.toArray(Object[]::new)); + return result; + } catch (ReflectiveOperationException e) { + throw new RuntimeException("New Record creation failed: " + e, e); + } + } + + /** + * Values passed to the record constructor + * + * @param values values which are passed to the new record + */ + public void setValues(List values) { + this.values = values; + } + + /** + * INTERNAL: + * Clones the InstantiationPolicy + */ + @Override + public Object clone() { + try { + // clones itself + return super.clone(); + } catch (Exception exception) { + throw new AssertionError(exception); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + "()"; + } +} diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/mappings/AggregateMapping.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/mappings/AggregateMapping.java index a460db93fa6..6f19c3e1197 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/mappings/AggregateMapping.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/mappings/AggregateMapping.java @@ -157,6 +157,8 @@ public void buildBackupClone(Object clone, Object backup, UnitOfWorkImpl unitOfW protected Object buildBackupClonePart(Object attributeValue, UnitOfWorkImpl unitOfWork) { if (attributeValue == null) { return null; + } else if (attributeValue instanceof Record) { + return referenceDescriptor.getCopyPolicy().buildClone(attributeValue, unitOfWork); } return getObjectBuilder(attributeValue, unitOfWork).buildBackupClone(attributeValue, unitOfWork); } @@ -223,7 +225,9 @@ protected Object buildClonePart(Object attributeValue, Object clone, CacheKey pa // bug 2612602 as we are building the working copy make sure that we call to correct clone method. Object clonedAttributeValue = aggregateObjectBuilder.instantiateWorkingCopyClone(attributeValue, cloningSession); - aggregateObjectBuilder.populateAttributesForClone(attributeValue, parentCacheKey, clonedAttributeValue, refreshCascade, cloningSession); + if (!(clonedAttributeValue instanceof Record)) { + aggregateObjectBuilder.populateAttributesForClone(attributeValue, parentCacheKey, clonedAttributeValue, refreshCascade, cloningSession); + } //also clone the fetch group reference if applied if (aggregateObjectBuilder.getDescriptor().hasFetchGroupManager()) { aggregateObjectBuilder.getDescriptor().getFetchGroupManager().copyAggregateFetchGroupInto(attributeValue, clonedAttributeValue, clone, cloningSession); @@ -700,23 +704,29 @@ public void mergeIntoObject(Object target, boolean isTargetUnInitialized, Object return; } - Object targetAttributeValue = getAttributeValueFromObject(target); - boolean originalWasNull = targetAttributeValue == null; - if (targetAttributeValue == null || targetAttributeValue == sourceAttributeValue || !targetAttributeValue.getClass().equals(sourceAttributeValue.getClass())) { - // avoid null-pointer/nothing to merge to - create a new instance - // (a new clone cannot be used as all changes must be merged) - targetAttributeValue = buildNewMergeInstanceOf(sourceAttributeValue, mergeManager.getSession()); - mergeAttributeValue(targetAttributeValue, true, sourceAttributeValue, mergeManager, targetSession); - // setting new instance so fire event as if set was called by user. - // this call will eventually get passed to updateChangeRecord which will - //ensure this new aggregates is fully initialized with listeners. - // If merge into the unit of work, must only merge and raise the event is the value changed. - if ((mergeManager.shouldMergeCloneIntoWorkingCopy() || mergeManager.shouldMergeCloneWithReferencesIntoWorkingCopy()) && !mergeManager.isForRefresh()) { - this.descriptor.getObjectChangePolicy().raiseInternalPropertyChangeEvent(target, getAttributeName(), getAttributeValueFromObject(target), targetAttributeValue); - } - + Object targetAttributeValue = null; + boolean originalWasNull = false; + if (sourceAttributeValue instanceof Record) { + targetAttributeValue = referenceDescriptor.getCopyPolicy().buildClone(sourceAttributeValue, targetSession); } else { - mergeAttributeValue(targetAttributeValue, isTargetUnInitialized, sourceAttributeValue, mergeManager, targetSession); + targetAttributeValue = getAttributeValueFromObject(target); + originalWasNull = targetAttributeValue == null; + if (targetAttributeValue == null || targetAttributeValue == sourceAttributeValue || !targetAttributeValue.getClass().equals(sourceAttributeValue.getClass())) { + // avoid null-pointer/nothing to merge to - create a new instance + // (a new clone cannot be used as all changes must be merged) + targetAttributeValue = buildNewMergeInstanceOf(sourceAttributeValue, mergeManager.getSession()); + mergeAttributeValue(targetAttributeValue, true, sourceAttributeValue, mergeManager, targetSession); + // setting new instance so fire event as if set was called by user. + // this call will eventually get passed to updateChangeRecord which will + //ensure this new aggregates is fully initialized with listeners. + // If merge into the unit of work, must only merge and raise the event is the value changed. + if ((mergeManager.shouldMergeCloneIntoWorkingCopy() || mergeManager.shouldMergeCloneWithReferencesIntoWorkingCopy()) && !mergeManager.isForRefresh()) { + this.descriptor.getObjectChangePolicy().raiseInternalPropertyChangeEvent(target, getAttributeName(), getAttributeValueFromObject(target), targetAttributeValue); + } + + } else { + mergeAttributeValue(targetAttributeValue, isTargetUnInitialized, sourceAttributeValue, mergeManager, targetSession); + } } if(this.descriptor.hasFetchGroupManager()) { FetchGroup sourceFetchGroup = this.descriptor.getFetchGroupManager().getObjectFetchGroup(source); diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/mappings/AggregateObjectMapping.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/mappings/AggregateObjectMapping.java index 8f625a67e97..a356490d8b3 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/mappings/AggregateObjectMapping.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/mappings/AggregateObjectMapping.java @@ -471,7 +471,11 @@ public Object buildAggregateFromRow(AbstractRecord databaseRow, Object targetObj // 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(); + if (this.getReferenceClass().isRecord()) { + aggregate = descriptor.getObjectBuilder().buildNewRecordInstance((Class) this.getReferenceClass(), descriptor.getMappings(), databaseRow, executionSession); + } else { + aggregate = descriptor.getObjectBuilder().buildNewInstance(); + } refreshing = false; } @@ -489,7 +493,9 @@ public Object buildAggregateFromRow(AbstractRecord databaseRow, Object targetObj } else if (executionSession.isUnitOfWork()) { descriptor.getObjectBuilder().buildAttributesIntoWorkingCopyClone(aggregate, buildWrapperCacheKeyForAggregate(cacheKey, targetIsProtected), nestedQuery, joinManager, databaseRow, (UnitOfWorkImpl)executionSession, refreshing); } else { - descriptor.getObjectBuilder().buildAttributesIntoObject(aggregate, buildWrapperCacheKeyForAggregate(cacheKey, targetIsProtected), databaseRow, nestedQuery, joinManager, nestedQuery.getExecutionFetchGroup(descriptor), refreshing, executionSession); + if (!this.getReferenceClass().isRecord()) { + descriptor.getObjectBuilder().buildAttributesIntoObject(aggregate, buildWrapperCacheKeyForAggregate(cacheKey, targetIsProtected), databaseRow, nestedQuery, joinManager, nestedQuery.getExecutionFetchGroup(descriptor), refreshing, executionSession); + } } if ((targetFetchGroup != null) && descriptor.hasFetchGroupManager() && cacheKey != null && !refreshing && sourceQuery.shouldMaintainCache() && !sourceQuery.shouldStoreBypassCache()) { diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/java/org/eclipse/persistence/testing/models/jpa/advanced/embeddable/EmbeddableRecordTableCreator.java b/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/java/org/eclipse/persistence/testing/models/jpa/advanced/embeddable/EmbeddableRecordTableCreator.java new file mode 100644 index 00000000000..6e368985f9d --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/java/org/eclipse/persistence/testing/models/jpa/advanced/embeddable/EmbeddableRecordTableCreator.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 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 v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// Oracle - Initial API and implementation. +package org.eclipse.persistence.testing.models.jpa.advanced.embeddable; + +import org.eclipse.persistence.testing.framework.TogglingFastTableCreator; +import org.eclipse.persistence.tools.schemaframework.TableDefinition; + +public class EmbeddableRecordTableCreator extends TogglingFastTableCreator { + + public EmbeddableRecordTableCreator() { + setName("EmbeddableProject"); + addTableDefinition(buildCmp3EmbedRecVisitorTable()); + } + + public static TableDefinition buildCmp3EmbedRecVisitorTable() { + TableDefinition table = createTable("CMP3_EMBED_REC_VISITOR"); + table.addField(createNumericPk("ID_INT")); + table.addField(createStringColumn("ID_STRING")); + table.addField(createStringColumn("NAME1")); + table.addField(createStringColumn("NAME2")); + table.addField(createStringColumn("NAME3")); + table.addField(createStringColumn("NAME4")); + return table; + } +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/java/org/eclipse/persistence/testing/models/jpa/advanced/embeddable/RecordAttribute.java b/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/java/org/eclipse/persistence/testing/models/jpa/advanced/embeddable/RecordAttribute.java new file mode 100644 index 00000000000..7dd1f7eafa3 --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/java/org/eclipse/persistence/testing/models/jpa/advanced/embeddable/RecordAttribute.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 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 v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// Oracle - Initial API and implementation. +package org.eclipse.persistence.testing.models.jpa.advanced.embeddable; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; + +import java.util.Objects; + +@Embeddable +public record RecordAttribute(@Embedded RecordNestedAttribute nestedAttribute, String name3) { + + public RecordAttribute() { + this(null, null); + } + + public RecordAttribute(RecordNestedAttribute nestedAttribute, String name3) { + this.nestedAttribute = nestedAttribute; + this.name3 = name3; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RecordAttribute that = (RecordAttribute) o; + return Objects.equals(nestedAttribute, that.nestedAttribute) && Objects.equals(name3, that.name3); + } + + @Override + public int hashCode() { + return Objects.hash(nestedAttribute, name3); + } +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/java/org/eclipse/persistence/testing/models/jpa/advanced/embeddable/RecordEntity.java b/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/java/org/eclipse/persistence/testing/models/jpa/advanced/embeddable/RecordEntity.java new file mode 100644 index 00000000000..a6abaf7e95b --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/java/org/eclipse/persistence/testing/models/jpa/advanced/embeddable/RecordEntity.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 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 v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// Oracle - Initial API and implementation. +package org.eclipse.persistence.testing.models.jpa.advanced.embeddable; + +import jakarta.persistence.Embedded; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.NamedQueries; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.Table; + +@NamedQueries({ + @NamedQuery(name = "findRecordEntityById", + query = "SELECT OBJECT(o) FROM RecordEntity o WHERE o.id = :id"), + @NamedQuery(name = "findRecordEntityByRecordAttribute", + query = "SELECT OBJECT(o) FROM RecordEntity o WHERE o.recordAttribute = :recordAttribute")}) +@Entity +@Table(name = "CMP3_EMBED_REC_VISITOR") +public class RecordEntity { + + @EmbeddedId + private RecordPK id; + + @Embedded + private RecordAttribute recordAttribute; + + private String name4; + + public RecordEntity() { + } + + public RecordPK getId() { + return id; + } + + public void setId(RecordPK id) { + this.id = id; + } + + public RecordAttribute getRecordAttribute() { + return recordAttribute; + } + + public void setRecordAttribute(RecordAttribute recordAttribute) { + this.recordAttribute = recordAttribute; + } + + public String getName4() { + return name4; + } + + public void setName4(String name4) { + this.name4 = name4; + } +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/java/org/eclipse/persistence/testing/models/jpa/advanced/embeddable/RecordNestedAttribute.java b/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/java/org/eclipse/persistence/testing/models/jpa/advanced/embeddable/RecordNestedAttribute.java new file mode 100644 index 00000000000..531e91b5f4c --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/java/org/eclipse/persistence/testing/models/jpa/advanced/embeddable/RecordNestedAttribute.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 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 v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// Oracle - Initial API and implementation. +package org.eclipse.persistence.testing.models.jpa.advanced.embeddable; + +import jakarta.persistence.Embeddable; + +import java.util.Objects; + +@Embeddable +public record RecordNestedAttribute(String name1, String name2) { + + public RecordNestedAttribute() { + this(null, null); + } + + public RecordNestedAttribute(String name1, String name2) { + this.name1 = name1; + this.name2 = name2; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RecordNestedAttribute that = (RecordNestedAttribute) o; + return Objects.equals(name1, that.name1) && Objects.equals(name2, that.name2); + } + + @Override + public int hashCode() { + return Objects.hash(name1, name2); + } +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/java/org/eclipse/persistence/testing/models/jpa/advanced/embeddable/RecordPK.java b/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/java/org/eclipse/persistence/testing/models/jpa/advanced/embeddable/RecordPK.java new file mode 100644 index 00000000000..bf7a617fb7e --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/java/org/eclipse/persistence/testing/models/jpa/advanced/embeddable/RecordPK.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 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 v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// Oracle - Initial API and implementation. +package org.eclipse.persistence.testing.models.jpa.advanced.embeddable; + +import jakarta.persistence.Embeddable; + +import java.io.Serializable; +import java.util.Objects; + +@Embeddable +public record RecordPK (int id_int, String id_string) implements Serializable { + + public RecordPK() { + this(0,null); + } + + public RecordPK(int id_int, String id_string) { + this.id_int = id_int; + this.id_string = id_string; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RecordPK recordPK = (RecordPK) o; + return id_int == recordPK.id_int && Objects.equals(id_string, recordPK.id_string); + } + + @Override + public int hashCode() { + return Objects.hash(id_int, id_string); + } +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/resources/META-INF/persistence.xml b/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/resources/META-INF/persistence.xml index 5b9eef02dd2..3b476626d3d 100644 --- a/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/resources/META-INF/persistence.xml +++ b/jpa/eclipselink.jpa.testapps/jpa.test.advanced.embeddable/src/main/resources/META-INF/persistence.xml @@ -1,6 +1,6 @@