Skip to content

Commit e13be02

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

File tree

2 files changed

+147
-5
lines changed

2 files changed

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

0 commit comments

Comments
 (0)