Merge remote-tracking branch 'upstream/master' into wip/6.0

This commit is contained in:
Andrea Boriero 2020-11-17 18:41:39 +01:00
commit aff9bb4609
11 changed files with 287 additions and 27 deletions

View File

@ -411,7 +411,7 @@ public class EnhancerImpl implements Enhancer {
continue;
}
AnnotatedFieldDescription annotatedField = new AnnotatedFieldDescription( enhancementContext, ctField );
if ( enhancementContext.isPersistentField( annotatedField ) && !enhancementContext.isMappedCollection( annotatedField ) ) {
if ( enhancementContext.isPersistentField( annotatedField ) && enhancementContext.isMappedCollection( annotatedField ) ) {
if ( ctField.getType().asErasure().isAssignableTo( Collection.class ) || ctField.getType().asErasure().isAssignableTo( Map.class ) ) {
collectionList.add( annotatedField );
}
@ -441,7 +441,7 @@ public class EnhancerImpl implements Enhancer {
for ( FieldDescription ctField : managedCtSuperclass.getDeclaredFields() ) {
if ( !Modifier.isStatic( ctField.getModifiers() ) ) {
AnnotatedFieldDescription annotatedField = new AnnotatedFieldDescription( enhancementContext, ctField );
if ( enhancementContext.isPersistentField( annotatedField ) && !enhancementContext.isMappedCollection( annotatedField ) ) {
if ( enhancementContext.isPersistentField( annotatedField ) && enhancementContext.isMappedCollection( annotatedField ) ) {
if ( ctField.getType().asErasure().isAssignableTo( Collection.class ) || ctField.getType().asErasure().isAssignableTo( Map.class ) ) {
collectionList.add( annotatedField );
}

View File

@ -6,6 +6,8 @@
*/
package org.hibernate.bytecode.enhance.spi;
import javax.persistence.Basic;
import javax.persistence.Convert;
import javax.persistence.ElementCollection;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
@ -106,7 +108,13 @@ public class DefaultEnhancementContext implements EnhancementContext {
*/
@Override
public boolean isMappedCollection(UnloadedField field) {
return field.hasAnnotation( OneToMany.class ) || field.hasAnnotation( ManyToMany.class ) || field.hasAnnotation( ElementCollection.class );
// If the collection is definitely a plural attribute, we respect that
if (field.hasAnnotation( OneToMany.class ) || field.hasAnnotation( ManyToMany.class ) || field.hasAnnotation( ElementCollection.class )) {
return true;
}
// But a collection might be treated like a singular attribute if it is annotated with `@Basic`
// If no annotations are given though, a collection is treated like a OneToMany
return !field.hasAnnotation( Basic.class );
}
/**

View File

@ -31,6 +31,7 @@ import org.hibernate.engine.spi.Status;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.persister.entity.UniqueKeyLoadable;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.proxy.HibernateProxy;
/**
* A base implementation of EntityEntry
@ -321,7 +322,24 @@ public abstract class AbstractEntityEntry implements Serializable, EntityEntry {
@SuppressWarnings( {"SimplifiableIfStatement"})
private boolean isUnequivocallyNonDirty(Object entity) {
if ( entity instanceof SelfDirtinessTracker ) {
return ! persister.hasCollections() && ! ( (SelfDirtinessTracker) entity ).$$_hibernate_hasDirtyAttributes();
boolean uninitializedProxy = false;
if ( entity instanceof PersistentAttributeInterceptable ) {
final PersistentAttributeInterceptable interceptable = (PersistentAttributeInterceptable) entity;
final PersistentAttributeInterceptor interceptor = interceptable.$$_hibernate_getInterceptor();
if ( interceptor instanceof EnhancementAsProxyLazinessInterceptor ) {
EnhancementAsProxyLazinessInterceptor enhancementAsProxyLazinessInterceptor = (EnhancementAsProxyLazinessInterceptor) interceptor;
// When a proxy has dirty attributes, we have to treat it like a normal entity to flush changes
uninitializedProxy = !enhancementAsProxyLazinessInterceptor.isInitialized() && !( (SelfDirtinessTracker) entity ).$$_hibernate_hasDirtyAttributes();
}
}
else if ( entity instanceof HibernateProxy ) {
uninitializedProxy = ( (HibernateProxy) entity ).getHibernateLazyInitializer()
.isUninitialized();
}
// we never have to check an uninitialized proxy
return uninitializedProxy || !persister.hasCollections()
&& !persister.hasMutableProperties()
&& !( (SelfDirtinessTracker) entity ).$$_hibernate_hasDirtyAttributes();
}
if ( entity instanceof PersistentAttributeInterceptable ) {

View File

@ -22,6 +22,7 @@ import org.hibernate.engine.internal.Nullability;
import org.hibernate.engine.internal.Versioning;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.ManagedEntity;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.PersistentAttributeInterceptable;
import org.hibernate.engine.spi.PersistentAttributeInterceptor;
@ -39,6 +40,7 @@ import org.hibernate.jpa.event.spi.CallbackRegistryConsumer;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.stat.spi.StatisticsImplementor;
import org.hibernate.type.Type;
@ -525,18 +527,13 @@ public class DefaultFlushEntityEventListener implements FlushEntityEventListener
if ( dirtyProperties == null ) {
if ( entity instanceof SelfDirtinessTracker ) {
if ( ( (SelfDirtinessTracker) entity ).$$_hibernate_hasDirtyAttributes() ) {
int[] dirty = persister.resolveAttributeIndexes( ( (SelfDirtinessTracker) entity ).$$_hibernate_getDirtyAttributes() );
// HHH-12051 - filter non-updatable attributes
// TODO: add Updatability to EnhancementContext and skip dirty tracking of those attributes
int count = 0;
for ( int i : dirty ) {
if ( persister.getPropertyUpdateability()[i] ) {
dirty[count++] = i;
}
}
dirtyProperties = count == 0 ? ArrayHelper.EMPTY_INT_ARRAY : count == dirty.length ? dirty : Arrays.copyOf( dirty, count );
if ( ( (SelfDirtinessTracker) entity ).$$_hibernate_hasDirtyAttributes() || persister.hasMutableProperties() ) {
dirtyProperties = persister.resolveDirtyAttributeIndexes(
values,
loadedState,
( (SelfDirtinessTracker) entity ).$$_hibernate_getDirtyAttributes(),
session
);
}
else {
dirtyProperties = ArrayHelper.EMPTY_INT_ARRAY;

View File

@ -12,6 +12,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
@ -78,6 +79,7 @@ import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.spi.CachedNaturalIdValueSource;
import org.hibernate.engine.spi.CascadeStyle;
import org.hibernate.engine.spi.CascadingActions;
import org.hibernate.engine.spi.CollectionKey;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.EntityEntryFactory;
@ -91,6 +93,7 @@ import org.hibernate.engine.spi.PersistentAttributeInterceptable;
import org.hibernate.engine.spi.PersistentAttributeInterceptor;
import org.hibernate.engine.spi.SelfDirtinessTracker;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.engine.spi.ValueInclusion;
import org.hibernate.event.spi.EventSource;
@ -2449,6 +2452,52 @@ public abstract class AbstractEntityPersister
return Arrays.copyOf( fields, counter );
}
@Override
public int[] resolveDirtyAttributeIndexes(
final Object[] currentState,
final Object[] previousState,
final String[] attributeNames,
final SessionImplementor session) {
final BitSet mutablePropertiesIndexes = entityMetamodel.getMutablePropertiesIndexes();
final int estimatedSize = attributeNames == null ? 0 : attributeNames.length + mutablePropertiesIndexes.cardinality();
final List<Integer> fields = new ArrayList<>( estimatedSize );
if ( estimatedSize == 0 ) {
return ArrayHelper.EMPTY_INT_ARRAY;
}
if ( !mutablePropertiesIndexes.isEmpty() ) {
// We have to check the state for "mutable" properties as dirty tracking isn't aware of mutable types
final Type[] propertyTypes = entityMetamodel.getPropertyTypes();
final boolean[] propertyCheckability = entityMetamodel.getPropertyCheckability();
mutablePropertiesIndexes.stream().forEach( i -> {
// This is kindly borrowed from org.hibernate.type.TypeHelper.findDirty
final boolean dirty = currentState[i] != LazyPropertyInitializer.UNFETCHED_PROPERTY &&
( previousState[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY ||
( propertyCheckability[i]
&& propertyTypes[i].isDirty(
previousState[i],
currentState[i],
propertyColumnUpdateable[i],
session
) ) );
if ( dirty ) {
fields.add( i );
}
} );
}
if ( attributeNames != null ) {
final boolean[] propertyUpdateability = entityMetamodel.getPropertyUpdateability();
for ( String attributeName : attributeNames ) {
final Integer index = entityMetamodel.getPropertyIndexOrNull( attributeName );
if ( index != null && propertyUpdateability[index] && !fields.contains( index ) ) {
fields.add( index );
}
}
}
return ArrayHelper.toIntArray( fields );
}
protected String[] getSubclassPropertySubclassNameClosure() {
return subclassPropertySubclassNameClosure;
}

View File

@ -26,6 +26,7 @@ import org.hibernate.cache.spi.entry.CacheEntryStructure;
import org.hibernate.engine.spi.CascadeStyle;
import org.hibernate.engine.spi.EntityEntryFactory;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.engine.spi.ValueInclusion;
import org.hibernate.id.IdentifierGenerator;
@ -884,6 +885,25 @@ public interface EntityPersister
*/
int[] resolveAttributeIndexes(String[] attributeNames);
/**
* Like {@link #resolveAttributeIndexes(String[])} but also always returns mutable attributes
*
*
* @param values
* @param loadedState
* @param attributeNames Array of names to be resolved
*
* @param session
* @return A set of unique indexes of the attribute names found in the metamodel
*/
default int[] resolveDirtyAttributeIndexes(
Object[] values,
Object[] loadedState,
String[] attributeNames,
SessionImplementor session) {
return resolveAttributeIndexes( attributeNames );
}
boolean canUseReferenceCacheEntries();
/**

View File

@ -8,6 +8,7 @@ package org.hibernate.tuple.entity;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@ -43,6 +44,7 @@ import org.hibernate.tuple.PropertyFactory;
import org.hibernate.tuple.ValueGeneration;
import org.hibernate.tuple.ValueGenerator;
import org.hibernate.type.AssociationType;
import org.hibernate.type.ComponentType;
import org.hibernate.type.CompositeType;
import org.hibernate.type.EntityType;
import org.hibernate.type.ManyToOneType;
@ -97,7 +99,7 @@ public class EntityMetamodel implements Serializable {
private final Map<String, Integer> propertyIndexes = new HashMap<>();
private final boolean hasCollections;
private final boolean hasMutableProperties;
private final BitSet mutablePropertiesIndexes;
private final boolean hasLazyProperties;
private final boolean hasNonIdentifierPropertyNamedId;
@ -206,7 +208,7 @@ public class EntityMetamodel implements Serializable {
int tempVersionProperty = NO_VERSION_INDX;
boolean foundCascade = false;
boolean foundCollection = false;
boolean foundMutable = false;
BitSet mutableIndexes = new BitSet();
boolean foundNonIdentifierPropertyNamedId = false;
boolean foundUpdateableNaturalIdProperty = false;
@ -316,8 +318,9 @@ public class EntityMetamodel implements Serializable {
foundCollection = true;
}
if ( propertyTypes[i].isMutable() && propertyCheckability[i] ) {
foundMutable = true;
// Component types are dirty tracked as well so they are not exactly mutable for the "maybeDirty" check
if ( propertyTypes[i].isMutable() && propertyCheckability[i] && !( propertyTypes[i] instanceof ComponentType ) ) {
mutableIndexes.set( i );
}
mapPropertyToIndex(prop, i);
@ -393,7 +396,7 @@ public class EntityMetamodel implements Serializable {
}
hasCollections = foundCollection;
hasMutableProperties = foundMutable;
mutablePropertiesIndexes = mutableIndexes;
iter = persistentClass.getSubclassIterator();
final Set<String> subclassEntityNamesLocal = new HashSet<>();
@ -874,7 +877,11 @@ public class EntityMetamodel implements Serializable {
}
public boolean hasMutableProperties() {
return hasMutableProperties;
return !mutablePropertiesIndexes.isEmpty();
}
public BitSet getMutablePropertiesIndexes() {
return mutablePropertiesIndexes;
}
public boolean hasNonIdentifierPropertyNamedId() {

View File

@ -63,11 +63,20 @@ public class DirtyTrackingTest {
EnhancerTestUtils.checkDirtyTracking( entity, "someStrings" );
EnhancerTestUtils.clearDirtyTracking( entity );
// Association: this should not set the entity to dirty
Set<Integer> intSet = new HashSet<>();
intSet.add( 42 );
entity.someInts = intSet;
// Association, 1: creating the association will mark it dirty
Set<OtherEntity> associatedSet = new HashSet<>();
OtherEntity o = new OtherEntity();
o.id = 1l;
o.name = "other";
associatedSet.add( o );
entity.someAssociation = associatedSet;
EnhancerTestUtils.checkDirtyTracking( entity, "someAssociation" );
EnhancerTestUtils.clearDirtyTracking( entity );
// Association, 2: modifying a related entity should not
o.name = "newName";
EnhancerTestUtils.checkDirtyTracking( entity );
EnhancerTestUtils.checkDirtyTracking( o, "name" );
// testing composite object
Address address = new Address();
@ -125,7 +134,7 @@ public class DirtyTrackingTest {
List<String> someStrings;
@OneToMany
Set<Integer> someInts;
Set<OtherEntity> someAssociation;
@Embedded
Address address;
@ -141,4 +150,11 @@ public class DirtyTrackingTest {
this.someNumber = someNumber;
}
}
@Entity
private static class OtherEntity {
@Id
Long id;
String name;
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.mutable;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.persistence.AttributeConverter;
public class MapStringConverter implements AttributeConverter<Map<String, String>, String> {
@Override
public String convertToDatabaseColumn(Map<String, String> attribute) {
if ( attribute == null ) {
return null;
}
return attribute.entrySet().stream()
.map( entry -> entry.getKey() + ";" + entry.getValue() )
.collect( Collectors.joining( ";" ) );
}
@Override
public Map<String, String> convertToEntityAttribute(String dbData) {
if ( dbData == null ) {
return null;
}
String[] strings = dbData.split( ";" );
Map<String, String> map = new HashMap<>();
for ( int i = 0; i < strings.length; i += 2 ) {
map.put( strings[i], strings[i + 1] );
}
return map;
}
}

View File

@ -0,0 +1,50 @@
/*
* 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.mutable;
import java.util.Date;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(BytecodeEnhancerRunner.class)
public class MutableTypeEnhancementTestCase extends BaseCoreFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] { TestEntity.class };
}
@Test
@TestForIssue(jiraKey = "HHH-14329")
public void testMutateMutableTypeObject() throws Exception {
inTransaction( entityManager -> {
TestEntity e = new TestEntity();
e.setId( 1L );
e.setDate( new Date() );
e.getTexts().put( "a", "abc" );
entityManager.persist( e );
} );
inTransaction( entityManager -> {
TestEntity e = entityManager.find( TestEntity.class, 1L );
e.getDate().setTime( 0 );
e.getTexts().put( "a", "def" );
} );
inTransaction( entityManager -> {
TestEntity e = entityManager.find( TestEntity.class, 1L );
Assert.assertEquals( 0L, e.getDate().getTime() );
Assert.assertEquals( "def", e.getTexts().get( "a" ) );
} );
}
}

View File

@ -0,0 +1,56 @@
/*
* 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.mutable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@Entity
public class TestEntity {
@Id
private Long id;
@Temporal(TemporalType.TIMESTAMP)
private Date date;
@Basic
@Column(name = "TEXTS")
@Convert(converter = MapStringConverter.class)
private Map<String, String> texts = new HashMap<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public Map<String, String> getTexts() {
return texts;
}
public void setTexts(Map<String, String> texts) {
this.texts = texts;
}
}