HHH-13241 : Constraint violation when deleting entites in bi-directional, lazy OneToMany association with bytecode enhancement

This commit is contained in:
Gail Badner 2019-02-26 22:24:54 -08:00 committed by gbadner
parent 69a1c2cc08
commit 980f24916c
7 changed files with 889 additions and 26 deletions

View File

@ -110,8 +110,8 @@ public abstract class AbstractEntityInsertAction extends EntityAction {
*/
protected final void nullifyTransientReferencesIfNotAlready() {
if ( ! areTransientReferencesNullified ) {
new ForeignKeys.Nullifier( getInstance(), false, isEarlyInsert(), getSession() )
.nullifyTransientReferences( getState(), getPersister().getPropertyTypes() );
new ForeignKeys.Nullifier( getInstance(), false, isEarlyInsert(), getSession(), getPersister() )
.nullifyTransientReferences( getState() );
new Nullability( getSession() ).checkNullability( getState(), getPersister(), false );
areTransientReferencesNullified = true;
}

View File

@ -13,7 +13,9 @@ import org.hibernate.HibernateException;
import org.hibernate.TransientObjectException;
import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.SelfDirtinessTracker;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
@ -36,6 +38,7 @@ public final class ForeignKeys {
private final boolean isEarlyInsert;
private final SharedSessionContractImplementor session;
private final Object self;
private final EntityPersister persister;
/**
* Constructs a Nullifier
@ -44,11 +47,18 @@ public final class ForeignKeys {
* @param isDelete Are we in the middle of a delete action?
* @param isEarlyInsert Is this an early insert (INSERT generated id strategy)?
* @param session The session
* @param persister The EntityPersister for {@code self}
*/
public Nullifier(Object self, boolean isDelete, boolean isEarlyInsert, SharedSessionContractImplementor session) {
public Nullifier(
final Object self,
final boolean isDelete,
final boolean isEarlyInsert,
final SharedSessionContractImplementor session,
final EntityPersister persister) {
this.isDelete = isDelete;
this.isEarlyInsert = isEarlyInsert;
this.session = session;
this.persister = persister;
this.self = self;
}
@ -57,11 +67,12 @@ public final class ForeignKeys {
* points toward that entity.
*
* @param values The entity attribute values
* @param types The entity attribute types
*/
public void nullifyTransientReferences(final Object[] values, final Type[] types) {
public void nullifyTransientReferences(final Object[] values) {
final String[] propertyNames = persister.getPropertyNames();
final Type[] types = persister.getPropertyTypes();
for ( int i = 0; i < types.length; i++ ) {
values[i] = nullifyTransientReferences( values[i], types[i] );
values[i] = nullifyTransientReferences( values[i], propertyNames[i], types[i] );
}
}
@ -70,34 +81,47 @@ public final class ForeignKeys {
* input argument otherwise. This is how Hibernate avoids foreign key constraint violations.
*
* @param value An entity attribute value
* @param propertyName An entity attribute name
* @param type An entity attribute type
*
* @return {@code null} if the argument is an unsaved entity; otherwise return the argument.
*/
private Object nullifyTransientReferences(final Object value, final Type type) {
private Object nullifyTransientReferences(final Object value, final String propertyName, final Type type) {
final Object returnedValue;
if ( value == null ) {
return null;
returnedValue = null;
}
else if ( type.isEntityType() ) {
final EntityType entityType = (EntityType) type;
if ( entityType.isOneToOne() ) {
return value;
returnedValue = value;
}
else {
final String entityName = entityType.getAssociatedEntityName();
return isNullifiable( entityName, value ) ? null : value;
// If value is lazy, it may need to be initialized to
// determine if the value is nullifiable.
final Object possiblyInitializedValue = initializeIfNecessary( value, propertyName, entityType );
// If the value is not nullifiable, make sure that the
// possibly initialized value is returned.
returnedValue = isNullifiable( entityType.getAssociatedEntityName(), possiblyInitializedValue )
? null
: possiblyInitializedValue;
}
}
else if ( type.isAnyType() ) {
return isNullifiable( null, value ) ? null : value;
returnedValue = isNullifiable( null, value ) ? null : value;
}
else if ( type.isComponentType() ) {
final CompositeType actype = (CompositeType) type;
final Object[] subvalues = actype.getPropertyValues( value, session );
final Type[] subtypes = actype.getSubtypes();
final String[] subPropertyNames = actype.getPropertyNames();
boolean substitute = false;
for ( int i = 0; i < subvalues.length; i++ ) {
final Object replacement = nullifyTransientReferences( subvalues[i], subtypes[i] );
final Object replacement = nullifyTransientReferences(
subvalues[i],
StringHelper.qualify( propertyName, subPropertyNames[i] ),
subtypes[i]
);
if ( replacement != subvalues[i] ) {
substitute = true;
subvalues[i] = replacement;
@ -107,7 +131,50 @@ public final class ForeignKeys {
// todo : need to account for entity mode on the CompositeType interface :(
actype.setPropertyValues( value, subvalues, EntityMode.POJO );
}
return value;
returnedValue = value;
}
else {
returnedValue = value;
}
// value != returnedValue if either:
// 1) returnedValue was nullified (set to null);
// or 2) returnedValue was initialized, but not nullified.
// When bytecode-enhancement is used for dirty-checking, the change should
// only be tracked when returnedValue was nullified (1)).
if ( value != returnedValue && returnedValue == null && SelfDirtinessTracker.class.isInstance( self ) ) {
( (SelfDirtinessTracker) self ).$$_hibernate_trackChange( propertyName );
}
return returnedValue;
}
private Object initializeIfNecessary(
final Object value,
final String propertyName,
final Type type) {
if ( isDelete &&
value == LazyPropertyInitializer.UNFETCHED_PROPERTY &&
type.isEntityType() &&
!session.getPersistenceContext().getNullifiableEntityKeys().isEmpty() ) {
// IMPLEMENTATION NOTE: If cascade-remove was mapped for the attribute,
// then value should have been initialized previously, when the remove operation was
// cascaded to the property (because CascadingAction.DELETE.performOnLazyProperty()
// returns true). This particular situation can only arise when cascade-remove is not
// mapped for the association.
// There is at least one nullifiable entity. We don't know if the lazy
// associated entity is one of the nullifiable entities. If it is, and
// the property is not nullified, then a constraint violation will result.
// The only way to find out if the associated entity is nullifiable is
// to initialize it.
// TODO: there may be ways to fine-tune when initialization is necessary
// (e.g., only initialize when the associated entity type is a
// superclass or the same as the entity type of a nullifiable entity).
// It is unclear if a more complicated check would impact performance
// more than just initializing the associated entity.
return persister
.getInstrumentationMetadata()
.extractInterceptor( self )
.fetchAttribute( self, propertyName );
}
else {
return value;
@ -162,9 +229,7 @@ public final class ForeignKeys {
else {
return entityEntry.isNullifiable( isEarlyInsert, session );
}
}
}
/**
@ -307,8 +372,8 @@ public final class ForeignKeys {
Object[] values,
boolean isEarlyInsert,
SharedSessionContractImplementor session) {
final Nullifier nullifier = new Nullifier( entity, false, isEarlyInsert, session );
final EntityPersister persister = session.getEntityPersister( entityName, entity );
final Nullifier nullifier = new Nullifier( entity, false, isEarlyInsert, session, persister );
final String[] propertyNames = persister.getPropertyNames();
final Type[] types = persister.getPropertyTypes();
final boolean[] nullability = persister.getPropertyNullability();

View File

@ -285,8 +285,7 @@ public class DefaultDeleteEventListener implements DeleteEventListener, Callback
cascadeBeforeDelete( session, persister, entity, entityEntry, transientEntities );
new ForeignKeys.Nullifier( entity, true, false, session )
.nullifyTransientReferences( entityEntry.getDeletedState(), propTypes );
new ForeignKeys.Nullifier( entity, true, false, session, persister ).nullifyTransientReferences( entityEntry.getDeletedState() );
new Nullability( session ).checkNullability( entityEntry.getDeletedState(), persister, Nullability.NullabilityCheckType.DELETE );
persistenceContext.getNullifiableEntityKeys().add( key );

View File

@ -1181,7 +1181,6 @@ public abstract class AbstractEntityPersister
rs = session.getJdbcCoordinator().getResultSetReturn().extract( ps );
rs.next();
}
final Object[] snapshot = entry.getLoadedState();
for ( LazyAttributeDescriptor fetchGroupAttributeDescriptor : fetchGroupAttributeDescriptors ) {
final boolean previousInitialized = initializedLazyAttributeNames.contains( fetchGroupAttributeDescriptor.getName() );
@ -1212,7 +1211,7 @@ public abstract class AbstractEntityPersister
fieldName,
entity,
session,
snapshot,
entry,
fetchGroupAttributeDescriptor.getLazyIndex(),
selectedValue
);
@ -1261,7 +1260,6 @@ public abstract class AbstractEntityPersister
Object result = null;
Serializable[] disassembledValues = cacheEntry.getDisassembledState();
final Object[] snapshot = entry.getLoadedState();
for ( int j = 0; j < lazyPropertyNames.length; j++ ) {
final Serializable cachedValue = disassembledValues[lazyPropertyNumbers[j]];
final Type lazyPropertyType = lazyPropertyTypes[j];
@ -1278,7 +1276,7 @@ public abstract class AbstractEntityPersister
session,
entity
);
if ( initializeLazyProperty( fieldName, entity, session, snapshot, j, propValue ) ) {
if ( initializeLazyProperty( fieldName, entity, session, entry, j, propValue ) ) {
result = propValue;
}
}
@ -1293,13 +1291,17 @@ public abstract class AbstractEntityPersister
final String fieldName,
final Object entity,
final SharedSessionContractImplementor session,
final Object[] snapshot,
final EntityEntry entry,
final int j,
final Object propValue) {
setPropertyValue( entity, lazyPropertyNumbers[j], propValue );
if ( snapshot != null ) {
if ( entry.getLoadedState() != null ) {
// object have been loaded with setReadOnly(true); HHH-2236
snapshot[lazyPropertyNumbers[j]] = lazyPropertyTypes[j].deepCopy( propValue, factory );
entry.getLoadedState()[lazyPropertyNumbers[j]] = lazyPropertyTypes[j].deepCopy( propValue, factory );
}
// If the entity has deleted state, then update that as well
if ( entry.getDeletedState() != null ) {
entry.getDeletedState()[lazyPropertyNumbers[j]] = lazyPropertyTypes[j].deepCopy( propValue, factory );
}
return fieldName.equals( lazyPropertyNames[j] );
}

View File

@ -0,0 +1,354 @@
/*
* 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.bytecode.enhancement.lazy;
import java.util.HashSet;
import java.util.Set;
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.Session;
import org.hibernate.annotations.LazyToOne;
import org.hibernate.annotations.LazyToOneOption;
import org.hibernate.bytecode.enhance.spi.DefaultEnhancementContext;
import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer;
import org.hibernate.bytecode.enhance.spi.UnloadedClass;
import org.hibernate.bytecode.enhance.spi.UnloadedField;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner;
import org.hibernate.testing.bytecode.enhancement.CustomEnhancementContext;
import org.hibernate.testing.bytecode.enhancement.EnhancerTestContext;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
/**
* Tests removing non-owning side of the bidirectional association,
* with and without dirty-checking using enhancement.
*
* @author Gail Badner
*/
@TestForIssue(jiraKey = "HHH-13241")
@RunWith(BytecodeEnhancerRunner.class)
@CustomEnhancementContext({
EnhancerTestContext.class, // supports laziness and dirty-checking
BidirectionalLazyTest.NoDirtyCheckEnhancementContext.class // supports laziness; does not support dirty-checking
})
public class BidirectionalLazyTest extends BaseCoreFunctionalTestCase {
public Class<?>[] getAnnotatedClasses() {
return new Class[] { Employer.class, Employee.class, Unrelated.class };
}
@Test
public void testRemoveWithDeletedAssociatedEntity() {
doInHibernate(
this::sessionFactory, session -> {
Employer employer = new Employer( "RedHat" );
session.persist( employer );
employer.addEmployee( new Employee( "Jack" ) );
employer.addEmployee( new Employee( "Jill" ) );
employer.addEmployee( new Employee( "John" ) );
for ( Employee employee : employer.getEmployees() ) {
session.persist( employee );
}
}
);
doInHibernate(
this::sessionFactory, session -> {
Employer employer = session.get( Employer.class, "RedHat" );
// Delete the associated entity first
session.remove( employer );
for ( Employee employee : employer.getEmployees() ) {
assertFalse( Hibernate.isPropertyInitialized( employee, "employer" ) );
session.remove( employee );
// Should be initialized because at least one entity was deleted beforehand
assertTrue( Hibernate.isPropertyInitialized( employee, "employer" ) );
assertSame( employer, employee.getEmployer() );
// employee.getEmployer was initialized, and should be nullified in EntityEntry#deletedState
checkEntityEntryState( session, employee, employer, true );
}
}
);
doInHibernate(
this::sessionFactory, session -> {
assertNull( session.find( Employer.class, "RedHat" ) );
assertTrue( session.createQuery( "from Employee e", Employee.class ).getResultList().isEmpty() );
}
);
}
@Test
public void testRemoveWithNonAssociatedRemovedEntity() {
doInHibernate(
this::sessionFactory, session -> {
Employer employer = new Employer( "RedHat" );
session.persist( employer );
Employee employee = new Employee( "Jack" );
employer.addEmployee( employee );
session.persist( employee );
session.persist( new Unrelated( 1 ) );
}
);
doInHibernate(
this::sessionFactory, session -> {
// Delete an entity that is not associated with Employee
session.remove( session.get( Unrelated.class, 1 ) );
final Employee employee = session.get( Employee.class, "Jack" );
assertFalse( Hibernate.isPropertyInitialized( employee, "employer" ) );
session.remove( employee );
// Should be initialized because at least one entity was deleted beforehand
assertTrue( Hibernate.isPropertyInitialized( employee, "employer" ) );
// employee.getEmployer was initialized, and should not be nullified in EntityEntry#deletedState
checkEntityEntryState( session, employee, employee.getEmployer(), false );
}
);
doInHibernate(
this::sessionFactory, session -> {
assertNull( session.find( Unrelated.class, 1 ) );
assertNull( session.find( Employee.class, "Jack" ) );
session.remove( session.find( Employer.class, "RedHat" ) );
}
);
}
@Test
public void testRemoveWithNoRemovedEntities() {
doInHibernate(
this::sessionFactory, session -> {
Employer employer = new Employer( "RedHat" );
session.persist( employer );
Employee employee = new Employee( "Jack" );
employer.addEmployee( employee );
session.persist( employee );
}
);
doInHibernate(
this::sessionFactory, session -> {
// Don't delete any entities before deleting the Employee
final Employee employee = session.get( Employee.class, "Jack" );
assertFalse( Hibernate.isPropertyInitialized( employee, "employer" ) );
session.remove( employee );
// There were no other deleted entities before employee was deleted,
// so there was no need to initialize employee.employer.
assertFalse( Hibernate.isPropertyInitialized( employee, "employer" ) );
// employee.getEmployer was not initialized, and should not be nullified in EntityEntry#deletedState
checkEntityEntryState( session, employee, LazyPropertyInitializer.UNFETCHED_PROPERTY, false );
}
);
doInHibernate(
this::sessionFactory, session -> {
assertNull( session.find( Employee.class, "Jack" ) );
session.remove( session.find( Employer.class, "RedHat" ) );
}
);
}
private void checkEntityEntryState(
final Session session,
final Employee employee,
final Object employer,
final boolean isEmployerNullified) {
final SessionImplementor sessionImplementor = (SessionImplementor) session;
final EntityEntry entityEntry = sessionImplementor.getPersistenceContext().getEntry( employee );
final int propertyNumber = entityEntry.getPersister().getEntityMetamodel().getPropertyIndex( "employer" );
assertEquals(
employer,
entityEntry.getLoadedState()[propertyNumber]
);
if ( isEmployerNullified ) {
assertEquals( null, entityEntry.getDeletedState()[propertyNumber] );
}
else {
assertEquals(
employer,
entityEntry.getDeletedState()[propertyNumber]
);
}
}
@Entity(name = "Employer")
public static class Employer {
private String name;
private Set<Employee> employees;
public Employer(String name) {
this();
setName( name );
}
@Id
public String getName() {
return name;
}
@OneToMany(mappedBy = "employer", fetch = FetchType.LAZY)
public Set<Employee> getEmployees() {
return employees;
}
public void addEmployee(Employee employee) {
if ( getEmployees() == null ) {
setEmployees( new HashSet<>() );
}
employees.add( employee );
employee.setEmployer( this );
}
protected Employer() {
// this form used by Hibernate
}
protected void setName(String name) {
this.name = name;
}
protected void setEmployees(Set<Employee> employees) {
this.employees = employees;
}
}
@Entity(name = "Employee")
public static class Employee {
private long id;
private String name;
private Employer employer;
public Employee(String name) {
this();
setName( name );
}
public long getId() {
return id;
}
@Id
public String getName() {
return name;
}
@ManyToOne(fetch = FetchType.LAZY)
@LazyToOne(LazyToOneOption.NO_PROXY)
@JoinColumn(name = "employer_name")
public Employer getEmployer() {
return employer;
}
protected Employee() {
// this form used by Hibernate
}
protected void setId(long id) {
this.id = id;
}
protected void setName(String name) {
this.name = name;
}
protected void setEmployer(Employer employer) {
this.employer = employer;
}
public int hashCode() {
if ( name != null ) {
return name.hashCode();
}
else {
return 0;
}
}
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
else if ( o instanceof Employee ) {
Employee other = Employee.class.cast( o );
if ( name != null ) {
return getName().equals( other.getName() );
}
else {
return other.getName() == null;
}
}
else {
return false;
}
}
}
@Entity(name = "Manager")
public static class Manager extends Employee {
}
@Entity(name = "Unrelated")
public static class Unrelated {
private int id;
public Unrelated() {
}
public Unrelated(int id) {
setId( id );
}
@Id
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
public static class NoDirtyCheckEnhancementContext extends DefaultEnhancementContext {
@Override
public boolean hasLazyLoadableAttributes(UnloadedClass classDescriptor) {
return true;
}
@Override
public boolean isLazyLoadable(UnloadedField field) {
return true;
}
@Override
public boolean doDirtyCheckingInline(UnloadedClass classDescriptor) {
return false;
}
}
}

View File

@ -0,0 +1,225 @@
/*
* 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.bytecode.enhancement.lazy.group;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import org.hibernate.annotations.LazyGroup;
import org.hibernate.annotations.LazyToOne;
import org.hibernate.annotations.LazyToOneOption;
import org.hibernate.bytecode.enhance.spi.DefaultEnhancementContext;
import org.hibernate.bytecode.enhance.spi.UnloadedClass;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner;
import org.hibernate.testing.bytecode.enhancement.CustomEnhancementContext;
import org.hibernate.testing.bytecode.enhancement.EnhancerTestContext;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
/**
* Tests removing non-owning side of the bidirectional association,
* where owning side is in an embeddable.
*
* Tests with and without dirty-checking using enhancement.
*
* @author Gail Badner
*/
@TestForIssue(jiraKey = "HHH-13241")
@RunWith(BytecodeEnhancerRunner.class)
@CustomEnhancementContext({
EnhancerTestContext.class,
BidirectionalLazyGroupsInEmbeddableTest.NoDirtyCheckEnhancementContext.class
})
public class BidirectionalLazyGroupsInEmbeddableTest extends BaseCoreFunctionalTestCase {
public Class<?>[] getAnnotatedClasses() {
return new Class[] { Employer.class, Employee.class };
}
@Test
public void test() {
doInHibernate(
this::sessionFactory, session -> {
Employer employer = new Employer( "RedHat" );
session.persist( employer );
employer.addEmployee( new Employee( "Jack" ) );
employer.addEmployee( new Employee( "Jill" ) );
employer.addEmployee( new Employee( "John" ) );
for ( Employee employee : employer.getEmployees() ) {
session.persist( employee );
}
}
);
doInHibernate(
this::sessionFactory, session -> {
Employer employer = session.createQuery( "from Employer e", Employer.class ).getSingleResult();
session.remove( employer );
for ( Employee employee : employer.getEmployees() ) {
session.remove( employee );
}
}
);
}
@Entity(name = "Employer")
public static class Employer {
private String name;
private Set<Employee> employees;
public Employer(String name) {
this();
setName( name );
}
@Id
public String getName() {
return name;
}
@OneToMany(mappedBy = "employerContainer.employer", fetch = FetchType.LAZY)
@LazyGroup("Employees")
public Set<Employee> getEmployees() {
return employees;
}
public void addEmployee(Employee employee) {
if ( getEmployees() == null ) {
setEmployees( new HashSet<>() );
}
employees.add( employee );
if ( employee.getEmployerContainer() == null ) {
employee.setEmployerContainer( new EmployerContainer() );
}
employee.getEmployerContainer().setEmployer( this );
}
protected Employer() {
// this form used by Hibernate
}
protected void setName(String name) {
this.name = name;
}
protected void setEmployees(Set<Employee> employees) {
this.employees = employees;
}
}
@Entity(name = "Employee")
public static class Employee {
private long id;
private String name;
public Employee(String name) {
this();
setName( name );
}
@Id
@GeneratedValue
public long getId() {
return id;
}
public String getName() {
return name;
}
public EmployerContainer employerContainer;
protected Employee() {
// this form used by Hibernate
}
public EmployerContainer getEmployerContainer() {
return employerContainer;
}
protected void setId(long id) {
this.id = id;
}
protected void setName(String name) {
this.name = name;
}
protected void setEmployerContainer(EmployerContainer employerContainer) {
this.employerContainer = employerContainer;
}
public int hashCode() {
if ( name != null ) {
return name.hashCode();
}
else {
return 0;
}
}
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
else if ( o instanceof Employee ) {
Employee other = Employee.class.cast( o );
if ( name != null ) {
return getName().equals( other.getName() );
}
else {
return other.getName() == null;
}
}
else {
return false;
}
}
}
@Embeddable
public static class EmployerContainer {
private Employer employer;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
@LazyToOne(LazyToOneOption.NO_PROXY)
@LazyGroup("EmployerForEmployee")
@JoinColumn(name = "employer_name")
public Employer getEmployer() {
return employer;
}
protected void setEmployer(Employer employer) {
this.employer = employer;
}
}
public static class NoDirtyCheckEnhancementContext extends DefaultEnhancementContext {
@Override
public boolean doDirtyCheckingInline(UnloadedClass classDescriptor) {
return false;
}
}
}

View File

@ -0,0 +1,218 @@
/*
* 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.bytecode.enhancement.lazy.group;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import org.hibernate.Hibernate;
import org.hibernate.annotations.LazyGroup;
import org.hibernate.annotations.LazyToOne;
import org.hibernate.annotations.LazyToOneOption;
import org.hibernate.bytecode.enhance.spi.DefaultEnhancementContext;
import org.hibernate.bytecode.enhance.spi.UnloadedClass;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner;
import org.hibernate.testing.bytecode.enhancement.CustomEnhancementContext;
import org.hibernate.testing.bytecode.enhancement.EnhancerTestContext;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
* Tests removing non-owning side of the bidirectional association,
* with and without dirty-checking using enhancement.
*
* @author Gail Badner
*/
@TestForIssue(jiraKey = "HHH-13241")
@RunWith(BytecodeEnhancerRunner.class)
@CustomEnhancementContext({
EnhancerTestContext.class,
BidirectionalLazyGroupsTest.NoDirtyCheckEnhancementContext.class
})
public class BidirectionalLazyGroupsTest extends BaseCoreFunctionalTestCase {
public Class<?>[] getAnnotatedClasses() {
return new Class[] { Employer.class, Employee.class };
}
@Test
public void testRemoveCollectionOwnerNoCascade() {
doInHibernate(
this::sessionFactory, session -> {
Employer employer = new Employer( "RedHat" );
session.persist( employer );
employer.addEmployee( new Employee( "Jack" ) );
employer.addEmployee( new Employee( "Jill" ) );
employer.addEmployee( new Employee( "John" ) );
for ( Employee employee : employer.getEmployees() ) {
session.persist( employee );
}
}
);
doInHibernate(
this::sessionFactory, session -> {
Employer employer = session.createQuery( "from Employer e", Employer.class ).getSingleResult();
session.remove( employer );
for ( Employee employee : employer.getEmployees() ) {
assertFalse( Hibernate.isPropertyInitialized( employee, "employer") );
session.remove( employee );
assertTrue( Hibernate.isPropertyInitialized( employee, "employer" ) );
}
}
);
doInHibernate(
this::sessionFactory, session -> {
assertNull( session.find( Employer.class, "RedHat" ) );
assertNull( session.createQuery( "from Employee e", Employee.class ).uniqueResult() );
}
);
}
@Entity(name = "Employer")
public static class Employer {
private String name;
private Set<Employee> employees;
public Employer(String name) {
this();
setName( name );
}
@Id
public String getName() {
return name;
}
@OneToMany(mappedBy = "employer", fetch = FetchType.LAZY)
@LazyGroup("Employees")
public Set<Employee> getEmployees() {
return employees;
}
public void addEmployee(Employee employee) {
if ( getEmployees() == null ) {
setEmployees( new HashSet<>() );
}
employees.add( employee );
employee.setEmployer( this );
}
protected Employer() {
// this form used by Hibernate
}
protected void setName(String name) {
this.name = name;
}
protected void setEmployees(Set<Employee> employees) {
this.employees = employees;
}
}
@Entity(name = "Employee")
public static class Employee {
private long id;
private String name;
private Employer employer;
public Employee(String name) {
this();
setName( name );
}
@Id
@GeneratedValue
public long getId() {
return id;
}
public String getName() {
return name;
}
@ManyToOne(fetch = FetchType.LAZY)
@LazyToOne(LazyToOneOption.NO_PROXY)
@LazyGroup("EmployerForEmployee")
@JoinColumn(name = "employer_name")
public Employer getEmployer() {
return employer;
}
protected Employee() {
// this form used by Hibernate
}
protected void setId(long id) {
this.id = id;
}
protected void setName(String name) {
this.name = name;
}
protected void setEmployer(Employer employer) {
this.employer = employer;
}
public int hashCode() {
if ( name != null ) {
return name.hashCode();
}
else {
return 0;
}
}
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
else if ( o instanceof Employee ) {
Employee other = Employee.class.cast( o );
if ( name != null ) {
return getName().equals( other.getName() );
}
else {
return other.getName() == null;
}
}
else {
return false;
}
}
}
public static class NoDirtyCheckEnhancementContext extends DefaultEnhancementContext {
@Override
public boolean doDirtyCheckingInline(UnloadedClass classDescriptor) {
return false;
}
}
}