Skip to content

Commit 5dd29f8

Browse files
committed
HHH-19963 Only consider a ToOne be bidirectional for OneToMany if FKs are equal
1 parent 1ad46ec commit 5dd29f8

File tree

2 files changed

+150
-5
lines changed

2 files changed

+150
-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
@@ -39,6 +39,7 @@
3939
import org.hibernate.metamodel.mapping.SelectableMapping;
4040
import org.hibernate.metamodel.mapping.SoftDeleteMapping;
4141
import org.hibernate.metamodel.mapping.TableDetails;
42+
import org.hibernate.metamodel.mapping.ValuedModelPart;
4243
import org.hibernate.metamodel.mapping.ordering.OrderByFragment;
4344
import org.hibernate.metamodel.mapping.ordering.OrderByFragmentTranslator;
4445
import org.hibernate.metamodel.mapping.ordering.TranslationContext;
@@ -252,12 +253,32 @@ private static void injectAttributeMapping(
252253

253254
@Override
254255
public boolean isBidirectionalAttributeName(NavigablePath fetchablePath, ToOneAttributeMapping modelPart) {
255-
if ( bidirectionalAttributeName == null ) {
256-
// If the FK-target of the to-one mapping is the same as the FK-target of this plural mapping,
257-
// then we say this is bidirectional, given that this is only invoked for model parts of the collection elements
258-
return fkDescriptor.getTargetPart() == modelPart.getForeignKeyDescriptor().getTargetPart();
256+
return bidirectionalAttributeName == null
257+
// If the FK-target of the to-one mapping is the same as the FK-target of this one-to-many mapping,
258+
// and the FK-key refer to the same column then we say this is bidirectional,
259+
// given that this is only invoked for model parts of the collection elements
260+
? modelPart.getSideNature() == ForeignKeyDescriptor.Nature.KEY
261+
&& collectionDescriptor.isOneToMany()
262+
&& fkDescriptor.getTargetPart() == modelPart.getForeignKeyDescriptor().getTargetPart()
263+
&& areEqual( fkDescriptor.getKeyPart(), modelPart.getForeignKeyDescriptor().getKeyPart() )
264+
: fetchablePath.getLocalName().equals( bidirectionalAttributeName );
265+
}
266+
267+
private boolean areEqual(ValuedModelPart part1, ValuedModelPart part2) {
268+
final int typeCount = part1.getJdbcTypeCount();
269+
if ( part2.getJdbcTypeCount() != typeCount ) {
270+
return false;
259271
}
260-
return fetchablePath.getLocalName().endsWith( bidirectionalAttributeName );
272+
for ( int i = 0; i < typeCount; i++ ) {
273+
final SelectableMapping selectable1 = part1.getSelectable( i );
274+
final SelectableMapping selectable2 = part2.getSelectable( i );
275+
if ( selectable1.getJdbcMapping() != selectable2.getJdbcMapping()
276+
|| !selectable1.getContainingTableExpression().equals( selectable2.getContainingTableExpression() )
277+
|| !selectable1.getSelectionExpression().equals( selectable2.getSelectionExpression() ) ) {
278+
return false;
279+
}
280+
}
281+
return true;
261282
}
262283

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

0 commit comments

Comments
 (0)