HHH-10714 - Add support for @Immutable attribute types
This commit is contained in:
parent
d47fc93090
commit
87fb8af34f
|
@ -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. @Immutable must be used on root entities only.
|
||||
|
@ -19,6 +19,10 @@ import java.lang.annotation.RetentionPolicy;
|
|||
* @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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ) ) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
|
||||
}
|
|
@ -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" );
|
||||
}
|
||||
|
||||
}
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue