Skip to content

Commit d522745

Browse files
committed
HHH-19963 Only consider a ToOne be bidirectional for OneToMany if FKs are equal
1 parent 2269a00 commit d522745

File tree

2 files changed

+146
-5
lines changed

2 files changed

+146
-5
lines changed

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

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.hibernate.metamodel.mapping.SelectableMapping;
3333
import org.hibernate.metamodel.mapping.SoftDeleteMapping;
3434
import org.hibernate.metamodel.mapping.TableDetails;
35+
import org.hibernate.metamodel.mapping.ValuedModelPart;
3536
import org.hibernate.metamodel.mapping.ordering.OrderByFragment;
3637
import org.hibernate.metamodel.mapping.ordering.OrderByFragmentTranslator;
3738
import org.hibernate.metamodel.mapping.ordering.TranslationContext;
@@ -225,11 +226,31 @@ private static void injectAttributeMapping(
225226
@Override
226227
public boolean isBidirectionalAttributeName(NavigablePath fetchablePath, ToOneAttributeMapping modelPart) {
227228
return bidirectionalAttributeName == null
228-
// If the FK-target of the to-one mapping is the same as the FK-target of this plural mapping,
229-
// then we say this is bidirectional, given that this is only invoked for model parts of the
230-
// collection elements
231-
? fkDescriptor.getTargetPart() == modelPart.getForeignKeyDescriptor().getTargetPart()
232-
: fetchablePath.getLocalName().endsWith( bidirectionalAttributeName );
229+
// If the FK-target of the to-one mapping is the same as the FK-target of this one-to-many mapping,
230+
// and the FK-key refer to the same column then we say this is bidirectional,
231+
// given that this is only invoked for model parts of the collection elements
232+
? modelPart.getSideNature() == ForeignKeyDescriptor.Nature.KEY
233+
&& collectionDescriptor.isOneToMany()
234+
&& fkDescriptor.getTargetPart() == modelPart.getForeignKeyDescriptor().getTargetPart()
235+
&& areEqual( fkDescriptor.getKeyPart(), modelPart.getForeignKeyDescriptor().getKeyPart() )
236+
: fetchablePath.getLocalName().equals( bidirectionalAttributeName );
237+
}
238+
239+
private boolean areEqual(ValuedModelPart part1, ValuedModelPart part2) {
240+
final int typeCount = part1.getJdbcTypeCount();
241+
if ( part2.getJdbcTypeCount() != typeCount ) {
242+
return false;
243+
}
244+
for ( int i = 0; i < typeCount; i++ ) {
245+
final SelectableMapping selectable1 = part1.getSelectable( i );
246+
final SelectableMapping selectable2 = part2.getSelectable( i );
247+
if ( selectable1.getJdbcMapping() != selectable2.getJdbcMapping()
248+
|| !selectable1.getContainingTableExpression().equals( selectable2.getContainingTableExpression() )
249+
|| !selectable1.getSelectionExpression().equals( selectable2.getSelectionExpression() ) ) {
250+
return false;
251+
}
252+
}
253+
return true;
233254
}
234255

235256
public void finishInitialization(
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.mapping.collections;
6+
7+
import jakarta.persistence.Entity;
8+
import jakarta.persistence.FetchType;
9+
import jakarta.persistence.Id;
10+
import jakarta.persistence.JoinColumn;
11+
import jakarta.persistence.JoinTable;
12+
import jakarta.persistence.ManyToMany;
13+
import jakarta.persistence.ManyToOne;
14+
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
15+
import org.hibernate.testing.orm.junit.Jira;
16+
import org.hibernate.testing.orm.junit.Jpa;
17+
import org.junit.jupiter.api.Test;
18+
19+
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.List;
22+
23+
import static org.junit.jupiter.api.Assertions.assertEquals;
24+
import static org.junit.jupiter.api.Assertions.assertNull;
25+
26+
@Jpa( annotatedClasses = {BidirectionalOneToManyTest.Organization.class, BidirectionalOneToManyTest.User.class} )
27+
@Jira("https://hibernate.atlassian.net/browse/HHH-19963")
28+
public class BidirectionalOneToManyTest {
29+
30+
@Test
31+
public void testParentNotTreatedAsBidirectional(EntityManagerFactoryScope scope) {
32+
scope.inTransaction( entityManager -> {
33+
Organization o3 = new Organization( 3L, "o1", null, new ArrayList<>() );
34+
Organization o1 = new Organization( 1L, "o1", null, new ArrayList<>( Arrays.asList( o3 )) );
35+
Organization o2 = new Organization( 2L, "o1", o1, new ArrayList<>() );
36+
entityManager.persist(o3);
37+
entityManager.persist(o1);
38+
entityManager.persist(o2);
39+
40+
User u1 = new User( 1L, o2 );
41+
User u2 = new User( 2L, o2 );
42+
entityManager.persist(u1);
43+
entityManager.persist(u2);
44+
});
45+
46+
scope.inTransaction( entityManager -> {
47+
User user1 = entityManager.find(User.class, "user_1");
48+
Organization ou3 = entityManager.find(Organization.class, "ou_3");
49+
assertNull( ou3.getParentOrganization(), "Parent of ou_3 is null");
50+
assertEquals(0, ou3.getPredecessorOrganizations().size(), "Predecessors of ou_3 is empty");
51+
});
52+
}
53+
54+
@Entity(name = "Organization")
55+
public static class Organization {
56+
57+
@Id
58+
private Long id;
59+
private String name;
60+
61+
@ManyToOne(fetch = FetchType.EAGER)
62+
@JoinColumn(name = "parentorganization_objectId")
63+
private Organization parentOrganization;
64+
65+
@ManyToMany(fetch = FetchType.EAGER)
66+
@JoinTable(name = "organization_predecessor")
67+
private List<Organization> predecessorOrganizations = new ArrayList<>();
68+
69+
public Organization() {
70+
}
71+
72+
public Organization(Long id, String name, Organization parentOrganization, List<Organization> predecessorOrganizations) {
73+
this.id = id;
74+
this.name = name;
75+
this.parentOrganization = parentOrganization;
76+
this.predecessorOrganizations = predecessorOrganizations;
77+
}
78+
79+
public Long getId() {
80+
return id;
81+
}
82+
83+
public String getName() {
84+
return name;
85+
}
86+
87+
public Organization getParentOrganization() {
88+
return parentOrganization;
89+
}
90+
91+
public List<Organization> getPredecessorOrganizations() {
92+
return predecessorOrganizations;
93+
}
94+
}
95+
96+
@Entity(name = "User")
97+
public static class User {
98+
99+
@Id
100+
private Long id;
101+
@ManyToOne(fetch = FetchType.EAGER)
102+
private Organization organization;
103+
104+
public User() {
105+
}
106+
107+
public User(Long id, Organization organization) {
108+
this.id = id;
109+
this.organization = organization;
110+
}
111+
112+
public Long getId() {
113+
return id;
114+
}
115+
116+
public Organization getOrganization() {
117+
return organization;
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)