Skip to content

Commit cd1daf0

Browse files
committed
@
fix: support Java records as JPA @IdClass createPrimaryKeyInstance() assumed the primary key class could be built via a no-arg constructor followed by field reflection. Records have neither a no-arg constructor nor writable fields. Detect a record pkClass and build it via RecordInstantiationPolicy and its canonical constructor. @
1 parent 1ef871b commit cd1daf0

4 files changed

Lines changed: 181 additions & 3 deletions

File tree

foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/CMPPolicy.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.eclipse.persistence.exceptions.DescriptorException;
2828
import org.eclipse.persistence.exceptions.ValidationException;
2929
import org.eclipse.persistence.internal.descriptors.ObjectBuilder;
30+
import org.eclipse.persistence.internal.descriptors.RecordInstantiationPolicy;
3031
import org.eclipse.persistence.internal.helper.DatabaseField;
3132
import org.eclipse.persistence.internal.identitymaps.CacheId;
3233
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
@@ -38,6 +39,7 @@
3839
import org.eclipse.persistence.queries.UpdateObjectQuery;
3940

4041
import java.io.Serializable;
42+
import java.util.ArrayList;
4143
import java.util.HashSet;
4244
import java.util.List;
4345
import java.util.Set;
@@ -462,7 +464,10 @@ public Object createPrimaryKeyInstance(Object object, AbstractSession session) {
462464
return fieldValue;
463465
}
464466

465-
Object keyInstance = getPKClassInstance();
467+
Class<?> pkClass = getPKClass();
468+
boolean isRecord = pkClass != null && pkClass.isRecord();
469+
Object keyInstance = isRecord ? null : getPKClassInstance();
470+
List<Object> recordValues = isRecord ? new ArrayList<>(pkElementArray.length) : null;
466471
Set<ObjectReferenceMapping> usedObjectReferenceMappings = new HashSet<>();
467472
for (int index = 0; index < pkElementArray.length; index++) {
468473
Object keyObj = object;
@@ -492,11 +497,27 @@ public Object createPrimaryKeyInstance(Object object, AbstractSession session) {
492497
fieldValue = mapping.getReferenceDescriptor().getCMPPolicy().createPrimaryKeyInstance(fieldValue, session);
493498
usedObjectReferenceMappings.add((ObjectReferenceMapping)mapping);
494499
}
495-
accessor.setValue(nestedKeyInstance, fieldValue);
500+
if (isRecord) {
501+
recordValues.add(fieldValue);
502+
} else {
503+
accessor.setValue(nestedKeyInstance, fieldValue);
504+
}
496505
}
497506
}
498507

499-
return keyInstance;
508+
return isRecord ? createRecordInstance(pkClass, recordValues) : keyInstance;
509+
}
510+
511+
/**
512+
* Build a record primary key class instance from its component values using
513+
* its canonical constructor. Records are immutable, so the default
514+
* no-arg-constructor + field-reflection approach used for bean id classes
515+
* cannot be applied.
516+
*/
517+
private Object createRecordInstance(Class<?> recordClass, List<?> values) {
518+
RecordInstantiationPolicy policy = new RecordInstantiationPolicy(recordClass);
519+
policy.setValues(values);
520+
return policy.buildNewInstance();
500521
}
501522

502523
/**
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0,
7+
* or the Eclipse Distribution License v. 1.0 which is available at
8+
* http://www.eclipse.org/org/documents/edl-v10.php.
9+
*
10+
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
11+
*/
12+
13+
package org.eclipse.persistence.jpa.test.record;
14+
15+
import jakarta.persistence.EntityManager;
16+
import jakarta.persistence.EntityManagerFactory;
17+
18+
import org.eclipse.persistence.jpa.test.framework.DDLGen;
19+
import org.eclipse.persistence.jpa.test.framework.Emf;
20+
import org.eclipse.persistence.jpa.test.framework.EmfRunner;
21+
import org.eclipse.persistence.jpa.test.record.model.RecordIdClass;
22+
import org.eclipse.persistence.jpa.test.record.model.RecordIdClassEntity;
23+
import org.junit.Test;
24+
import org.junit.runner.RunWith;
25+
26+
import static org.junit.Assert.assertEquals;
27+
import static org.junit.Assert.assertNotNull;
28+
import static org.junit.Assert.assertTrue;
29+
30+
/**
31+
* Tests for a {@code java.lang.Record} used as a JPA {@code @IdClass}.
32+
*/
33+
@RunWith(EmfRunner.class)
34+
public class TestRecordIdClass {
35+
36+
@Emf(createTables = DDLGen.DROP_CREATE, classes = { RecordIdClassEntity.class })
37+
private EntityManagerFactory emf;
38+
39+
@Test
40+
public void testPersistAndFind() {
41+
EntityManager em = emf.createEntityManager();
42+
try {
43+
em.getTransaction().begin();
44+
RecordIdClassEntity entity = new RecordIdClassEntity("code-1", "description-1");
45+
em.persist(entity);
46+
em.getTransaction().commit();
47+
em.clear();
48+
49+
RecordIdClass idClass = new RecordIdClass(entity.getId(), entity.getCode());
50+
RecordIdClassEntity found = em.find(RecordIdClassEntity.class, idClass);
51+
assertNotNull("Entity should be found after persist", found);
52+
assertEquals("description-1", found.getDescription());
53+
} finally {
54+
if (em.getTransaction().isActive()) {
55+
em.getTransaction().rollback();
56+
}
57+
em.close();
58+
}
59+
}
60+
61+
@Test
62+
public void testGetIdentifier() {
63+
RecordIdClassEntity entity = new RecordIdClassEntity("code-1", "description-1");
64+
65+
Object identifier = emf.getPersistenceUnitUtil().getIdentifier(entity);
66+
67+
assertNotNull("Identifier should not be null", identifier);
68+
assertTrue("Identifier should be a RecordIdClass", identifier instanceof RecordIdClass);
69+
RecordIdClass idClass = (RecordIdClass) identifier;
70+
assertEquals(entity.getId(), idClass.id());
71+
assertEquals("code-1", idClass.code());
72+
}
73+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0,
7+
* or the Eclipse Distribution License v. 1.0 which is available at
8+
* http://www.eclipse.org/org/documents/edl-v10.php.
9+
*
10+
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
11+
*/
12+
13+
package org.eclipse.persistence.jpa.test.record.model;
14+
15+
import java.util.Objects;
16+
import java.util.UUID;
17+
18+
/**
19+
* A two-component record used as a JPA {@code @IdClass}.
20+
*/
21+
public record RecordIdClass(UUID id, String code) {
22+
23+
public RecordIdClass {
24+
Objects.requireNonNull(id, "id must not be null");
25+
Objects.requireNonNull(code, "code must not be null");
26+
}
27+
28+
public RecordIdClass(String code) {
29+
this(UUID.randomUUID(), code);
30+
}
31+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0,
7+
* or the Eclipse Distribution License v. 1.0 which is available at
8+
* http://www.eclipse.org/org/documents/edl-v10.php.
9+
*
10+
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
11+
*/
12+
13+
package org.eclipse.persistence.jpa.test.record.model;
14+
15+
import jakarta.persistence.Entity;
16+
import jakarta.persistence.Id;
17+
import jakarta.persistence.IdClass;
18+
19+
import java.util.UUID;
20+
21+
@Entity
22+
@IdClass(RecordIdClass.class)
23+
public class RecordIdClassEntity {
24+
25+
@Id
26+
private UUID id;
27+
28+
@Id
29+
private String code;
30+
31+
private String description;
32+
33+
protected RecordIdClassEntity() {
34+
}
35+
36+
public RecordIdClassEntity(String code, String description) {
37+
this.id = UUID.randomUUID();
38+
this.code = code;
39+
this.description = description;
40+
}
41+
42+
public UUID getId() {
43+
return id;
44+
}
45+
46+
public String getCode() {
47+
return code;
48+
}
49+
50+
public String getDescription() {
51+
return description;
52+
}
53+
}

0 commit comments

Comments
 (0)