HHH-13241 : Constraint violation when deleting entites in bi-directional, lazy OneToMany association with bytecode enhancement
This commit is contained in:
parent
69a1c2cc08
commit
980f24916c
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 );
|
||||
|
||||
|
|
|
@ -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] );
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue