HHH-16191 Add test and fix checks for skipping resolving polymorphic associated entity instances

This commit is contained in:
Marco Belladelli 2023-03-01 15:34:37 +01:00 committed by Christian Beikov
parent 10bfcabee2
commit 8312db1b9e
8 changed files with 457 additions and 42 deletions

View File

@ -78,15 +78,15 @@ public interface Initializer {
default boolean isAttributeAssignableToConcreteDescriptor( default boolean isAttributeAssignableToConcreteDescriptor(
FetchParentAccess parentAccess, FetchParentAccess parentAccess,
AttributeMapping referencedModelPart) { AttributeMapping referencedModelPart) {
if ( parentAccess != null && parentAccess.isEntityInitializer() ) { final EntityInitializer entityInitializer = parentAccess == null ?
final EntityPersister concreteDescriptor = parentAccess.findFirstEntityInitializer() null :
.getConcreteDescriptor(); parentAccess.findFirstEntityInitializer();
if ( entityInitializer != null ) {
final EntityPersister concreteDescriptor = entityInitializer.getConcreteDescriptor();
if ( concreteDescriptor.getEntityMetamodel().isPolymorphic() ) { if ( concreteDescriptor.getEntityMetamodel().isPolymorphic() ) {
final EntityPersister declaringType = (EntityPersister) referencedModelPart.getDeclaringType(); final EntityPersister declaringType = (EntityPersister) referencedModelPart.getDeclaringType();
if ( concreteDescriptor != declaringType ) { if ( concreteDescriptor != declaringType ) {
if ( !declaringType.getSubclassEntityNames().contains( concreteDescriptor.getEntityName() ) ) { return declaringType.getSubclassEntityNames().contains( concreteDescriptor.getEntityName() );
return false;
}
} }
} }
} }

View File

@ -98,7 +98,7 @@ public abstract class AbstractEntityInitializer extends AbstractFetchParentAcces
private EntityKey entityKey; private EntityKey entityKey;
private Object entityInstance; private Object entityInstance;
private Object entityInstanceForNotify; private Object entityInstanceForNotify;
private boolean missing; protected boolean missing;
boolean isInitialized; boolean isInitialized;
private boolean isOwningInitializer; private boolean isOwningInitializer;
private Object[] resolvedEntityState; private Object[] resolvedEntityState;
@ -399,26 +399,44 @@ public abstract class AbstractEntityInitializer extends AbstractFetchParentAcces
} }
} }
private boolean shouldSkipResolveInstance(RowProcessingState rowProcessingState) { protected boolean shouldSkipResolveInstance(RowProcessingState rowProcessingState) {
final NavigablePath parent = navigablePath.getParent(); if ( navigablePath.getParent() != null ) {
if ( parent != null ) { Initializer parentInitializer = rowProcessingState.resolveInitializer( navigablePath.getParent() );
final Initializer parentInitializer = rowProcessingState.resolveInitializer( parent ); if ( parentInitializer != null ) {
if ( parentInitializer != null && parentInitializer.isEntityInitializer() ) { ModelPart modelPart = referencedModelPart;
if ( isReferencedModelPartAssignableToConcreteParent( parentInitializer ) ) { NavigablePath currentNavigablePath = navigablePath;
return true; // Walk back initializers until we find an EntityInitializer
while ( parentInitializer != null && !parentInitializer.isEntityInitializer() ) {
modelPart = parentInitializer.getInitializedPart();
currentNavigablePath = currentNavigablePath.getParent();
parentInitializer = rowProcessingState.resolveInitializer( currentNavigablePath.getParent() );
}
if ( parentInitializer != null && parentInitializer.asEntityInitializer()
.getEntityDescriptor()
.getEntityMetamodel()
.isPolymorphic() ) {
parentInitializer.resolveKey( rowProcessingState );
return isReferencedModelPartInConcreteParent(
modelPart,
currentNavigablePath,
parentInitializer
);
} }
} }
} }
return false; return false;
} }
private boolean isReferencedModelPartAssignableToConcreteParent(Initializer parentInitializer) { private boolean isReferencedModelPartInConcreteParent(
ModelPart modelPart,
NavigablePath partNavigablePath,
Initializer parentInitializer) {
final EntityPersister parentConcreteDescriptor = parentInitializer.asEntityInitializer() final EntityPersister parentConcreteDescriptor = parentInitializer.asEntityInitializer()
.getConcreteDescriptor(); .getConcreteDescriptor();
if ( parentConcreteDescriptor != null && parentConcreteDescriptor.getEntityMetamodel().isPolymorphic() ) { if ( parentConcreteDescriptor != null && parentConcreteDescriptor.getEntityMetamodel().isPolymorphic() ) {
final ModelPart concreteModelPart = parentConcreteDescriptor.findByPath( navigablePath.getLocalName() ); final ModelPart concreteModelPart = parentConcreteDescriptor.findByPath( partNavigablePath.getLocalName() );
if ( concreteModelPart == null if ( concreteModelPart == null
|| !referencedModelPart.getJavaType().getJavaTypeClass() || !modelPart.getJavaType().getJavaTypeClass()
.isAssignableFrom( concreteModelPart.getJavaType().getJavaTypeClass() ) ) { .isAssignableFrom( concreteModelPart.getJavaType().getJavaTypeClass() ) ) {
/* /*
Given: Given:

View File

@ -71,6 +71,11 @@ public class EntityJoinedFetchInitializer extends AbstractEntityInitializer {
@Override @Override
public void resolveKey(RowProcessingState rowProcessingState) { public void resolveKey(RowProcessingState rowProcessingState) {
if ( shouldSkipResolveInstance( rowProcessingState ) ) {
missing = true;
return;
}
super.resolveKey( rowProcessingState ); super.resolveKey( rowProcessingState );
// super processes the foreign-key target column. here we // super processes the foreign-key target column. here we

View File

@ -67,10 +67,8 @@ public class LoadANonExistingNotFoundEntityTest extends BaseNonConfigCoreFunctio
} }
); );
// The Employee#employer must be initialized immediately because // we should get 1 query for the Employee with join
// enhanced proxies (and HibernateProxy objects) should never be created assertEquals( 1, statistics.getPrepareStatementCount() );
// for a "not found" association.
assertEquals( 2, statistics.getPrepareStatementCount() );
} }
@Test @Test
@ -86,10 +84,8 @@ public class LoadANonExistingNotFoundEntityTest extends BaseNonConfigCoreFunctio
} }
); );
// The Employee#employer must be initialized immediately because // we should get 1 query for the Employee with join
// enhanced proxies (and HibernateProxy objects) should never be created assertEquals( 1, statistics.getPrepareStatementCount() );
// for a "not found" association.
assertEquals( 2, statistics.getPrepareStatementCount() );
} }
@Test @Test
@ -178,7 +174,7 @@ public class LoadANonExistingNotFoundEntityTest extends BaseNonConfigCoreFunctio
private String name; private String name;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "employer_id",foreignKey = @ForeignKey(value= ConstraintMode.NO_CONSTRAINT)) @JoinColumn(name = "employer_id",foreignKey = @ForeignKey(value= ConstraintMode.NO_CONSTRAINT))
@NotFound(action=NotFoundAction.IGNORE) @NotFound(action=NotFoundAction.IGNORE)
private Employer employer; private Employer employer;

View File

@ -7,10 +7,6 @@
package org.hibernate.orm.test.inheritance; package org.hibernate.orm.test.inheritance;
import java.util.List; import java.util.List;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode; import jakarta.persistence.ConstraintMode;
import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.DiscriminatorColumn;
@ -307,7 +303,6 @@ public class TransientOverrideAsPersistentMappedSuperclass {
// Editor#title (which uses the same e_title column) can be non-null, // Editor#title (which uses the same e_title column) can be non-null,
// and there is no associated group. // and there is no associated group.
@ManyToOne(optional = false) @ManyToOne(optional = false)
@NotFound(action = NotFoundAction.IGNORE)
@JoinColumn(name = "e_title", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) @JoinColumn(name = "e_title", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
public Group getGroup() { public Group getGroup() {
return group; return group;

View File

@ -7,10 +7,6 @@
package org.hibernate.orm.test.inheritance; package org.hibernate.orm.test.inheritance;
import java.util.List; import java.util.List;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode; import jakarta.persistence.ConstraintMode;
import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.DiscriminatorColumn;
@ -300,7 +296,6 @@ public class TransientOverrideAsPersistentSingleTable {
// Editor#title (which uses the same e_title column) can be non-null, // Editor#title (which uses the same e_title column) can be non-null,
// and there is no associated group. // and there is no associated group.
@ManyToOne(optional = false) @ManyToOne(optional = false)
@NotFound(action = NotFoundAction.IGNORE)
@JoinColumn(name = "e_title", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) @JoinColumn(name = "e_title", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
public Group getGroup() { public Group getGroup() {
return group; return group;

View File

@ -8,10 +8,6 @@ package org.hibernate.orm.test.inheritance;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode; import jakarta.persistence.ConstraintMode;
import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.DiscriminatorColumn;
@ -303,7 +299,6 @@ public class TransientOverrideAsPersistentTablePerClass {
// Editor#title (which uses the same e_title column) can be non-null, // Editor#title (which uses the same e_title column) can be non-null,
// and there is no associated group. // and there is no associated group.
@ManyToOne(optional = false) @ManyToOne(optional = false)
@NotFound(action = NotFoundAction.IGNORE)
@JoinColumn(name = "e_title", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) @JoinColumn(name = "e_title", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
public Group getGroup() { public Group getGroup() {
return group; return group;

View File

@ -0,0 +1,411 @@
/*
* 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.orm.test.inheritance;
import java.util.List;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode;
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.ParameterExpression;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
@TestForIssue(jiraKey = "HHH-14103")
@DomainModel(
annotatedClasses = {
TransientOverrideAsPersistentWithEmbeddable.Employee.class,
TransientOverrideAsPersistentWithEmbeddable.Editor.class,
TransientOverrideAsPersistentWithEmbeddable.Writer.class,
TransientOverrideAsPersistentWithEmbeddable.Group.class,
TransientOverrideAsPersistentWithEmbeddable.Job.class
}
)
@SessionFactory
public class TransientOverrideAsPersistentWithEmbeddable {
@Test
public void testFindByRootClass(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final Employee editor = session.find( Employee.class, "Jane Smith" );
assertNotNull( editor );
assertEquals( "Senior Editor", editor.getTitle() );
final Employee writer = session.find( Employee.class, "John Smith" );
assertThat( writer, instanceOf( Writer.class ) );
assertEquals( "Writing", writer.getTitle() );
assertNotNull( ( (Writer) writer ).getWriterEmbeddable().getGroup() );
final Group group = ( (Writer) writer ).getWriterEmbeddable().getGroup();
assertEquals( writer.getTitle(), group.getName() );
final Job jobEditor = session.find( Job.class, "Edit" );
assertSame( editor, jobEditor.getEmployee() );
final Job jobWriter = session.find( Job.class, "Write" );
assertSame( writer, jobWriter.getEmployee() );
} );
}
@Test
public void testFindBySubclass(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final Editor editor = session.find( Editor.class, "Jane Smith" );
assertNotNull( editor );
assertEquals( "Senior Editor", editor.getTitle() );
final Writer writer = session.find( Writer.class, "John Smith" );
assertEquals( "Writing", writer.getTitle() );
assertNotNull( writer.getWriterEmbeddable().getGroup() );
final Group group = writer.getWriterEmbeddable().getGroup();
assertEquals( writer.getTitle(), group.getName() );
final Job jobEditor = session.find( Job.class, "Edit" );
assertSame( editor, jobEditor.getEmployee() );
final Job jobWriter = session.find( Job.class, "Write" );
assertSame( writer, jobWriter.getEmployee() );
} );
}
@Test
public void testQueryByRootClass(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final List<Employee> employees = session.createQuery( "from Employee", Employee.class )
.getResultList();
assertEquals( 2, employees.size() );
assertThat( employees.get( 0 ), instanceOf( Editor.class ) );
assertThat( employees.get( 1 ), instanceOf( Writer.class ) );
final Editor editor = (Editor) employees.get( 0 );
assertEquals( "Senior Editor", editor.getTitle() );
final Writer writer = (Writer) employees.get( 1 );
assertEquals( "Writing", writer.getTitle() );
assertNotNull( writer.getWriterEmbeddable().getGroup() );
final Group group = writer.getWriterEmbeddable().getGroup();
assertEquals( writer.getTitle(), group.getName() );
} );
}
@Test
public void testQueryByRootClassAndOverridenProperty(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final Employee editor = session.createQuery( "from Employee where title=:title", Employee.class )
.setParameter( "title", "Senior Editor" )
.getSingleResult();
assertThat( editor, instanceOf( Editor.class ) );
final Employee writer = session.createQuery( "from Employee where title=:title", Employee.class )
.setParameter( "title", "Writing" )
.getSingleResult();
assertThat( writer, instanceOf( Writer.class ) );
assertNotNull( ( (Writer) writer ).getWriterEmbeddable().getGroup() );
assertEquals( writer.getTitle(), ( (Writer) writer ).getWriterEmbeddable().getGroup().getName() );
} );
}
@Test
public void testQueryByRootClassAndOverridenPropertyTreat(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final Employee editor = session.createQuery(
"from Employee e where treat( e as Editor ).title=:title",
Employee.class
)
.setParameter( "title", "Senior Editor" )
.getSingleResult();
assertThat( editor, instanceOf( Editor.class ) );
final Employee writer = session.createQuery(
"from Employee e where treat( e as Writer).title=:title",
Employee.class
)
.setParameter( "title", "Writing" )
.getSingleResult();
assertThat( writer, instanceOf( Writer.class ) );
assertNotNull( ( (Writer) writer ).getWriterEmbeddable().getGroup() );
assertEquals( writer.getTitle(), ( (Writer) writer ).getWriterEmbeddable().getGroup().getName() );
} );
}
@Test
public void testQueryBySublassAndOverridenProperty(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final Editor editor = session.createQuery( "from Editor where title=:title", Editor.class )
.setParameter( "title", "Senior Editor" )
.getSingleResult();
assertThat( editor, instanceOf( Editor.class ) );
final Writer writer = session.createQuery( "from Writer where title=:title", Writer.class )
.setParameter( "title", "Writing" )
.getSingleResult();
assertNotNull( writer.getWriterEmbeddable().getGroup() );
assertEquals( writer.getTitle(), writer.getWriterEmbeddable().getGroup().getName() );
} );
}
@Test
public void testCriteriaQueryByRootClassAndOverridenProperty(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final CriteriaBuilder builder = session.getCriteriaBuilder();
final CriteriaQuery<Employee> query = builder.createQuery( Employee.class );
final Root<Employee> root = query.from( Employee.class );
final ParameterExpression<String> parameter = builder.parameter( String.class, "title" );
final Predicate predicateEditor = builder.equal(
builder.treat( root, Editor.class ).get( "title" ),
parameter
);
query.where( predicateEditor );
final Employee editor = session.createQuery( query )
.setParameter( "title", "Senior Editor" )
.getSingleResult();
assertThat( editor, instanceOf( Editor.class ) );
final Predicate predicateWriter = builder.equal(
builder.treat( root, Writer.class ).get( "title" ),
parameter
);
query.where( predicateWriter );
final Employee writer = session.createQuery( query )
.setParameter( "title", "Writing" )
.getSingleResult();
assertThat( writer, instanceOf( Writer.class ) );
assertNotNull( ( (Writer) writer ).getWriterEmbeddable().getGroup() );
assertEquals( writer.getTitle(), ( (Writer) writer ).getWriterEmbeddable().getGroup().getName() );
} );
}
@BeforeEach
public void setupData(SessionFactoryScope scope) {
scope.inTransaction( session -> {
Job jobEditor = new Job( "Edit" );
jobEditor.setEmployee( new Editor( "Jane Smith", "Senior Editor" ) );
Job jobWriter = new Job( "Write" );
jobWriter.setEmployee( new Writer( "John Smith", new Group( "Writing" ) ) );
Employee editor = jobEditor.getEmployee();
Employee writer = jobWriter.getEmployee();
Group group = Writer.class.cast( writer ).getWriterEmbeddable().getGroup();
session.persist( editor );
session.persist( group );
session.persist( writer );
session.persist( jobEditor );
session.persist( jobWriter );
} );
}
@AfterEach
public void cleanupData(SessionFactoryScope scope) {
scope.inTransaction( session -> {
session.createQuery( "delete from Job" ).executeUpdate();
session.createQuery( "delete from Employee" ).executeUpdate();
session.createQuery( "delete from Group" ).executeUpdate();
} );
}
@MappedSuperclass
public static class AbstractEmployee {
private String title;
@Transient
public String getTitle() {
return title;
}
protected void setTitle(String title) {
this.title = title;
}
}
@Entity(name = "Employee")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "department")
public static abstract class Employee extends AbstractEmployee {
private String name;
protected Employee(String name) {
this();
setName( name );
}
@Id
public String getName() {
return name;
}
protected Employee() {
// this form used by Hibernate
}
protected void setName(String name) {
this.name = name;
}
}
@Entity(name = "Editor")
public static class Editor extends Employee {
public Editor(String name, String title) {
super( name );
setTitle( title );
}
@Column(name = "e_title")
public String getTitle() {
return super.getTitle();
}
public void setTitle(String title) {
super.setTitle( title );
}
protected Editor() {
// this form used by Hibernate
super();
}
}
@Embeddable
public static class WriterEmbeddable {
private Group group;
// Cannot have a constraint on e_title because
// Editor#title (which uses the same e_title column) can be non-null,
// and there is no associated group.
@ManyToOne(optional = false)
@JoinColumn(name = "e_title", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
public Group getGroup() {
return group;
}
public void setGroup(Group group) {
this.group = group;
}
}
@Entity(name = "Writer")
public static class Writer extends Employee {
private WriterEmbeddable writerEmbeddable;
public Writer(String name, Group group) {
super( name );
this.writerEmbeddable = new WriterEmbeddable();
setGroup( group );
}
@Column(name = "e_title", insertable = false, updatable = false)
public String getTitle() {
return super.getTitle();
}
public void setTitle(String title) {
super.setTitle( title );
}
protected Writer() {
// this form used by Hibernate
super();
}
protected void setGroup(Group group) {
this.writerEmbeddable.setGroup( group );
setTitle( group.getName() );
}
public WriterEmbeddable getWriterEmbeddable() {
return writerEmbeddable;
}
public void setWriterEmbeddable(WriterEmbeddable writerEmbeddable) {
this.writerEmbeddable = writerEmbeddable;
}
}
@Entity(name = "Group")
@Table(name = "WorkGroup")
public static class Group {
private String name;
private String desctiption;
public Group(String name) {
this();
setName( name );
}
@Id
public String getName() {
return name;
}
protected Group() {
// this form used by Hibernate
}
protected void setName(String name) {
this.name = name;
}
}
@Entity(name = "Job")
public static class Job {
private String name;
private Employee employee;
private String description;
public Job(String name) {
this();
setName( name );
}
@Id
public String getName() {
return name;
}
@OneToOne
@JoinColumn(name = "employee_name")
public Employee getEmployee() {
return employee;
}
protected Job() {
// this form used by Hibernate
}
protected void setName(String name) {
this.name = name;
}
protected void setEmployee(Employee e) {
this.employee = e;
}
}
}