HHH-10667 - Fix Envers allowing @IdClass mappings using entity primary keys.

This commit is contained in:
Chris Cranford 2018-03-14 09:54:38 -04:00
parent 1ae930ef69
commit 27a6b5d143
9 changed files with 428 additions and 87 deletions

View File

@ -86,37 +86,6 @@ public final class BasicMetadataGenerator {
}
}
@SuppressWarnings({"unchecked"})
boolean addManyToOne(
Element parent,
PropertyAuditingData propertyAuditingData,
Value value,
SimpleMapperBuilder mapper) {
final Type type = value.getType();
// A null mapper occurs when adding to composite-id element
final Element manyToOneElement = parent.addElement( mapper != null ? "many-to-one" : "key-many-to-one" );
manyToOneElement.addAttribute( "name", propertyAuditingData.getName() );
manyToOneElement.addAttribute( "class", type.getName() );
// HHH-11107
// Use FK hbm magic value 'none' to skip making foreign key constraints between the Envers
// schema and the base table schema when a @ManyToOne is present in an identifier.
if ( mapper == null ) {
manyToOneElement.addAttribute( "foreign-key", "none" );
}
MetadataTools.addColumns( manyToOneElement, value.getColumnIterator() );
// A null mapper means that we only want to add xml mappings
if ( mapper != null ) {
final PropertyData propertyData = propertyAuditingData.resolvePropertyData( value.getType() );
mapper.add( propertyData );
}
return true;
}
private boolean isAddNestedType(Value value) {
if ( value instanceof SimpleValue ) {
if ( ( (SimpleValue) value ).getTypeParameters() != null ) {

View File

@ -7,6 +7,7 @@
package org.hibernate.envers.configuration.internal.metadata;
import java.util.Iterator;
import java.util.Locale;
import org.hibernate.MappingException;
import org.hibernate.envers.ModificationStore;
@ -21,10 +22,12 @@ import org.hibernate.envers.internal.entities.mapper.id.MultipleIdMapper;
import org.hibernate.envers.internal.entities.mapper.id.SimpleIdMapperBuilder;
import org.hibernate.envers.internal.entities.mapper.id.SingleIdMapper;
import org.hibernate.envers.internal.tools.ReflectionTools;
import org.hibernate.loader.PropertyPath;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.ToOne;
import org.hibernate.mapping.Value;
import org.hibernate.type.ManyToOneType;
import org.hibernate.type.Type;
@ -35,6 +38,7 @@ import org.dom4j.tree.DefaultElement;
* Generates metadata for primary identifiers (ids) of versions entities.
*
* @author Adam Warski (adam at warski dot org)
* @author Chris Cranford
*/
public final class IdMetadataGenerator {
private final AuditMetadataGenerator mainGenerator;
@ -43,51 +47,86 @@ public final class IdMetadataGenerator {
mainGenerator = auditMetadataGenerator;
}
@SuppressWarnings({"unchecked"})
private boolean addIdProperties(
private Class<?> loadClass(Component component) {
final String className = component.getComponentClassName();
return ReflectionTools.loadClass( className, mainGenerator.getClassLoaderService() );
}
private static boolean isSameType(Property left, Property right) {
return left.getType().getName().equals( right.getType().getName() );
}
private boolean addIdProperty(
Element parent,
Iterator<Property> properties,
SimpleMapperBuilder mapper,
boolean key,
boolean audited) {
while ( properties.hasNext() ) {
final Property property = properties.next();
final Type propertyType = property.getType();
if ( !"_identifierMapper".equals( property.getName() ) ) {
boolean added = false;
if ( propertyType instanceof ManyToOneType ) {
added = mainGenerator.getBasicMetadataGenerator().addManyToOne(
parent,
getIdPersistentPropertyAuditingData( property ),
property.getValue(),
mapper
);
}
else {
// Last but one parameter: ids are always insertable
added = mainGenerator.getBasicMetadataGenerator().addBasic(
parent,
getIdPersistentPropertyAuditingData( property ),
property.getValue(),
mapper,
true,
key
);
}
if ( !added ) {
// If the entity is audited, and a non-supported id component is used, throwing an exception.
// If the entity is not audited, then we simply don't support this entity, even in
// target relation mode not audited.
if ( audited ) {
throw new MappingException( "Type not supported: " + propertyType.getClass().getName() );
}
else {
return false;
}
}
}
SimpleIdMapperBuilder mapper,
Property mappedProperty,
Property virtualProperty) {
if ( PropertyPath.IDENTIFIER_MAPPER_PROPERTY.equals( mappedProperty.getName() ) ) {
return false;
}
final PropertyAuditingData propertyAuditingData = getIdPersistentPropertyAuditingData( mappedProperty );
if ( ManyToOneType.class.isInstance( mappedProperty.getType() ) ) {
// This can technically be a @ManyToOne or logical @OneToOne
final boolean added = addManyToOne( parent, propertyAuditingData, mappedProperty.getValue(), mapper );
if ( added && mapper != null ) {
if ( virtualProperty != null && !isSameType( mappedProperty, virtualProperty ) ) {
// A virtual property is only available when an @IdClass is used. We specifically need to map
// both the value and virtual types when they differ so we can adequately map between them at
// appropriate points.
final Type valueType = mappedProperty.getType();
final Type virtualValueType = virtualProperty.getType();
mapper.add( propertyAuditingData.resolvePropertyData( valueType, virtualValueType ) );
}
else {
// In this branch the identifier either doesn't use an @IdClass or the property types between
// the @IdClass and containing entity are identical, allowing us to use prior behavior.
mapper.add( propertyAuditingData.resolvePropertyData( mappedProperty.getType() ) );
}
}
return added;
}
return addBasic( parent, propertyAuditingData, mappedProperty.getValue(), mapper, key );
}
private boolean addIdProperties(
Element parent,
Component component,
Component virtualComponent,
SimpleIdMapperBuilder mapper,
boolean key,
boolean audited) {
final Iterator properties = component.getPropertyIterator();
while ( properties.hasNext() ) {
final Property property = (Property) properties.next();
final Property virtualProperty;
if ( virtualComponent != null ) {
virtualProperty = virtualComponent.getProperty( property.getName() );
}
else {
virtualProperty = null;
}
if ( !addIdProperty( parent, key, mapper, property, virtualProperty ) ) {
// If the entity is audited, and a non-supported id component is used, throw exception.
if ( audited ) {
throw new MappingException(
String.format(
Locale.ROOT,
"Type not supported: %s",
property.getType().getClass().getName()
)
);
}
return false;
}
}
return true;
}
@ -155,14 +194,13 @@ public final class IdMetadataGenerator {
SimpleIdMapperBuilder mapper;
if ( idMapper != null ) {
// Multiple id
final Class componentClass = ReflectionTools.loadClass(
( (Component) pc.getIdentifier() ).getComponentClassName(),
mainGenerator.getClassLoaderService()
);
final Class componentClass = loadClass( (Component) pc.getIdentifier() );
final Component virtualComponent = (Component) pc.getIdentifier();
mapper = new MultipleIdMapper( componentClass, pc.getServiceRegistry() );
if ( !addIdProperties(
relIdMapping,
(Iterator<Property>) idMapper.getPropertyIterator(),
idMapper,
virtualComponent,
mapper,
false,
audited
@ -173,7 +211,8 @@ public final class IdMetadataGenerator {
// null mapper - the mapping where already added the first time, now we only want to generate the xml
if ( !addIdProperties(
origIdMapping,
(Iterator<Property>) idMapper.getPropertyIterator(),
idMapper,
virtualComponent,
null,
true,
audited
@ -184,14 +223,12 @@ public final class IdMetadataGenerator {
else if ( idProp.isComposite() ) {
// Embedded id
final Component idComponent = (Component) idProp.getValue();
final Class embeddableClass = ReflectionTools.loadClass(
idComponent.getComponentClassName(),
mainGenerator.getClassLoaderService()
);
final Class embeddableClass = loadClass( idComponent );
mapper = new EmbeddedIdMapper( getIdPropertyData( idProp ), embeddableClass, pc.getServiceRegistry() );
if ( !addIdProperties(
relIdMapping,
(Iterator<Property>) idComponent.getPropertyIterator(),
idComponent,
null,
mapper,
false,
audited
@ -202,7 +239,8 @@ public final class IdMetadataGenerator {
// null mapper - the mapping where already added the first time, now we only want to generate the xml
if ( !addIdProperties(
origIdMapping,
(Iterator<Property>) idComponent.getPropertyIterator(),
idComponent,
null,
null,
true,
audited
@ -256,4 +294,45 @@ public final class IdMetadataGenerator {
ModificationStore.FULL, RelationTargetAuditMode.AUDITED, null, null, false
);
}
@SuppressWarnings({"unchecked"})
boolean addManyToOne(
Element parent,
PropertyAuditingData propertyAuditingData,
Value value,
SimpleMapperBuilder mapper) {
final Type type = value.getType();
// A null mapper occurs when adding to composite-id element
final Element manyToOneElement = parent.addElement( mapper != null ? "many-to-one" : "key-many-to-one" );
manyToOneElement.addAttribute( "name", propertyAuditingData.getName() );
manyToOneElement.addAttribute( "class", type.getName() );
// HHH-11107
// Use FK hbm magic value 'none' to skip making foreign key constraints between the Envers
// schema and the base table schema when a @ManyToOne is present in an identifier.
if ( mapper == null ) {
manyToOneElement.addAttribute( "foreign-key", "none" );
}
MetadataTools.addColumns( manyToOneElement, value.getColumnIterator() );
return true;
}
boolean addBasic(
Element parent,
PropertyAuditingData propertyAuditingData,
Value value,
SimpleIdMapperBuilder mapper,
boolean key) {
return mainGenerator.getBasicMetadataGenerator().addBasic(
parent,
propertyAuditingData,
value,
mapper,
true,
key
);
}
}

View File

@ -160,6 +160,20 @@ public class PropertyAuditingData {
);
}
public PropertyData resolvePropertyData(Type propertyType, Type virtualType) {
return new PropertyData(
name,
beanName,
accessType,
store,
usingModifiedFlag,
modifiedFlagName,
syntheic,
propertyType,
virtualType.getReturnedClass()
);
}
public List<AuditOverride> getAuditingOverrides() {
return auditJoinTableOverrides;
}

View File

@ -30,6 +30,7 @@ public class PropertyData {
// They're properties used for bookkeeping by Hibernate
private boolean synthetic;
private Type propertyType;
private Class<?> virtualReturnClass;
/**
* Copies the given property data, except the name.
@ -42,6 +43,12 @@ public class PropertyData {
this.beanName = propertyData.beanName;
this.accessType = propertyData.accessType;
this.store = propertyData.store;
this.usingModifiedFlag = propertyData.usingModifiedFlag;
this.modifiedFlagName = propertyData.modifiedFlagName;
this.synthetic = propertyData.synthetic;
this.propertyType = propertyData.propertyType;
this.virtualReturnClass = propertyData.virtualReturnClass;
}
/**
@ -92,8 +99,22 @@ public class PropertyData {
String modifiedFlagName,
boolean synthetic,
Type propertyType) {
this( name, beanName, accessType, store, usingModifiedFlag, modifiedFlagName, synthetic, propertyType, null );
}
public PropertyData(
String name,
String beanName,
String accessType,
ModificationStore store,
boolean usingModifiedFlag,
String modifiedFlagName,
boolean synthetic,
Type propertyType,
Class<?> virtualReturnClass) {
this( name, beanName, accessType, store, usingModifiedFlag, modifiedFlagName, synthetic );
this.propertyType = propertyType;
this.virtualReturnClass = virtualReturnClass;
}
public String getName() {
@ -132,6 +153,10 @@ public class PropertyData {
return propertyType;
}
public Class<?> getVirtualReturnClass() {
return virtualReturnClass;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {

View File

@ -9,6 +9,7 @@ package org.hibernate.envers.internal.entities.mapper.id;
import java.util.List;
import java.util.Map;
import org.hibernate.Session;
import org.hibernate.envers.internal.tools.query.Parameters;
import org.hibernate.service.ServiceRegistry;
@ -21,6 +22,11 @@ public interface IdMapper {
void mapToMapFromId(Map<String, Object> data, Object obj);
default void mapToMapFromId(Session session, Map<String, Object> data, Object obj) {
// Delegate to the old behavior, allowing implementations to override.
mapToMapFromId( data, obj );
}
void mapToMapFromEntity(Map<String, Object> data, Object obj);
/**

View File

@ -11,17 +11,44 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.hibernate.Session;
import org.hibernate.envers.internal.entities.PropertyData;
import org.hibernate.service.ServiceRegistry;
/**
* @author Adam Warski (adam at warski dot org)
* @author Chris Cranford
*/
public class MultipleIdMapper extends AbstractCompositeIdMapper implements SimpleIdMapperBuilder {
public MultipleIdMapper(Class compositeIdClass, ServiceRegistry serviceRegistry) {
super( compositeIdClass, serviceRegistry );
}
@Override
public void add(PropertyData propertyData) {
ids.put( propertyData, resolveIdMapper( propertyData ) );
}
@Override
public void mapToMapFromId(Session session, Map<String, Object> data, Object obj) {
if ( compositeIdClass.isInstance( obj ) ) {
for ( Map.Entry<PropertyData, SingleIdMapper> entry : ids.entrySet() ) {
final PropertyData propertyData = entry.getKey();
final SingleIdMapper idMapper = entry.getValue();
if ( propertyData.getVirtualReturnClass() == null ) {
idMapper.mapToMapFromEntity( data, obj );
}
else {
idMapper.mapToMapFromId( session, data, obj );
}
}
}
else {
mapToMapFromId( data, obj );
}
}
@Override
public void mapToMapFromId(Map<String, Object> data, Object obj) {
for ( IdMapper idMapper : ids.values() ) {
@ -31,7 +58,9 @@ public class MultipleIdMapper extends AbstractCompositeIdMapper implements Simpl
@Override
public void mapToMapFromEntity(Map<String, Object> data, Object obj) {
mapToMapFromId( data, obj );
for ( IdMapper idMapper : ids.values() ) {
idMapper.mapToMapFromEntity( data, obj );
}
}
@Override
@ -50,7 +79,7 @@ public class MultipleIdMapper extends AbstractCompositeIdMapper implements Simpl
for ( PropertyData propertyData : ids.keySet() ) {
final String propertyName = propertyData.getName();
ret.ids.put( propertyData, new SingleIdMapper( getServiceRegistry(), new PropertyData( prefix + propertyName, propertyData ) ) );
ret.ids.put( propertyData, resolveIdMapper( new PropertyData( prefix + propertyName, propertyData ) ) );
}
return ret;
@ -83,4 +112,11 @@ public class MultipleIdMapper extends AbstractCompositeIdMapper implements Simpl
return ret;
}
private SingleIdMapper resolveIdMapper(PropertyData propertyData) {
if ( propertyData.getVirtualReturnClass() != null ) {
return new VirtualEntitySingleIdMapper( getServiceRegistry(), propertyData );
}
return new SingleIdMapper( getServiceRegistry(), propertyData );
}
}

View File

@ -0,0 +1,168 @@
/*
* 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.envers.internal.entities.mapper.id;
import java.io.Serializable;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Map;
import org.hibernate.Session;
import org.hibernate.envers.boot.internal.EnversService;
import org.hibernate.envers.internal.entities.PropertyData;
import org.hibernate.envers.internal.tools.ReflectionTools;
import org.hibernate.property.access.spi.Getter;
import org.hibernate.property.access.spi.Setter;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.type.EntityType;
/**
* An extension to the {@link SingleIdMapper} implementation that supports the use case of an {@code @IdClass}
* mapping that contains an entity association where the {@code @IdClass} stores the primary key of the
* associated entity rather than the entity object itself.
*
* Internally this mapper is capable of transforming the primary key values into the associated entity object
* and vice versa depending upon the operation.
*
* @author Chris Cranford
*/
public class VirtualEntitySingleIdMapper extends SingleIdMapper {
private final PropertyData propertyData;
private final String entityName;
private IdMapper entityIdMapper;
public VirtualEntitySingleIdMapper(ServiceRegistry serviceRegistry, PropertyData propertyData) {
super( serviceRegistry, propertyData );
this.propertyData = propertyData;
this.entityName = resolveEntityName( this.propertyData );
}
@Override
public void mapToMapFromId(Session session, Map<String, Object> data, Object obj) {
final Serializable value = AccessController.doPrivileged(
new PrivilegedAction<Serializable>() {
@Override
public Serializable run() {
final Getter getter = ReflectionTools.getGetter(
obj.getClass(),
propertyData,
getServiceRegistry()
);
return (Serializable) getter.get( obj );
}
}
);
// Either loads the entity from the session's 1LC if it already exists or potentially creates a
// proxy object to represent the entity by identifier so that we can reference it in the map.
final Object entity = session.load( this.entityName, value );
data.put( propertyData.getName(), entity );
}
@Override
public boolean mapToEntityFromMap(Object obj, Map data) {
if ( data == null || obj == null ) {
return false;
}
final Object value = data.get( propertyData.getName() );
if ( value == null ) {
return false;
}
return AccessController.doPrivileged(
new PrivilegedAction<Boolean>() {
@Override
public Boolean run() {
final Setter setter = ReflectionTools.getSetter(
obj.getClass(),
propertyData,
getServiceRegistry()
);
final Class<?> paramClass = ReflectionTools.getType(
obj.getClass(),
propertyData,
getServiceRegistry()
);
if ( paramClass != null && paramClass.equals( propertyData.getVirtualReturnClass() ) ) {
setter.set( obj, getAssociatedEntityIdMapper().mapToIdFromEntity( value ), null );
}
else {
setter.set( obj, value, null );
}
return true;
}
}
);
}
@Override
public void mapToMapFromEntity(Map<String, Object> data, Object obj) {
if ( obj == null ) {
data.put( propertyData.getName(), null );
}
else {
if ( obj instanceof HibernateProxy ) {
final HibernateProxy proxy = (HibernateProxy) obj;
data.put( propertyData.getName(), proxy.getHibernateLazyInitializer().getIdentifier() );
}
else {
final Object value = AccessController.doPrivileged(
new PrivilegedAction<Object>() {
@Override
public Object run() {
final Getter getter = ReflectionTools.getGetter(
obj.getClass(),
propertyData,
getServiceRegistry()
);
return getter.get( obj );
}
}
);
if ( propertyData.getVirtualReturnClass().isInstance( value ) ) {
// The value is the primary key, need to map it via IdMapper
getPrefixedAssociatedEntityIdMapper( propertyData ).mapToMapFromId( data, value );
}
else {
data.put( propertyData.getName(), value );
}
}
}
}
private IdMapper getAssociatedEntityIdMapper() {
if ( entityIdMapper == null ) {
entityIdMapper = resolveEntityIdMapper( getServiceRegistry(), entityName );
}
return entityIdMapper;
}
private IdMapper getPrefixedAssociatedEntityIdMapper(PropertyData propertyData) {
return getAssociatedEntityIdMapper().prefixMappedProperties( propertyData.getName() + "." );
}
private static String resolveEntityName(PropertyData propertyData) {
if ( EntityType.class.isInstance( propertyData.getType() ) ) {
final EntityType entityType = (EntityType) propertyData.getType();
return entityType.getAssociatedEntityName();
}
return null;
}
private static IdMapper resolveEntityIdMapper(ServiceRegistry serviceRegistry, String entityName) {
final EnversService enversService = serviceRegistry.getService( EnversService.class );
return enversService.getEntitiesConfigurations().get( entityName ).getIdMapper();
}
}

View File

@ -15,12 +15,14 @@ import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.envers.RevisionType;
import org.hibernate.envers.boot.internal.EnversService;
import org.hibernate.envers.configuration.internal.AuditEntitiesConfiguration;
import org.hibernate.envers.internal.entities.mapper.id.IdMapper;
import org.hibernate.envers.strategy.AuditStrategy;
/**
* @author Adam Warski (adam at warski dot org)
* @author Stephanie Pau at Markit Group Plc
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
* @author Chris Cranford
*/
public abstract class AbstractAuditWorkUnit implements AuditWorkUnit {
protected final SessionImplementor sessionImplementor;
@ -52,7 +54,9 @@ public abstract class AbstractAuditWorkUnit implements AuditWorkUnit {
final Map<String, Object> originalId = new HashMap<>();
originalId.put( entitiesCfg.getRevisionFieldName(), revision );
enversService.getEntitiesConfigurations().get( getEntityName() ).getIdMapper().mapToMapFromId( originalId, id );
final IdMapper idMapper = enversService.getEntitiesConfigurations().get( getEntityName() ).getIdMapper();
idMapper.mapToMapFromId( sessionImplementor, originalId, id );
data.put( entitiesCfg.getRevisionTypePropName(), revisionType );
data.put( entitiesCfg.getOriginalIdPropName(), originalId );
}

View File

@ -6,12 +6,15 @@
*/
package org.hibernate.envers.internal.tools;
import java.lang.reflect.Field;
import java.util.Locale;
import java.util.Map;
import org.hibernate.annotations.common.reflection.XClass;
import org.hibernate.annotations.common.reflection.XProperty;
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
import org.hibernate.boot.registry.classloading.spi.ClassLoadingException;
import org.hibernate.envers.exception.AuditException;
import org.hibernate.envers.internal.entities.PropertyData;
import org.hibernate.envers.tools.Pair;
import org.hibernate.internal.util.collections.ConcurrentReferenceHashMap;
@ -24,6 +27,7 @@ import org.hibernate.service.ServiceRegistry;
/**
* @author Adam Warski (adam at warski dot org)
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
* @author Chris Cranford
*/
public abstract class ReflectionTools {
private static final Map<Pair<Class, String>, Getter> GETTER_CACHE = new ConcurrentReferenceHashMap<>(
@ -74,6 +78,42 @@ public abstract class ReflectionTools {
return value;
}
public static Field getField(Class cls, PropertyData propertyData) {
Field field = null;
Class<?> clazz = cls;
while ( clazz != null && field == null ) {
try {
field = clazz.getDeclaredField( propertyData.getName() );
}
catch ( Exception e ) {
// ignore
}
clazz = clazz.getSuperclass();
}
return field;
}
public static Class<?> getType(Class cls, PropertyData propertyData, ServiceRegistry serviceRegistry) {
final Setter setter = getSetter( cls, propertyData, serviceRegistry );
if ( setter.getMethod() != null && setter.getMethod().getParameterCount() > 0 ) {
return setter.getMethod().getParameterTypes()[0];
}
final Field field = getField( cls, propertyData );
if ( field != null ) {
return field.getType();
}
throw new AuditException(
String.format(
Locale.ROOT,
"Failed to determine type for field [%s] on class [%s].",
propertyData.getName(),
cls.getName()
)
);
}
/**
* @param clazz Source class.
* @param propertyName Property name.