challenge

This commit is contained in:
Steve Ebersole 2017-11-27 15:29:52 -06:00
parent 99428251c4
commit a4d9841915
9 changed files with 331 additions and 9 deletions

View File

@ -17,6 +17,7 @@ import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterc
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.engine.spi.CascadeStyle;
import org.hibernate.engine.spi.CascadingAction;
import org.hibernate.engine.spi.CascadingActions;
import org.hibernate.engine.spi.CollectionEntry;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.Status;
@ -72,9 +73,12 @@ public final class Cascade {
* which is specific to each CascadingAction type
*/
public static void cascade(
final CascadingAction action, final CascadePoint cascadePoint,
final EventSource eventSource, final EntityPersister persister, final Object parent, final Object anything)
throws HibernateException {
final CascadingAction action,
final CascadePoint cascadePoint,
final EventSource eventSource,
final EntityPersister persister,
final Object parent,
final Object anything) throws HibernateException {
if ( persister.hasCascades() || action.requiresNoCascadeChecking() ) { // performance opt
final boolean traceEnabled = LOG.isTraceEnabled();
@ -138,6 +142,10 @@ public final class Cascade {
);
}
else {
if ( action == CascadingActions.DELETE ) {
}
if ( action.requiresNoCascadeChecking() ) {
action.noCascade(
eventSource,

View File

@ -14,6 +14,7 @@ import org.hibernate.TransientObjectException;
import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.engine.spi.Status;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
@ -159,6 +160,9 @@ public final class ForeignKeys {
if ( entityEntry == null ) {
return isTransient( entityName, object, null, session );
}
else if ( isDelete && ( entityEntry.getStatus() == Status.DELETED || entityEntry.getStatus() == Status.GONE ) ) {
return false;
}
else {
return entityEntry.isNullifiable( isEarlyInsert, session );
}

View File

@ -11,11 +11,14 @@ import java.util.Iterator;
import org.hibernate.HibernateException;
import org.hibernate.PropertyValueException;
import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer;
import org.hibernate.engine.spi.CascadeStyle;
import org.hibernate.engine.spi.CascadingActions;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.engine.spi.Status;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.type.CollectionType;
import org.hibernate.type.CompositeType;
import org.hibernate.type.EntityType;
import org.hibernate.type.Type;
/**
@ -49,7 +52,21 @@ public final class Nullability {
public void checkNullability(
final Object[] values,
final EntityPersister persister,
final boolean isUpdate) throws HibernateException {
final boolean isUpdate) {
checkNullability( values, persister, isUpdate ? NullabilityCheckType.UPDATE : NullabilityCheckType.CREATE );
}
public enum NullabilityCheckType {
CREATE,
UPDATE,
DELETE
}
public void checkNullability(
final Object[] values,
final EntityPersister persister,
final NullabilityCheckType checkType) {
/*
* Typically when Bean Validation is on, we don't want to validate null values
* at the Hibernate Core level. Hence the checkNullability setting.
@ -60,7 +77,7 @@ public final class Nullability {
* Check for any level one nullability breaks
* Look at non null components to
* recursively check next level of nullability breaks
* Look at Collections contraining component to
* Look at Collections containing components to
* recursively check next level of nullability breaks
*
*
@ -74,9 +91,9 @@ public final class Nullability {
*/
final boolean[] nullability = persister.getPropertyNullability();
final boolean[] checkability = isUpdate ?
persister.getPropertyUpdateability() :
persister.getPropertyInsertability();
final boolean[] checkability = checkType == NullabilityCheckType.CREATE
? persister.getPropertyInsertability()
: persister.getPropertyUpdateability();
final Type[] propertyTypes = persister.getPropertyTypes();
for ( int i = 0; i < values.length; i++ ) {
@ -85,6 +102,22 @@ public final class Nullability {
final Object value = values[i];
if ( !nullability[i] && value == null ) {
// generally speaking this is an exception as we will throw below
// but first, we check for a special condition in which:
// 1) we are processing a delete
// 2) the property represents the "other side" of this association
// 3) the other side is currently in the "being deleted" status
// 4) the property is defined to cascade deletes from the other side to
// this association property
//
// in such a case we allow this to continue
if ( checkType == NullabilityCheckType.DELETE ) {
if ( propertyTypes[i].isEntityType() ) {
final EntityType associationType = (EntityType) propertyTypes[i];
}
}
//check basic level one nullablilty
throw new PropertyValueException(
"not-null property references a null or transient value",

View File

@ -258,7 +258,7 @@ public class DefaultDeleteEventListener implements DeleteEventListener {
new ForeignKeys.Nullifier( entity, true, false, session )
.nullifyTransientReferences( entityEntry.getDeletedState(), propTypes );
new Nullability( session ).checkNullability( entityEntry.getDeletedState(), persister, true );
new Nullability( session ).checkNullability( entityEntry.getDeletedState(), persister, Nullability.NullabilityCheckType.DELETE );
persistenceContext.getNullifiableEntityKeys().add( key );
if ( isOrphanRemovalBeforeUpdates ) {

View File

@ -0,0 +1,68 @@
/*
* 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.jpa.compliance.joincolumn;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;
/**
* @author Steve Ebersole
*/
@Entity
public class Company {
private Integer id;
private String name;
private Set<Location> locations;
public Company() {
}
public Company(Integer id, String name) {
this.id = id;
this.name = name;
}
public void addLocation(Location location) {
if ( locations == null ) {
locations = new HashSet<>();
}
locations.add( location );
location.setCompany( this );
}
@Id
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@OneToMany(cascade = CascadeType.PERSIST, mappedBy = "company")
public Set<Location> getLocations() {
return locations == null ? Collections.emptySet() : locations;
}
public void setLocations(Set<Location> locations) {
this.locations = locations;
}
}

View File

@ -0,0 +1,116 @@
/*
* 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.jpa.compliance.joincolumn;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.testing.FailureExpected;
import org.hibernate.testing.RequiresDialect;
import org.hibernate.testing.junit4.BaseUnitTestCase;
import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil2.inTransaction;
/**
* @author Steve Ebersole
*/
@RequiresDialect(H2Dialect.class )
@FailureExpected( jiraKey = "tck-challenge" )
public class JoinColumnTest extends BaseUnitTestCase {
@Test
public void testIt() {
final StandardServiceRegistry ssr = new StandardServiceRegistryBuilder()
.applySetting( AvailableSettings.HBM2DDL_AUTO, "create-drop" )
.build();
try {
try (SessionFactoryImplementor sf = (SessionFactoryImplementor) new MetadataSources( ssr )
.addAnnotatedClass( Company.class )
.addAnnotatedClass( Location.class )
.addResource( "org/hibernate/test/jpa/compliance/joincolumn/orm.xml" )
.buildMetadata()
.buildSessionFactory()) {
try {
inTransaction(
sf,
session -> {
final Company acme = new Company( 1, "Acme Corp" );
new Location( 1, "86-215", acme );
new Location( 2, "20-759", acme );
session.persist( acme );
}
);
inTransaction(
sf,
session -> {
final Company acme = session.get( Company.class, 1 );
assert acme.getLocations().size() == 2;
// this fails. however it is due to a number of bad assumptions
// in the TCK:
// First the spec says:
//
// {quote}
// The relationship modeling annotation constrains the use of the cascade=REMOVE specification. The
// cascade=REMOVE specification should only be applied to associations that are specified as One-
// ToOne or OneToMany. Applications that apply cascade=REMOVE to other associations are not por-
// table.
// {quote}
//
// Here the test is applying cascade=REMOVE to a ManyToOne.
//
// Secondly, the spec says:
//
// {quote}
// The persistence provider runtime is permitted to perform synchronization to the database at other times
// as well when a transaction is active and the persistence context is joined to the transaction.
// {quote}
//
//
// In other words, the provider is actually legal to perform the database delete immediately. Since
// the TCK deletes the Company first, a provider is legally able to perform the database delete on
// Company immediately. However the TCK test as defined makes this impossible:
// 1) simply deleting Company won't work since Location rows still reference it. Locations
// would need to be deleted first (cascade the remove to the @OneToMany `locations`
// attribute), or
// 2) perform a SQL update, updating the COMP_ID columns in the Location table to be null
// so that deleting Company wont cause FK violations. But again this is made impossible
// by the TCK because it defines the column as non-nullable (and not even in the @JoinColumn
// btw which would be another basis for challenge).
session.remove( acme );
for ( Location location : acme.getLocations() ) {
session.remove( location );
}
}
);
}
finally {
inTransaction(
sf,
session -> {
session.createQuery( "delete Location" ).executeUpdate();
session.createQuery( "delete Company" ).executeUpdate();
}
);
}
}
}
finally {
StandardServiceRegistryBuilder.destroy( ssr );
}
}
}

View File

@ -0,0 +1,61 @@
/*
* 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.jpa.compliance.joincolumn;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
/**
* @author Steve Ebersole
*/
@Entity
public class Location {
private Integer id;
private String code;
private Company company;
public Location() {
}
public Location(Integer id, String code, Company company) {
this.id = id;
this.code = code;
this.company = company;
company.addLocation( this );
}
@Id
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
@ManyToOne(cascade = CascadeType.REMOVE)
@JoinColumn(name = "comp_id", referencedColumnName = "id", nullable = false)
public Company getCompany() {
return company;
}
public void setCompany(Company company) {
this.company = company;
}
}

View File

@ -50,6 +50,7 @@ log4j.logger.org.hibernate.engine.internal.StatisticalLoggingSessionEventListene
log4j.logger.org.hibernate.boot.model.source.internal.hbm.ModelBinder=debug
log4j.logger.org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry=debug
log4j.logger.org.hibernate.engine.internal.Cascade=trace
### When entity copy merge functionality is enabled using:
### hibernate.event.merge.entity_copy_observer=log, the following will

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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
-->
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm
http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd"
version="2.1">
<entity class="org.hibernate.test.jpa.compliance.joincolumn.Company">
<attributes>
<one-to-many name="locations" mapped-by="company">
<cascade>
<cascade-persist/>
</cascade>
</one-to-many>
</attributes>
</entity>
<entity class="org.hibernate.test.jpa.compliance.joincolumn.Location">
<attributes>
<many-to-one name="company">
<join-column name="COMP_ID" referenced-column-name="ID" nullable="false" />
</many-to-one>
</attributes>
</entity>
</entity-mappings>