HHH-14466 : StackOverflowError loading an entity with eager one-to-many if bidirectional and many-to-one side is the ID
This commit is contained in:
parent
59735d2329
commit
2bacaabc37
|
@ -16,13 +16,20 @@ import org.hibernate.engine.spi.SessionFactoryImplementor;
|
|||
import org.hibernate.internal.CoreLogging;
|
||||
import org.hibernate.loader.plan.spi.CollectionReturn;
|
||||
import org.hibernate.loader.plan.spi.EntityReturn;
|
||||
import org.hibernate.loader.plan.spi.FetchSource;
|
||||
import org.hibernate.loader.plan.spi.LoadPlan;
|
||||
import org.hibernate.loader.plan.spi.Return;
|
||||
import org.hibernate.persister.collection.CollectionPersister;
|
||||
import org.hibernate.persister.entity.EntityPersister;
|
||||
import org.hibernate.persister.walking.spi.AssociationAttributeDefinition;
|
||||
import org.hibernate.persister.walking.spi.EncapsulatedEntityIdentifierDefinition;
|
||||
import org.hibernate.persister.walking.spi.EntityIdentifierDefinition;
|
||||
import org.hibernate.persister.walking.spi.NonEncapsulatedEntityIdentifierDefinition;
|
||||
import org.hibernate.persister.walking.spi.WalkingException;
|
||||
import org.hibernate.type.CompositeType;
|
||||
import org.hibernate.type.EmbeddedComponentType;
|
||||
import org.hibernate.type.EntityType;
|
||||
import org.hibernate.type.Type;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
|
@ -190,9 +197,59 @@ public class FetchStyleLoadPlanBuildingAssociationVisitationStrategy
|
|||
return new FetchStrategy( fetchStrategy.getTiming(), FetchStyle.SELECT );
|
||||
}
|
||||
|
||||
if ( attributeDefinition.getType().isCollectionType() && isTooManyCollections() ) {
|
||||
// todo : have this revert to batch or subselect fetching once "sql gen redesign" is in place
|
||||
return new FetchStrategy( fetchStrategy.getTiming(), FetchStyle.SELECT );
|
||||
final FetchSource currentSource = currentSource();
|
||||
final Type attributeType = attributeDefinition.getType();
|
||||
|
||||
|
||||
if ( attributeType.isCollectionType() ) {
|
||||
if ( isTooManyCollections() ) {
|
||||
// todo : have this revert to batch or subselect fetching once "sql gen redesign" is in place
|
||||
return new FetchStrategy( fetchStrategy.getTiming(), FetchStyle.SELECT );
|
||||
}
|
||||
if ( currentSource.resolveEntityReference() != null ) {
|
||||
CollectionPersister collectionPersister =
|
||||
(CollectionPersister) attributeDefinition.getType().getAssociatedJoinable( sessionFactory() );
|
||||
// Check if this is an eager "mappedBy" (inverse) side of a bidirectional
|
||||
// one-to-many/many-to-one association, with the many-to-one side
|
||||
// being the associated entity's ID as in:
|
||||
//
|
||||
// @Entity
|
||||
// public class Foo {
|
||||
// ...
|
||||
// @OneToMany(mappedBy = "foo", fetch = FetchType.EAGER)
|
||||
// private Set<Bar> bars = new HashSet<>();
|
||||
// }
|
||||
// @Entity
|
||||
// public class Bar implements Serializable {
|
||||
// @Id
|
||||
// @ManyToOne(fetch = FetchType.EAGER)
|
||||
// private Foo foo;
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
if ( fetchStrategy.getTiming() == FetchTiming.IMMEDIATE &&
|
||||
fetchStrategy.getStyle() == FetchStyle.JOIN &&
|
||||
collectionPersister.isOneToMany() &&
|
||||
collectionPersister.isInverse() ) {
|
||||
// This is an eager "mappedBy" (inverse) side of a bidirectional
|
||||
// one-to-many/many-to-one association
|
||||
final EntityType elementType = (EntityType) collectionPersister.getElementType();
|
||||
final Type elementIdType = ( (EntityPersister) elementType.getAssociatedJoinable( sessionFactory() ) ).getIdentifierType();
|
||||
if ( elementIdType.isComponentType() && ( (CompositeType) elementIdType ).isEmbedded() ) {
|
||||
final EmbeddedComponentType elementIdTypeEmbedded = (EmbeddedComponentType) elementIdType;
|
||||
if ( elementIdTypeEmbedded.getSubtypes().length == 1 &&
|
||||
elementIdTypeEmbedded.getPropertyNames()[ 0 ].equals( collectionPersister.getMappedByProperty() ) ) {
|
||||
// The associated entity's ID is the other (many-to-one) side of the association.
|
||||
// The one-to-many side must be set to FetchMode.SELECT; otherwise,
|
||||
// there will be an infinite loop because the current entity
|
||||
// would need to be loaded before the associated entity can be loaded,
|
||||
// but the associated entity cannot be loaded until after the current
|
||||
// entity is loaded (since the current entity is the associated entity's ID).
|
||||
return new FetchStrategy( fetchStrategy.getTiming(), FetchStyle.SELECT );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fetchStrategy;
|
||||
|
|
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
|
||||
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
|
||||
*/
|
||||
package org.hibernate.test.annotations.derivedidentities.bidirectional;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import javax.persistence.CascadeType;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.FetchType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.OneToMany;
|
||||
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import org.hibernate.testing.TestForIssue;
|
||||
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ManyToOneEagerDerivedIdFetchModeJoinTest extends BaseCoreFunctionalTestCase {
|
||||
private Foo foo;
|
||||
|
||||
@Test
|
||||
@TestForIssue(jiraKey = "HHH-14466")
|
||||
public void testQuery() {
|
||||
doInHibernate( this::sessionFactory, session -> {
|
||||
Bar newBar = (Bar) session.createQuery( "SELECT b FROM Bar b WHERE b.foo.id = :id" )
|
||||
.setParameter( "id", foo.getId() )
|
||||
.uniqueResult();
|
||||
assertNotNull( newBar );
|
||||
assertNotNull( newBar.getFoo() );
|
||||
assertTrue( Hibernate.isInitialized( newBar.getFoo() ) );
|
||||
assertEquals( foo.getId(), newBar.getFoo().getId() );
|
||||
assertTrue( Hibernate.isInitialized( newBar.getFoo().getBars() ) );
|
||||
assertEquals( 1, newBar.getFoo().getBars().size() );
|
||||
assertSame( newBar, newBar.getFoo().getBars().iterator().next() );
|
||||
assertEquals( "Some details", newBar.getDetails() );
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestForIssue(jiraKey = "HHH-14466")
|
||||
public void testQueryById() {
|
||||
|
||||
doInHibernate( this::sessionFactory, session -> {
|
||||
Bar newBar = (Bar) session.createQuery( "SELECT b FROM Bar b WHERE b.foo = :foo" )
|
||||
.setParameter( "foo", foo )
|
||||
.uniqueResult();
|
||||
assertNotNull( newBar );
|
||||
assertNotNull( newBar.getFoo() );
|
||||
assertTrue( Hibernate.isInitialized( newBar.getFoo() ) );
|
||||
assertEquals( foo.getId(), newBar.getFoo().getId() );
|
||||
assertTrue( Hibernate.isInitialized( newBar.getFoo().getBars() ) );
|
||||
assertEquals( 1, newBar.getFoo().getBars().size() );
|
||||
assertSame( newBar, newBar.getFoo().getBars().iterator().next() );
|
||||
assertEquals( "Some details", newBar.getDetails() );
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestForIssue(jiraKey = "HHH-14466")
|
||||
public void testFindByPrimaryKey() {
|
||||
|
||||
doInHibernate( this::sessionFactory, session -> {
|
||||
Bar newBar = session.find( Bar.class, foo.getId() );
|
||||
assertNotNull( newBar );
|
||||
assertNotNull( newBar.getFoo() );
|
||||
assertTrue( Hibernate.isInitialized( newBar.getFoo() ) );
|
||||
assertEquals( foo.getId(), newBar.getFoo().getId() );
|
||||
assertTrue( Hibernate.isInitialized( newBar.getFoo().getBars() ) );
|
||||
assertEquals( 1, newBar.getFoo().getBars().size() );
|
||||
assertSame( newBar, newBar.getFoo().getBars().iterator().next() );
|
||||
assertEquals( "Some details", newBar.getDetails() );
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestForIssue(jiraKey = "HHH-14466")
|
||||
public void testFindByInversePrimaryKey() {
|
||||
|
||||
doInHibernate( this::sessionFactory, session -> {
|
||||
Foo newFoo = session.find( Foo.class, foo.getId() );
|
||||
assertNotNull( newFoo );
|
||||
assertNotNull( newFoo.getBars() );
|
||||
assertTrue( Hibernate.isInitialized( newFoo.getBars() ) );
|
||||
assertEquals( 1, newFoo.getBars().size() );
|
||||
assertSame( newFoo, newFoo.getBars().iterator().next().getFoo() );
|
||||
assertEquals( "Some details", newFoo.getBars().iterator().next().getDetails() );
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setupData() {
|
||||
this.foo = doInHibernate( this::sessionFactory, session -> {
|
||||
Foo foo = new Foo();
|
||||
foo.id = 1L;
|
||||
session.persist( foo );
|
||||
|
||||
Bar bar = new Bar();
|
||||
bar.setFoo( foo );
|
||||
bar.setDetails( "Some details" );
|
||||
|
||||
foo.getBars().add( bar );
|
||||
|
||||
session.persist( bar );
|
||||
|
||||
session.flush();
|
||||
|
||||
assertNotNull( foo.getId() );
|
||||
assertEquals( foo.getId(), bar.getFoo().getId() );
|
||||
|
||||
return foo;
|
||||
});
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanupData() {
|
||||
doInHibernate( this::sessionFactory, session -> {
|
||||
session.delete( session.find( Foo.class, foo.id ) );
|
||||
});
|
||||
this.foo = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?>[] getAnnotatedClasses() {
|
||||
return new Class<?>[] {
|
||||
Foo.class,
|
||||
Bar.class,
|
||||
};
|
||||
}
|
||||
|
||||
@Entity(name = "Foo")
|
||||
public static class Foo {
|
||||
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
@OneToMany(mappedBy = "foo", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
private Set<Bar> bars = new HashSet<>();
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Set<Bar> getBars() {
|
||||
return bars;
|
||||
}
|
||||
|
||||
public void setBars(Set<Bar> bars) {
|
||||
this.bars = bars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if ( this == o ) {
|
||||
return true;
|
||||
}
|
||||
if ( o == null || getClass() != o.getClass() ) {
|
||||
return false;
|
||||
}
|
||||
Foo foo = (Foo) o;
|
||||
return id.equals( foo.id );
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash( id );
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(name = "Bar")
|
||||
public static class Bar implements Serializable {
|
||||
|
||||
@Id
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "BAR_ID")
|
||||
private Foo foo;
|
||||
|
||||
private String details;
|
||||
|
||||
public Foo getFoo() {
|
||||
return foo;
|
||||
}
|
||||
|
||||
public void setFoo(Foo foo) {
|
||||
this.foo = foo;
|
||||
}
|
||||
|
||||
public String getDetails() {
|
||||
return details;
|
||||
}
|
||||
|
||||
public void setDetails(String details) {
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if ( this == o ) {
|
||||
return true;
|
||||
}
|
||||
if ( o == null || getClass() != o.getClass() ) {
|
||||
return false;
|
||||
}
|
||||
Bar bar = (Bar) o;
|
||||
return foo.equals( bar.foo );
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash( foo );
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue