HHH-7610 Add an option to initialize empty components when all the properties are null

Note that this commit also changes the semantic of the comparison between a null component
and a component with all properties null: they are considered equivalent.
This commit is contained in:
Guillaume Smet 2015-09-15 12:32:04 +02:00
parent 701f59b132
commit d71f931429
8 changed files with 305 additions and 15 deletions

View File

@ -1009,4 +1009,13 @@ public interface AvailableSettings {
* Values are {@code true} (pass the NULLs) or {@code false} (do not pass the NULLs). * Values are {@code true} (pass the NULLs) or {@code false} (do not pass the NULLs).
*/ */
String PROCEDURE_NULL_PARAM_PASSING = "hibernate.proc.param_null_passing"; String PROCEDURE_NULL_PARAM_PASSING = "hibernate.proc.param_null_passing";
/*
* Enable instantiation of composite/embedded objects when all of its attribute values are {@code null}.
* The default (and historical) behavior is that a {@code null} reference will be used to represent the
* composite when all of its attributes are {@code null}
*
* @since 5.1
*/
String CREATE_EMPTY_COMPOSITES_ENABLED = "hibernate.create_empty_composites.enabled";
} }

View File

@ -14,6 +14,9 @@ import java.util.Map;
import org.hibernate.EntityMode; import org.hibernate.EntityMode;
import org.hibernate.HibernateException; import org.hibernate.HibernateException;
import org.hibernate.boot.spi.MetadataBuildingOptions; import org.hibernate.boot.spi.MetadataBuildingOptions;
import org.hibernate.cfg.Environment;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.internal.util.config.ConfigurationHelper;
import org.hibernate.mapping.Component; import org.hibernate.mapping.Component;
import org.hibernate.mapping.Property; import org.hibernate.mapping.Property;
import org.hibernate.tuple.PropertyFactory; import org.hibernate.tuple.PropertyFactory;
@ -39,6 +42,7 @@ public class ComponentMetamodel implements Serializable {
// cached for efficiency... // cached for efficiency...
private final int propertySpan; private final int propertySpan;
private final Map propertyIndexes = new HashMap(); private final Map propertyIndexes = new HashMap();
private final boolean createEmptyCompositesEnabled;
// public ComponentMetamodel(Component component, SessionFactoryImplementor sessionFactory) { // public ComponentMetamodel(Component component, SessionFactoryImplementor sessionFactory) {
public ComponentMetamodel(Component component, MetadataBuildingOptions metadataBuildingOptions) { public ComponentMetamodel(Component component, MetadataBuildingOptions metadataBuildingOptions) {
@ -65,6 +69,15 @@ public class ComponentMetamodel implements Serializable {
entityMode, entityMode,
component component
) : componentTuplizerFactory.constructTuplizer( tuplizerClassName, component ); ) : componentTuplizerFactory.constructTuplizer( tuplizerClassName, component );
final ConfigurationService cs = component.getMetadata().getMetadataBuildingOptions().getServiceRegistry()
.getService(ConfigurationService.class);
this.createEmptyCompositesEnabled = ConfigurationHelper.getBoolean(
Environment.CREATE_EMPTY_COMPOSITES_ENABLED,
cs.getSettings(),
false
);
} }
public boolean isKey() { public boolean isKey() {
@ -106,4 +119,8 @@ public class ComponentMetamodel implements Serializable {
return componentTuplizer; return componentTuplizer;
} }
public boolean isCreateEmptyCompositesEnabled() {
return createEmptyCompositesEnabled;
}
} }

View File

@ -49,6 +49,7 @@ public class ComponentType extends AbstractType implements CompositeType, Proced
private final FetchMode[] joinedFetch; private final FetchMode[] joinedFetch;
private final boolean isKey; private final boolean isKey;
private boolean hasNotNullProperty; private boolean hasNotNullProperty;
private final boolean createEmptyCompositesEnabled;
protected final EntityMode entityMode; protected final EntityMode entityMode;
protected final ComponentTuplizer componentTuplizer; protected final ComponentTuplizer componentTuplizer;
@ -80,6 +81,7 @@ public class ComponentType extends AbstractType implements CompositeType, Proced
this.entityMode = metamodel.getEntityMode(); this.entityMode = metamodel.getEntityMode();
this.componentTuplizer = metamodel.getComponentTuplizer(); this.componentTuplizer = metamodel.getComponentTuplizer();
this.createEmptyCompositesEnabled = metamodel.isCreateEmptyCompositesEnabled();
} }
public boolean isKey() { public boolean isKey() {
@ -158,9 +160,7 @@ public class ComponentType extends AbstractType implements CompositeType, Proced
if ( x == y ) { if ( x == y ) {
return true; return true;
} }
if ( x == null || y == null ) { // null value and empty component are considered equivalent
return false;
}
Object[] xvalues = getPropertyValues( x, entityMode ); Object[] xvalues = getPropertyValues( x, entityMode );
Object[] yvalues = getPropertyValues( y, entityMode ); Object[] yvalues = getPropertyValues( y, entityMode );
for ( int i = 0; i < propertySpan; i++ ) { for ( int i = 0; i < propertySpan; i++ ) {
@ -176,9 +176,7 @@ public class ComponentType extends AbstractType implements CompositeType, Proced
if ( x == y ) { if ( x == y ) {
return true; return true;
} }
if ( x == null || y == null ) { // null value and empty component are considered equivalent
return false;
}
for ( int i = 0; i < propertySpan; i++ ) { for ( int i = 0; i < propertySpan; i++ ) {
if ( !propertyTypes[i].isEqual( getPropertyValue( x, i ), getPropertyValue( y, i ) ) ) { if ( !propertyTypes[i].isEqual( getPropertyValue( x, i ), getPropertyValue( y, i ) ) ) {
return false; return false;
@ -193,9 +191,7 @@ public class ComponentType extends AbstractType implements CompositeType, Proced
if ( x == y ) { if ( x == y ) {
return true; return true;
} }
if ( x == null || y == null ) { // null value and empty component are considered equivalent
return false;
}
for ( int i = 0; i < propertySpan; i++ ) { for ( int i = 0; i < propertySpan; i++ ) {
if ( !propertyTypes[i].isEqual( getPropertyValue( x, i ), getPropertyValue( y, i ), factory ) ) { if ( !propertyTypes[i].isEqual( getPropertyValue( x, i ), getPropertyValue( y, i ), factory ) ) {
return false; return false;
@ -253,9 +249,7 @@ public class ComponentType extends AbstractType implements CompositeType, Proced
if ( x == y ) { if ( x == y ) {
return false; return false;
} }
if ( x == null || y == null ) { // null value and empty component are considered equivalent
return true;
}
for ( int i = 0; i < propertySpan; i++ ) { for ( int i = 0; i < propertySpan; i++ ) {
if ( propertyTypes[i].isDirty( getPropertyValue( x, i ), getPropertyValue( y, i ), session ) ) { if ( propertyTypes[i].isDirty( getPropertyValue( x, i ), getPropertyValue( y, i ), session ) ) {
return true; return true;
@ -269,9 +263,7 @@ public class ComponentType extends AbstractType implements CompositeType, Proced
if ( x == y ) { if ( x == y ) {
return false; return false;
} }
if ( x == null || y == null ) { // null value and empty component are considered equivalent
return true;
}
int loc = 0; int loc = 0;
for ( int i = 0; i < propertySpan; i++ ) { for ( int i = 0; i < propertySpan; i++ ) {
int len = propertyTypes[i].getColumnSpan( session.getFactory() ); int len = propertyTypes[i].getColumnSpan( session.getFactory() );
@ -408,6 +400,9 @@ public class ComponentType extends AbstractType implements CompositeType, Proced
public Object getPropertyValue(Object component, int i) public Object getPropertyValue(Object component, int i)
throws HibernateException { throws HibernateException {
if (component == null) {
component = new Object[propertySpan];
}
if ( component instanceof Object[] ) { if ( component instanceof Object[] ) {
// A few calls to hashCode pass the property values already in an // A few calls to hashCode pass the property values already in an
// Object[] (ex: QueryKey hash codes for cached queries). // Object[] (ex: QueryKey hash codes for cached queries).
@ -429,6 +424,9 @@ public class ComponentType extends AbstractType implements CompositeType, Proced
@Override @Override
public Object[] getPropertyValues(Object component, EntityMode entityMode) public Object[] getPropertyValues(Object component, EntityMode entityMode)
throws HibernateException { throws HibernateException {
if (component == null) {
component = new Object[propertySpan];
}
if ( component instanceof Object[] ) { if ( component instanceof Object[] ) {
// A few calls to hashCode pass the property values already in an // A few calls to hashCode pass the property values already in an
// Object[] (ex: QueryKey hash codes for cached queries). // Object[] (ex: QueryKey hash codes for cached queries).
@ -689,6 +687,9 @@ public class ComponentType extends AbstractType implements CompositeType, Proced
setPropertyValues( result, resolvedValues, entityMode ); setPropertyValues( result, resolvedValues, entityMode );
return result; return result;
} }
else if ( isCreateEmptyCompositesEnabled() ) {
return instantiate( owner, session );
}
else { else {
return null; return null;
} }
@ -827,4 +828,8 @@ public class ComponentType extends AbstractType implements CompositeType, Proced
public boolean hasNotNullProperty() { public boolean hasNotNullProperty() {
return hasNotNullProperty; return hasNotNullProperty;
} }
private boolean isCreateEmptyCompositesEnabled() {
return createEmptyCompositesEnabled;
}
} }

View File

@ -0,0 +1,34 @@
/*
* 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.component.empty;
import javax.persistence.Embeddable;
@Embeddable
public class ComponentEmptyEmbedded {
private String field1;
private String field2;
public String getField1() {
return field1;
}
public void setField1(String field1) {
this.field1 = field1;
}
public String getField2() {
return field2;
}
public void setField2(String field2) {
this.field2 = field2;
}
}

View File

@ -0,0 +1,38 @@
/*
* 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.component.empty;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class ComponentEmptyEmbeddedOwner {
@Id
@GeneratedValue
private Integer id;
private ComponentEmptyEmbedded embedded;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public ComponentEmptyEmbedded getEmbedded() {
return embedded;
}
public void setEmbedded(ComponentEmptyEmbedded embedded) {
this.embedded = embedded;
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.component.empty;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import org.hibernate.Session;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;
/**
* Test class for empty embedded dirtiness computation.
*
* @author Laurent Almeras
*/
public class EmptyCompositesDirtynessTest extends BaseCoreFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] { ComponentEmptyEmbeddedOwner.class, ComponentEmptyEmbedded.class };
}
@Override
protected void configure(Configuration configuration) {
super.configure( configuration );
configuration.getProperties().put( Environment.CREATE_EMPTY_COMPOSITES_ENABLED, Boolean.valueOf( false ) );
}
/**
* Test for dirtyness computation consistency when a property is an empty composite.
*/
@Test
@TestForIssue(jiraKey = "HHH-7610")
public void testCompositesEmpty() {
Session s = openSession();
s.getTransaction().begin();
ComponentEmptyEmbeddedOwner owner = new ComponentEmptyEmbeddedOwner();
s.persist( owner );
s.flush();
s.getTransaction().commit();
s.clear();
s.getTransaction().begin();
owner = (ComponentEmptyEmbeddedOwner) s.get( ComponentEmptyEmbeddedOwner.class, owner.getId() );
assertNull( owner.getEmbedded() );
owner.setEmbedded( new ComponentEmptyEmbedded() );
// technically, as all properties are null, update may not be necessary
assertFalse( session.isDirty() ); // must be false to avoid unnecessary updates
s.getTransaction().rollback();
}
}

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.component.empty;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import org.hibernate.Session;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;
/**
* Test class for empty embedded dirtiness computation.
*
* @author Laurent Almeras
*/
public class EmptyInitializedCompositesDirtynessTest extends BaseCoreFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] { ComponentEmptyEmbeddedOwner.class, ComponentEmptyEmbedded.class };
}
@Override
protected void configure(Configuration configuration) {
super.configure( configuration );
configuration.getProperties().put( Environment.CREATE_EMPTY_COMPOSITES_ENABLED, Boolean.valueOf( true ) );
}
/**
* Test for dirtyness computation consistency when a property is an empty composite and that empty composite
* initialization is set.
*/
@Test
@TestForIssue(jiraKey = "HHH-7610")
public void testInitializedCompositesEmpty() {
Session s = openSession();
s.getTransaction().begin();
ComponentEmptyEmbeddedOwner owner = new ComponentEmptyEmbeddedOwner();
s.persist( owner );
s.flush();
s.getTransaction().commit();
s.clear();
s.getTransaction().begin();
owner = (ComponentEmptyEmbeddedOwner) s.get( ComponentEmptyEmbeddedOwner.class, owner.getId() );
assertNotNull( owner.getEmbedded() );
assertFalse( s.isDirty() );
s.getTransaction().rollback();
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.component.empty;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import org.hibernate.Session;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;
/**
* Test class for empty embedded dirtiness computation.
*
* @author Laurent Almeras
*/
public class EmptyInitializedCompositesTest extends BaseCoreFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] { ComponentEmptyEmbeddedOwner.class, ComponentEmptyEmbedded.class };
}
@Override
protected void configure(Configuration configuration) {
super.configure( configuration );
configuration.getProperties().put( Environment.CREATE_EMPTY_COMPOSITES_ENABLED, Boolean.valueOf( true ) );
}
/**
* Test empty composite initialization.
*/
@Test
@TestForIssue(jiraKey = "HHH-7610")
public void testCompositesEmpty() {
Session s = openSession();
s.getTransaction().begin();
ComponentEmptyEmbeddedOwner owner = new ComponentEmptyEmbeddedOwner();
s.persist( owner );
s.flush();
s.getTransaction().commit();
s.clear();
s.getTransaction().begin();
owner = (ComponentEmptyEmbeddedOwner) s.get( ComponentEmptyEmbeddedOwner.class, owner.getId() );
assertNotNull( owner.getEmbedded() );
assertFalse( s.isDirty() );
owner.setEmbedded( null );
assertFalse( s.isDirty() ); // must be false to avoid unnecessary updates
s.getTransaction().rollback();
}
}