Skip to content

Commit c9c3078

Browse files
committed
HHH-19056 prevent NPE when using @mapsid on an embeddable
1 parent 95779d4 commit c9c3078

File tree

6 files changed

+233
-2
lines changed

6 files changed

+233
-2
lines changed

hibernate-core/src/main/java/org/hibernate/boot/model/internal/ToOneBinder.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ static void defineFetchingStrategy(
335335
handleLazy( toOne, property );
336336
handleFetch( toOne, property );
337337
handleFetchProfileOverrides( toOne, property, propertyHolder, inferredData );
338+
handleMapsId( toOne, property );
338339
}
339340

340341
private static void handleLazy(ToOne toOne, MemberDetails property) {
@@ -373,6 +374,13 @@ private static void handleFetch(ToOne toOne, MemberDetails property) {
373374
}
374375
}
375376

377+
private static void handleMapsId(ToOne toOne, MemberDetails property) {
378+
final MapsId mapsIdAnnotation = property.getDirectAnnotationUsage( MapsId.class );
379+
if ( mapsIdAnnotation != null ) {
380+
toOne.setHasMapsId( true );
381+
}
382+
}
383+
376384
private static void setHibernateFetchMode(ToOne toOne, MemberDetails property, org.hibernate.annotations.FetchMode fetchMode) {
377385
switch ( fetchMode ) {
378386
case JOIN:

hibernate-core/src/main/java/org/hibernate/id/CompositeNestedGeneratedValueGenerator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public void addGeneratedValuePlan(GenerationPlan plan) {
133133
public Object generate(SharedSessionContractImplementor session, Object object) {
134134
final Object context = generationContextLocator.locateGenerationContext( session, object );
135135
final List<Object> generatedValues = generatedValues( session, object, context );
136-
if ( generatedValues != null) {
136+
if ( generatedValues != null ) {
137137
final Object[] values = compositeType.getPropertyValues( context );
138138
for ( int i = 0; i < generatedValues.size(); i++ ) {
139139
values[generationPlans.get( i ).getPropertyIndex()] = generatedValues.get( i );

hibernate-core/src/main/java/org/hibernate/mapping/Component.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,13 @@
3535
import org.hibernate.internal.util.collections.CollectionHelper;
3636
import org.hibernate.metamodel.mapping.DiscriminatorType;
3737
import org.hibernate.metamodel.mapping.EmbeddableDiscriminatorConverter;
38+
import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart;
3839
import org.hibernate.metamodel.mapping.internal.DiscriminatorTypeImpl;
40+
import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping;
3941
import org.hibernate.metamodel.spi.EmbeddableInstantiator;
4042
import org.hibernate.persister.entity.DiscriminatorHelper;
4143
import org.hibernate.models.spi.ClassDetails;
44+
import org.hibernate.persister.entity.EntityPersister;
4245
import org.hibernate.property.access.spi.Setter;
4346
import org.hibernate.resource.beans.internal.FallbackBeanInstanceProducer;
4447
import org.hibernate.type.ComponentType;
@@ -782,7 +785,28 @@ public StandardGenerationContextLocator(String entityName) {
782785

783786
@Override
784787
public Object locateGenerationContext(SharedSessionContractImplementor session, Object incomingObject) {
785-
return session.getEntityPersister( entityName, incomingObject ).getIdentifier( incomingObject, session );
788+
final var persister = session.getEntityPersister( entityName, incomingObject );
789+
final var context = persister.getIdentifier( incomingObject, session );
790+
if ( context != null ) {
791+
return context;
792+
}
793+
794+
if ( persister.getIdentifierMapping() instanceof EmbeddableValuedModelPart embeddableId && containsMapsId( persister ) ) {
795+
final var strategy = embeddableId.getEmbeddableTypeDescriptor().getRepresentationStrategy();
796+
return strategy.getInstantiator().instantiate( null );
797+
}
798+
return null;
799+
}
800+
801+
private static boolean containsMapsId(EntityPersister persister) {
802+
final var attributeMappings = persister.getAttributeMappings();
803+
for ( var i = 0; i < attributeMappings.size(); i++ ) {
804+
final var attributeMapping = attributeMappings.get( i );
805+
if ( attributeMapping instanceof ToOneAttributeMapping toOneMapping && toOneMapping.hasMapsId() ) {
806+
return true;
807+
}
808+
}
809+
return false;
786810
}
787811
}
788812

hibernate-core/src/main/java/org/hibernate/mapping/ToOne.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public abstract sealed class ToOne
3434
private boolean unwrapProxy;
3535
private boolean unwrapProxyImplicit;
3636
private boolean referenceToPrimaryKey = true;
37+
private boolean hasMapsId = false;
3738

3839
protected ToOne(MetadataBuildingContext buildingContext, Table table) {
3940
super( buildingContext, table );
@@ -79,6 +80,14 @@ public void setReferencedEntityName(String referencedEntityName) {
7980
null : referencedEntityName.intern();
8081
}
8182

83+
public boolean hasMapsId() {
84+
return hasMapsId;
85+
}
86+
87+
public void setHasMapsId(boolean hasMapsId) {
88+
this.hasMapsId = hasMapsId;
89+
}
90+
8291
public String getPropertyName() {
8392
return propertyName;
8493
}

hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ public class Entity1 {
155155

156156
private final Cardinality cardinality;
157157
private final boolean hasJoinTable;
158+
private final boolean hasMapsId;
158159
/*
159160
Capture the other side's name of a possibly bidirectional association to allow resolving circular fetches.
160161
It may be null if the referenced property is a non-entity.
@@ -183,6 +184,7 @@ protected ToOneAttributeMapping(ToOneAttributeMapping original) {
183184
targetKeyPropertyName = original.targetKeyPropertyName;
184185
cardinality = original.cardinality;
185186
hasJoinTable = original.hasJoinTable;
187+
hasMapsId = original.hasMapsId;
186188
bidirectionalAttributePath = original.bidirectionalAttributePath;
187189
declaringTableGroupProducer = original.declaringTableGroupProducer;
188190
isKeyTableNullable = original.isKeyTableNullable;
@@ -250,6 +252,7 @@ public ToOneAttributeMapping(
250252
);
251253
sqlAliasStem = SqlAliasStemHelper.INSTANCE.generateStemFromAttributeName( name );
252254
isNullable = bootValue.isNullable();
255+
hasMapsId = bootValue.hasMapsId();
253256
isLazy = navigableRole.getParent().getParent() == null
254257
&& declaringEntityPersister.getBytecodeEnhancementMetadata()
255258
.getLazyAttributesMetadata()
@@ -679,6 +682,7 @@ private ToOneAttributeMapping(
679682
this.targetKeyPropertyNames = original.targetKeyPropertyNames;
680683
this.cardinality = original.cardinality;
681684
this.hasJoinTable = original.hasJoinTable;
685+
this.hasMapsId = original.hasMapsId;
682686
this.bidirectionalAttributePath = original.bidirectionalAttributePath;
683687
this.declaringTableGroupProducer = declaringTableGroupProducer;
684688
this.isInternalLoadNullable = original.isInternalLoadNullable;
@@ -2395,6 +2399,10 @@ public boolean hasNotFoundAction() {
23952399
return notFoundAction != null;
23962400
}
23972401

2402+
public boolean hasMapsId() {
2403+
return hasMapsId;
2404+
}
2405+
23982406
@Override
23992407
public boolean isUnwrapProxy() {
24002408
return unwrapProxy;
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.annotations.mapsid;
6+
7+
import jakarta.persistence.CascadeType;
8+
import jakarta.persistence.EmbeddedId;
9+
import jakarta.persistence.Entity;
10+
import jakarta.persistence.GeneratedValue;
11+
import jakarta.persistence.Id;
12+
import jakarta.persistence.ManyToOne;
13+
import jakarta.persistence.MapsId;
14+
import jakarta.persistence.OneToMany;
15+
import jakarta.persistence.OneToOne;
16+
import org.hibernate.id.IdentifierGenerationException;
17+
import org.hibernate.testing.orm.junit.DomainModel;
18+
import org.hibernate.testing.orm.junit.Jira;
19+
import org.hibernate.testing.orm.junit.SessionFactory;
20+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
21+
import org.junit.jupiter.api.Test;
22+
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
import java.util.Objects;
26+
27+
import static org.junit.jupiter.api.Assertions.assertThrows;
28+
29+
@SessionFactory
30+
@DomainModel(
31+
annotatedClasses = {
32+
MapsEmbeddedIdNullTest.Level0.class,
33+
MapsEmbeddedIdNullTest.Level1.class,
34+
MapsEmbeddedIdNullTest.Level2.class,
35+
MapsEmbeddedIdNullTest.Level1NoMapsId.class,
36+
MapsEmbeddedIdNullTest.Level1OneToOne.class,
37+
MapsEmbeddedIdNullTest.Level1NoMapsIdOneToOne.class,
38+
})
39+
@Jira("https://hibernate.atlassian.net/browse/HHH-19056")
40+
public class MapsEmbeddedIdNullTest {
41+
42+
@Test
43+
void test(SessionFactoryScope scope) {
44+
scope.inTransaction( s -> {
45+
Level0 level0 = new Level0();
46+
Level2 level2 = new Level2();
47+
Level1 level1 = new Level1( level0, level2 );
48+
level0.level1s.add( level1 );
49+
s.persist( level0 );
50+
} );
51+
52+
scope.inTransaction( s -> {
53+
Level0 level0 = new Level0();
54+
Level2 level2 = new Level2();
55+
Level1OneToOne level1 = new Level1OneToOne( level0, level2 );
56+
s.persist( level1 );
57+
} );
58+
59+
assertThrows( IdentifierGenerationException.class, () ->
60+
scope.inTransaction( s -> {
61+
Level0 level0 = new Level0();
62+
Level2 level2 = new Level2();
63+
Level1NoMapsId level1NoMapsId = new Level1NoMapsId();
64+
level1NoMapsId.level0 = level0;
65+
level1NoMapsId.level2 = level2;
66+
s.persist( level1NoMapsId );
67+
} )
68+
);
69+
70+
assertThrows( IdentifierGenerationException.class, () ->
71+
scope.inTransaction( s -> {
72+
Level0 level0 = new Level0();
73+
Level2 level2 = new Level2();
74+
Level1NoMapsIdOneToOne level1 = new Level1NoMapsIdOneToOne();
75+
level1.level0 = level0;
76+
level1.level2 = level2;
77+
s.persist( level1 );
78+
} )
79+
);
80+
}
81+
82+
@Entity(name = "Level0")
83+
public static class Level0 {
84+
@Id
85+
@GeneratedValue
86+
private Integer id;
87+
@OneToMany(mappedBy = "level0", cascade = CascadeType.ALL)
88+
private List<Level1> level1s = new ArrayList<>();
89+
}
90+
91+
@Entity(name = "Level1")
92+
public static class Level1 {
93+
@EmbeddedId
94+
Level1PK id;
95+
@MapsId("level0Id")
96+
@ManyToOne
97+
private Level0 level0;
98+
@MapsId("level2Id")
99+
@ManyToOne(cascade = CascadeType.ALL)
100+
private Level2 level2;
101+
102+
public Level1() {
103+
}
104+
105+
public Level1(Level0 level0, Level2 level2) {
106+
super();
107+
this.level0 = level0;
108+
this.level2 = level2;
109+
}
110+
}
111+
112+
@Entity(name = "Level1OneToOne")
113+
public static class Level1OneToOne {
114+
@EmbeddedId
115+
Level1PK id;
116+
@OneToOne
117+
@MapsId("level0Id")
118+
private Level0 level0;
119+
@OneToOne
120+
@MapsId("level2Id")
121+
private Level2 level2;
122+
123+
public Level1OneToOne() {
124+
}
125+
126+
public Level1OneToOne(Level0 level0, Level2 level2) {
127+
super();
128+
this.level0 = level0;
129+
this.level2 = level2;
130+
}
131+
}
132+
133+
134+
@Entity(name = "Level1NoMapsId")
135+
public static class Level1NoMapsId {
136+
@EmbeddedId
137+
Level1PK id;
138+
@ManyToOne
139+
private Level0 level0;
140+
@ManyToOne(cascade = CascadeType.ALL)
141+
private Level2 level2;
142+
}
143+
144+
@Entity(name = "Level1NoMapsIdOneToOne")
145+
public static class Level1NoMapsIdOneToOne {
146+
@EmbeddedId
147+
Level1PK id;
148+
@ManyToOne
149+
private Level0 level0;
150+
@ManyToOne(cascade = CascadeType.ALL)
151+
private Level2 level2;
152+
}
153+
154+
@Entity(name = "Level2")
155+
public static class Level2 {
156+
@Id
157+
@GeneratedValue
158+
private Integer id;
159+
}
160+
161+
public static class Level1PK {
162+
private Integer level0Id;
163+
private Integer level2Id;
164+
165+
@Override
166+
public final boolean equals(Object o) {
167+
if ( !(o instanceof Level1PK level1PK) ) {
168+
return false;
169+
}
170+
171+
return Objects.equals( level0Id, level1PK.level0Id )
172+
&& Objects.equals( level2Id, level1PK.level2Id );
173+
}
174+
175+
@Override
176+
public int hashCode() {
177+
int result = Objects.hashCode( level0Id );
178+
result = 31 * result + Objects.hashCode( level2Id );
179+
return result;
180+
}
181+
}
182+
}

0 commit comments

Comments
 (0)