HHH-10714 - Add support for @Immutable attribute types

This commit is contained in:
soldierkam 2015-12-15 00:01:27 +01:00 committed by Vlad Mihalcea
parent d47fc93090
commit 87fb8af34f
9 changed files with 353 additions and 32 deletions

View File

@ -10,7 +10,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Mark an Entity or a Collection as immutable. No annotation means the element is mutable.
* Mark an Entity, a Collection, or an Attribute type as immutable. No annotation means the element is mutable.
* <p>
* An immutable entity may not be updated by the application. Updates to an immutable
* entity will be ignored, but no exception is thrown. &#064;Immutable must be used on root entities only.
@ -19,6 +19,10 @@ import java.lang.annotation.RetentionPolicy;
* &#064;Immutable placed on a collection makes the collection immutable, meaning additions and
* deletions to and from the collection are not allowed. A <i>HibernateException</i> is thrown in this case.
* </p>
* <p>
* An immutable attribute type will not be copied in the currently running Persistence Context in order to detect if the underlying value is dirty. As a result loading the entity will require less memory
* and checking changes will be much faster.
* </p>
*
* @author Emmanuel Bernard
*/

View File

@ -11,6 +11,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.hibernate.HibernateException;
import org.hibernate.annotations.Immutable;
import org.hibernate.type.descriptor.WrapperOptions;
import org.jboss.logging.Logger;
@ -129,20 +130,24 @@ public class JavaTypeDescriptorRegistry {
public static class FallbackJavaTypeDescriptor<T> extends AbstractTypeDescriptor<T> {
@SuppressWarnings("unchecked")
protected FallbackJavaTypeDescriptor(final Class<T> type) {
// MutableMutabilityPlan is the "safest" option, but we do not necessarily know how to deepCopy etc...
super(
type,
new MutableMutabilityPlan<T>() {
@Override
protected T deepCopyNotNull(T value) {
throw new HibernateException(
"Not known how to deep copy value of type: [" + type.getName() + "]"
);
}
}
);
super(type, createMutabilityPlan(type));
}
@SuppressWarnings("unchecked")
private static <T> MutabilityPlan<T> createMutabilityPlan(final Class<T> type) {
if ( type.isAnnotationPresent( Immutable.class ) ) {
return ImmutableMutabilityPlan.INSTANCE;
}
// MutableMutabilityPlan is the "safest" option, but we do not necessarily know how to deepCopy etc...
return new MutableMutabilityPlan<T>() {
@Override
protected T deepCopyNotNull(T value) {
throw new HibernateException(
"Not known how to deep copy value of type: [" + type.getName() + "]"
);
}
};
}
@Override

View File

@ -13,6 +13,7 @@ import java.sql.Blob;
import java.sql.SQLException;
import org.hibernate.HibernateException;
import org.hibernate.annotations.Immutable;
import org.hibernate.engine.jdbc.BinaryStream;
import org.hibernate.engine.jdbc.internal.BinaryStreamImpl;
import org.hibernate.internal.util.SerializationHelper;
@ -28,13 +29,11 @@ public class SerializableTypeDescriptor<T extends Serializable> extends Abstract
// unfortunately the param types cannot be the same so use something other than 'T' here to make that obvious
public static class SerializableMutabilityPlan<S extends Serializable> extends MutableMutabilityPlan<S> {
private final Class<S> type;
public static final SerializableMutabilityPlan<Serializable> INSTANCE
= new SerializableMutabilityPlan<Serializable>( Serializable.class );
= new SerializableMutabilityPlan<Serializable>( );
public SerializableMutabilityPlan(Class<S> type) {
this.type = type;
public SerializableMutabilityPlan() {
}
@Override
@ -45,14 +44,16 @@ public class SerializableTypeDescriptor<T extends Serializable> extends Abstract
}
@SuppressWarnings({ "unchecked" })
public SerializableTypeDescriptor(Class<T> type) {
super(
type,
Serializable.class.equals( type )
? (MutabilityPlan<T>) SerializableMutabilityPlan.INSTANCE
: new SerializableMutabilityPlan<T>( type )
);
super( type, createMutabilityPlan( type ) );
}
@SuppressWarnings({ "unchecked" })
private static <T> MutabilityPlan<T> createMutabilityPlan(Class<T> type) {
if ( type.isAnnotationPresent( Immutable.class ) ) {
return ImmutableMutabilityPlan.INSTANCE;
}
return (MutabilityPlan<T>) SerializableMutabilityPlan.INSTANCE;
}
public String toString(T value) {
@ -97,10 +98,10 @@ public class SerializableTypeDescriptor<T extends Serializable> extends Abstract
else if ( BinaryStream.class.isAssignableFrom( type ) ) {
return (X) new BinaryStreamImpl( toBytes( value ) );
}
else if ( Blob.class.isAssignableFrom( type )) {
return (X) options.getLobCreator().createBlob( toBytes(value) );
else if ( Blob.class.isAssignableFrom( type ) ) {
return (X) options.getLobCreator().createBlob( toBytes( value ) );
}
throw unknownUnwrap( type );
}
@ -115,12 +116,12 @@ public class SerializableTypeDescriptor<T extends Serializable> extends Abstract
else if ( InputStream.class.isInstance( value ) ) {
return fromBytes( DataHelper.extractBytes( (InputStream) value ) );
}
else if ( Blob.class.isInstance( value )) {
else if ( Blob.class.isInstance( value ) ) {
try {
return fromBytes( DataHelper.extractBytes( ( (Blob) value ).getBinaryStream() ) );
return fromBytes( DataHelper.extractBytes( ((Blob) value).getBinaryStream() ) );
}
catch ( SQLException e ) {
throw new HibernateException(e);
throw new HibernateException( e );
}
}
else if ( getJavaTypeClass().isInstance( value ) ) {

View File

@ -0,0 +1,48 @@
/*
* 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.annotations.immutable;
import org.hibernate.annotations.Immutable;
/**
* Created by soldier on 12.04.16.
*/
@Immutable
public class Caption {
private String text;
public Caption(String text) {
this.text = text;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Caption caption = (Caption) o;
return text != null ? text.equals( caption.text ) : caption.text == null;
}
@Override
public int hashCode() {
return text != null ? text.hashCode() : 0;
}
}

View File

@ -0,0 +1,25 @@
/*
* 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.annotations.immutable;
import javax.persistence.AttributeConverter;
/**
* Created by soldier on 12.04.16.
*/
public class CaptionConverter implements AttributeConverter<Caption, String> {
@Override
public String convertToDatabaseColumn(Caption attribute) {
return attribute.getText();
}
@Override
public Caption convertToEntityAttribute(String dbData) {
return new Caption( dbData );
}
}

View File

@ -0,0 +1,59 @@
/*
* 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.annotations.immutable;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.hibernate.annotations.Immutable;
/**
*
* @author soldierkam
*/
@Immutable
@SuppressWarnings("serial")
public class Exif implements Serializable {
private final Map<String, String> attributes;
public Exif(Map<String, String> attributes) {
this.attributes = new HashMap<>( attributes );
}
public Map<String, String> getAttributes() {
return attributes;
}
public String getAttribute(String name) {
return attributes.get( name );
}
@Override
public int hashCode() {
int hash = 7;
hash = 79 * hash + Objects.hashCode( this.attributes );
return hash;
}
@Override
public boolean equals(Object obj) {
if ( this == obj ) {
return true;
}
if ( obj == null ) {
return false;
}
if ( getClass() != obj.getClass() ) {
return false;
}
final Exif other = (Exif) obj;
return Objects.equals( this.attributes, other.attributes );
}
}

View File

@ -0,0 +1,30 @@
/*
* 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.annotations.immutable;
import java.util.Collections;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/**
*
* @author soldierkam
*/
@Converter(autoApply=true)
public class ExifConverter implements AttributeConverter<String, Exif> {
@Override
public Exif convertToDatabaseColumn(String attribute) {
return new Exif( Collections.singletonMap( "fakeAttr", attribute ) );
}
@Override
public String convertToEntityAttribute(Exif dbData) {
return dbData.getAttributes().get( "fakeAttr" );
}
}

View File

@ -8,6 +8,7 @@ package org.hibernate.test.annotations.immutable;
import javax.persistence.PersistenceException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.hibernate.AnnotationException;
@ -135,7 +136,90 @@ public class ImmutableTest extends BaseCoreFunctionalTestCase {
tx.commit();
s.close();
}
@Test
public void testImmutableAttribute(){
configuration().addAttributeConverter( ExifConverter.class);
configuration().addAttributeConverter( CaptionConverter.class);
Session s = openSession();
Transaction tx = s.beginTransaction();
Photo photo = new Photo();
photo.setName( "cat.jpg");
photo.setMetadata( new Exif(Collections.singletonMap( "fake", "first value")));
photo.setCaption( new Caption( "Cat.jpg caption" ) );
s.persist(photo);
tx.commit();
s.close();
// try changing the attribute
s = openSession();
tx = s.beginTransaction();
Photo cat = s.get(Photo.class, photo.getId());
assertNotNull(cat);
cat.getMetadata().getAttributes().put( "fake", "second value");
cat.getCaption().setText( "new caption" );
tx.commit();
s.close();
// retrieving the attribute again - it should be unmodified since object identity is the same
s = openSession();
tx = s.beginTransaction();
cat = s.get(Photo.class, photo.getId());
assertNotNull(cat);
assertEquals("Metadata should not have changed", "first value", cat.getMetadata().getAttribute( "fake"));
assertEquals("Caption should not have changed", "Cat.jpg caption", cat.getCaption().getText());
tx.commit();
s.close();
}
@Test
public void testChangeImmutableAttribute(){
configuration().addAttributeConverter( ExifConverter.class);
configuration().addAttributeConverter( CaptionConverter.class);
Session s = openSession();
Transaction tx = s.beginTransaction();
Photo photo = new Photo();
photo.setName( "cat.jpg");
photo.setMetadata( new Exif(Collections.singletonMap( "fake", "first value")));
photo.setCaption( new Caption( "Cat.jpg caption" ) );
s.persist(photo);
tx.commit();
s.close();
// replacing the attribute
s = openSession();
tx = s.beginTransaction();
Photo cat = s.get(Photo.class, photo.getId());
assertNotNull(cat);
cat.setMetadata( new Exif(Collections.singletonMap( "fake", "second value")));
cat.setCaption( new Caption( "new caption" ) );
tx.commit();
s.close();
// retrieving the attribute again - it should be modified since the holder object has changed as well
s = openSession();
tx = s.beginTransaction();
cat = s.get(Photo.class, photo.getId());
assertNotNull(cat);
assertEquals("Metadata should have changed", "second value", cat.getMetadata().getAttribute( "fake"));
assertEquals("Caption should have changed", "new caption", cat.getCaption().getText());
tx.commit();
s.close();
}
@Test
public void testMisplacedImmutableAnnotation() {
try {
@ -148,6 +232,6 @@ public class ImmutableTest extends BaseCoreFunctionalTestCase {
@Override
protected Class[] getAnnotatedClasses() {
return new Class[] { Country.class, State.class};
return new Class[] { Country.class, State.class, Photo.class };
}
}

View File

@ -0,0 +1,65 @@
/*
* 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.annotations.immutable;
import java.io.Serializable;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
*
* @author soldierkam
*/
@Entity
@SuppressWarnings("serial")
public class Photo implements Serializable {
private Integer id;
private String name;
private Exif metadata;
private Caption caption;
@Id
@GeneratedValue
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public void setId(Integer integer) {
id = integer;
}
public void setName(String string) {
name = string;
}
public Exif getMetadata() {
return metadata;
}
public void setMetadata(Exif metadata) {
this.metadata = metadata;
}
@Convert(converter = CaptionConverter.class)
public Caption getCaption() {
return caption;
}
public void setCaption(Caption caption) {
this.caption = caption;
}
}