Skip to content

Commit 527325f

Browse files
committed
HHH-19963 Only consider a ToOne be bidirectional for OneToMany if FKs are equal
1 parent 482d095 commit 527325f

File tree

2 files changed

+148
-5
lines changed

2 files changed

+148
-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;
@@ -226,12 +227,32 @@ private static void injectAttributeMapping(
226227

227228
@Override
228229
public boolean isBidirectionalAttributeName(NavigablePath fetchablePath, ToOneAttributeMapping modelPart) {
229-
if ( bidirectionalAttributeName == null ) {
230-
// If the FK-target of the to-one mapping is the same as the FK-target of this plural mapping,
231-
// then we say this is bidirectional, given that this is only invoked for model parts of the collection elements
232-
return fkDescriptor.getTargetPart() == modelPart.getForeignKeyDescriptor().getTargetPart();
230+
return bidirectionalAttributeName == null
231+
// If the FK-target of the to-one mapping is the same as the FK-target of this one-to-many mapping,
232+
// and the FK-key refer to the same column then we say this is bidirectional,
233+
// given that this is only invoked for model parts of the collection elements
234+
? modelPart.getSideNature() == ForeignKeyDescriptor.Nature.KEY
235+
&& collectionDescriptor.isOneToMany()
236+
&& fkDescriptor.getTargetPart() == modelPart.getForeignKeyDescriptor().getTargetPart()
237+
&& areEqual( fkDescriptor.getKeyPart(), modelPart.getForeignKeyDescriptor().getKeyPart() )
238+
: fetchablePath.getLocalName().equals( bidirectionalAttributeName );
239+
}
240+
241+
private boolean areEqual(ValuedModelPart part1, ValuedModelPart part2) {
242+
final int typeCount = part1.getJdbcTypeCount();
243+
if ( part2.getJdbcTypeCount() != typeCount ) {
244+
return false;
233245
}
234-
return fetchablePath.getLocalName().endsWith( bidirectionalAttributeName );
246+
for ( int i = 0; i < typeCount; i++ ) {
247+
final SelectableMapping selectable1 = part1.getSelectable( i );
248+
final SelectableMapping selectable2 = part2.getSelectable( i );
249+
if ( selectable1.getJdbcMapping() != selectable2.getJdbcMapping()
250+
|| !selectable1.getContainingTableExpression().equals( selectable2.getContainingTableExpression() )
251+
|| !selectable1.getSelectionExpression().equals( selectable2.getSelectionExpression() ) ) {
252+
return false;
253+
}
254+
}
255+
return true;
235256
}
236257

237258
public void finishInitialization(
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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 jakarta.persistence.Table;
15+
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
16+
import org.hibernate.testing.orm.junit.Jira;
17+
import org.hibernate.testing.orm.junit.Jpa;
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, "o3", null, new ArrayList<>() );
35+
Organization o1 = new Organization( 1L, "o1", null, new ArrayList<>( Arrays.asList( o3 )) );
36+
Organization o2 = new Organization( 2L, "o2", 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, 1L);
49+
Organization ou3 = entityManager.find(Organization.class, 3L);
50+
assertNull( ou3.getParentOrganization(), "Parent of o3 is null");
51+
assertEquals(0, ou3.getPredecessorOrganizations().size(), "Predecessors of o3 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+
@Table(name = "usr_tbl")
99+
public static class User {
100+
101+
@Id
102+
private Long id;
103+
@ManyToOne(fetch = FetchType.EAGER)
104+
private Organization organization;
105+
106+
public User() {
107+
}
108+
109+
public User(Long id, Organization organization) {
110+
this.id = id;
111+
this.organization = organization;
112+
}
113+
114+
public Long getId() {
115+
return id;
116+
}
117+
118+
public Organization getOrganization() {
119+
return organization;
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)