HHH-17954 initial implementation of collection persistence for StatelessSession
Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
parent
adec141a7f
commit
7f89c6260e
|
@ -27,15 +27,15 @@ import org.hibernate.graph.GraphSemantic;
|
||||||
* <li>it does not have a first-level cache,
|
* <li>it does not have a first-level cache,
|
||||||
* <li>nor interact with any second-level cache,
|
* <li>nor interact with any second-level cache,
|
||||||
* <li>nor does it implement transactional write-behind or automatic dirty
|
* <li>nor does it implement transactional write-behind or automatic dirty
|
||||||
* checking, and
|
* checking.
|
||||||
* <li>nor do operations cascade to associated instances.
|
|
||||||
* </ul>
|
* </ul>
|
||||||
* <p>
|
* <p>
|
||||||
* Furthermore:
|
* Furthermore, operations performed via a stateless session:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>collections are completely ignored by a stateless session, and
|
* <li>never cascade to associated instances, no matter what the
|
||||||
* <li>operations performed via a stateless session bypass Hibernate's event
|
* {@link jakarta.persistence.CascadeType}, and
|
||||||
* model, lifecycle callbacks, and interceptors.
|
* <li>bypass Hibernate's {@linkplain org.hibernate.event.spi event model},
|
||||||
|
* lifecycle callbacks, and {@linkplain Interceptor interceptors}.
|
||||||
* </ul>
|
* </ul>
|
||||||
* <p>
|
* <p>
|
||||||
* Stateless sessions are vulnerable to data aliasing effects, due to the
|
* Stateless sessions are vulnerable to data aliasing effects, due to the
|
||||||
|
|
|
@ -404,10 +404,10 @@ public final class CollectionEntry implements Serializable {
|
||||||
// does the collection already have
|
// does the collection already have
|
||||||
// it's own up-to-date snapshot?
|
// it's own up-to-date snapshot?
|
||||||
final CollectionPersister loadedPersister = getLoadedPersister();
|
final CollectionPersister loadedPersister = getLoadedPersister();
|
||||||
Serializable snapshot = getSnapshot();
|
final Serializable snapshot = getSnapshot();
|
||||||
return collection.wasInitialized() &&
|
return collection.wasInitialized()
|
||||||
( loadedPersister == null || loadedPersister.isMutable() ) &&
|
&& ( loadedPersister == null || loadedPersister.isMutable() )
|
||||||
(snapshot != null ? collection.isSnapshotEmpty( snapshot ) : true);
|
&& ( snapshot == null || collection.isSnapshotEmpty(snapshot) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
package org.hibernate.internal;
|
package org.hibernate.internal;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
import org.hibernate.CacheMode;
|
import org.hibernate.CacheMode;
|
||||||
import org.hibernate.FlushMode;
|
import org.hibernate.FlushMode;
|
||||||
|
@ -19,6 +20,7 @@ import org.hibernate.UnresolvableObjectException;
|
||||||
import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor;
|
import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor;
|
||||||
import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata;
|
import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata;
|
||||||
import org.hibernate.cache.spi.access.EntityDataAccess;
|
import org.hibernate.cache.spi.access.EntityDataAccess;
|
||||||
|
import org.hibernate.collection.spi.CollectionSemantics;
|
||||||
import org.hibernate.collection.spi.PersistentCollection;
|
import org.hibernate.collection.spi.PersistentCollection;
|
||||||
import org.hibernate.engine.internal.StatefulPersistenceContext;
|
import org.hibernate.engine.internal.StatefulPersistenceContext;
|
||||||
import org.hibernate.engine.spi.EffectiveEntityGraph;
|
import org.hibernate.engine.spi.EffectiveEntityGraph;
|
||||||
|
@ -36,6 +38,7 @@ import org.hibernate.generator.values.GeneratedValues;
|
||||||
import org.hibernate.graph.GraphSemantic;
|
import org.hibernate.graph.GraphSemantic;
|
||||||
import org.hibernate.graph.spi.RootGraphImplementor;
|
import org.hibernate.graph.spi.RootGraphImplementor;
|
||||||
import org.hibernate.loader.ast.spi.CascadingFetchProfile;
|
import org.hibernate.loader.ast.spi.CascadingFetchProfile;
|
||||||
|
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
|
||||||
import org.hibernate.persister.collection.CollectionPersister;
|
import org.hibernate.persister.collection.CollectionPersister;
|
||||||
import org.hibernate.persister.entity.EntityPersister;
|
import org.hibernate.persister.entity.EntityPersister;
|
||||||
import org.hibernate.proxy.LazyInitializer;
|
import org.hibernate.proxy.LazyInitializer;
|
||||||
|
@ -118,10 +121,11 @@ public class StatelessSessionImpl extends AbstractSharedSessionContract implemen
|
||||||
id = castNonNull( generatedValues ).getGeneratedValue( persister.getIdentifierMapping() );
|
id = castNonNull( generatedValues ).getGeneratedValue( persister.getIdentifierMapping() );
|
||||||
}
|
}
|
||||||
persister.setIdentifier( entity, id, this );
|
persister.setIdentifier( entity, id, this );
|
||||||
|
forEachOwnedCollection( entity, id, persister,
|
||||||
|
(descriptor, collection) -> descriptor.recreate( collection, id, this) );
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// deletes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
// deletes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -136,6 +140,8 @@ public class StatelessSessionImpl extends AbstractSharedSessionContract implemen
|
||||||
final EntityPersister persister = getEntityPersister( entityName, entity );
|
final EntityPersister persister = getEntityPersister( entityName, entity );
|
||||||
final Object id = persister.getIdentifier( entity, this );
|
final Object id = persister.getIdentifier( entity, this );
|
||||||
final Object version = persister.getVersion( entity );
|
final Object version = persister.getVersion( entity );
|
||||||
|
forEachOwnedCollection( entity, id, persister,
|
||||||
|
(descriptor, collection) -> descriptor.remove(id, this) );
|
||||||
persister.getDeleteCoordinator().delete( entity, id, version, this );
|
persister.getDeleteCoordinator().delete( entity, id, version, this );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,6 +177,11 @@ public class StatelessSessionImpl extends AbstractSharedSessionContract implemen
|
||||||
oldVersion = null;
|
oldVersion = null;
|
||||||
}
|
}
|
||||||
persister.getUpdateCoordinator().update( entity, id, null, state, oldVersion, null, null, false, this );
|
persister.getUpdateCoordinator().update( entity, id, null, state, oldVersion, null, null, false, this );
|
||||||
|
// TODO: can we do better here?
|
||||||
|
forEachOwnedCollection( entity, id, persister,
|
||||||
|
(descriptor, collection) -> descriptor.remove(id, this) );
|
||||||
|
forEachOwnedCollection( entity, id, persister,
|
||||||
|
(descriptor, collection) -> descriptor.recreate( collection, id, this) );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -181,6 +192,11 @@ public class StatelessSessionImpl extends AbstractSharedSessionContract implemen
|
||||||
final Object[] state = persister.getValues( entity );
|
final Object[] state = persister.getValues( entity );
|
||||||
final Object oldVersion = versionToUpsert( entity, persister, state );
|
final Object oldVersion = versionToUpsert( entity, persister, state );
|
||||||
persister.getMergeCoordinator().update( entity, id, null, state, oldVersion, null, null, false, this );
|
persister.getMergeCoordinator().update( entity, id, null, state, oldVersion, null, null, false, this );
|
||||||
|
// TODO: can we do better here?
|
||||||
|
forEachOwnedCollection( entity, id, persister,
|
||||||
|
(descriptor, collection) -> descriptor.remove(id, this) );
|
||||||
|
forEachOwnedCollection( entity, id, persister,
|
||||||
|
(descriptor, collection) -> descriptor.recreate( collection, id, this) );
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object versionToUpsert(Object entity, EntityPersister persister, Object[] state) {
|
private Object versionToUpsert(Object entity, EntityPersister persister, Object[] state) {
|
||||||
|
@ -223,6 +239,45 @@ public class StatelessSessionImpl extends AbstractSharedSessionContract implemen
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collections ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
private void forEachOwnedCollection(
|
||||||
|
Object entity, Object key,
|
||||||
|
EntityPersister persister, BiConsumer<CollectionPersister, PersistentCollection<?>> action) {
|
||||||
|
persister.visitAttributeMappings( att -> {
|
||||||
|
if ( att.isPluralAttributeMapping() ) {
|
||||||
|
final PluralAttributeMapping pluralAttributeMapping = att.asPluralAttributeMapping();
|
||||||
|
final CollectionPersister descriptor = pluralAttributeMapping.getCollectionDescriptor();
|
||||||
|
if ( !descriptor.isInverse() ) {
|
||||||
|
final Object collection = att.getPropertyAccess().getGetter().get(entity);
|
||||||
|
final PersistentCollection<?> persistentCollection;
|
||||||
|
if (collection instanceof PersistentCollection) {
|
||||||
|
persistentCollection = (PersistentCollection<?>) collection;
|
||||||
|
if ( !persistentCollection.wasInitialized() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
persistentCollection = collection == null
|
||||||
|
? instantiateEmpty(key, descriptor)
|
||||||
|
: wrap(descriptor, collection);
|
||||||
|
}
|
||||||
|
action.accept(descriptor, persistentCollection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
private PersistentCollection<?> instantiateEmpty(Object key, CollectionPersister descriptor) {
|
||||||
|
return descriptor.getCollectionSemantics().instantiateWrapper(key, descriptor, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: is this the right way to do this?
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
private PersistentCollection<?> wrap(CollectionPersister descriptor, Object collection) {
|
||||||
|
final CollectionSemantics collectionSemantics = descriptor.getCollectionSemantics();
|
||||||
|
return collectionSemantics.wrap(collection, descriptor, this);
|
||||||
|
}
|
||||||
|
|
||||||
// loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
// loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -437,11 +437,8 @@ public interface EntityMappingType
|
||||||
/**
|
/**
|
||||||
* Visit the mappings, but limited to just attributes defined
|
* Visit the mappings, but limited to just attributes defined
|
||||||
* in the targetType or its super-type(s) if any.
|
* in the targetType or its super-type(s) if any.
|
||||||
*
|
|
||||||
* @apiNote Passing {@code null} indicates that subclasses should be included. This
|
|
||||||
* matches legacy non-TREAT behavior and meets the need for EntityGraph processing
|
|
||||||
*/
|
*/
|
||||||
default void visitAttributeMappings(Consumer<? super AttributeMapping> action, EntityMappingType targetType) {
|
default void visitAttributeMappings(Consumer<? super AttributeMapping> action) {
|
||||||
getAttributeMappings().forEach( action );
|
getAttributeMappings().forEach( action );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -145,7 +145,7 @@ public interface CollectionPersister extends Restrictable {
|
||||||
Class<?> getElementClass();
|
Class<?> getElementClass();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is this an array or primitive values?
|
* Is this an array of primitive values?
|
||||||
*/
|
*/
|
||||||
boolean isPrimitiveArray();
|
boolean isPrimitiveArray();
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6253,9 +6253,7 @@ public abstract class AbstractEntityPersister
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void visitAttributeMappings(
|
public void visitAttributeMappings(Consumer<? super AttributeMapping> action) {
|
||||||
Consumer<? super AttributeMapping> action,
|
|
||||||
EntityMappingType targetType) {
|
|
||||||
attributeMappings.forEach( action );
|
attributeMappings.forEach( action );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
package org.hibernate.orm.test.stateless;
|
||||||
|
|
||||||
|
import jakarta.persistence.ElementCollection;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import org.hibernate.testing.orm.junit.DomainModel;
|
||||||
|
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||||
|
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.hibernate.Hibernate.isInitialized;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
@SessionFactory
|
||||||
|
@DomainModel(annotatedClasses = StatelessCollectionsTest.WithCollection.class)
|
||||||
|
public class StatelessCollectionsTest {
|
||||||
|
@Test
|
||||||
|
void test(SessionFactoryScope scope) {
|
||||||
|
WithCollection inserted = new WithCollection();
|
||||||
|
inserted.name = "Gavin";
|
||||||
|
inserted.elements.add("Hello");
|
||||||
|
inserted.elements.add("World");
|
||||||
|
scope.inStatelessTransaction(s -> s.insert(inserted));
|
||||||
|
|
||||||
|
scope.inStatelessTransaction(s -> {
|
||||||
|
WithCollection loaded = s.get(WithCollection.class, inserted.id);
|
||||||
|
assertFalse(isInitialized(loaded.elements));
|
||||||
|
s.fetch(loaded.elements);
|
||||||
|
assertTrue(isInitialized(loaded.elements));
|
||||||
|
assertEquals(2, loaded.elements.size());
|
||||||
|
|
||||||
|
loaded.elements.add("Goodbye");
|
||||||
|
s.update(loaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.inStatelessTransaction(s -> {
|
||||||
|
WithCollection loaded = s.get(WithCollection.class, inserted.id);
|
||||||
|
assertFalse(isInitialized(loaded.elements));
|
||||||
|
s.fetch(loaded.elements);
|
||||||
|
assertTrue(isInitialized(loaded.elements));
|
||||||
|
assertEquals(3, loaded.elements.size());
|
||||||
|
|
||||||
|
loaded.elements.remove("Hello");
|
||||||
|
s.update(loaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.inStatelessTransaction(s -> {
|
||||||
|
WithCollection loaded = s.get(WithCollection.class, inserted.id);
|
||||||
|
assertFalse(isInitialized(loaded.elements));
|
||||||
|
s.fetch(loaded.elements);
|
||||||
|
assertTrue(isInitialized(loaded.elements));
|
||||||
|
assertEquals(2, loaded.elements.size());
|
||||||
|
|
||||||
|
s.delete(loaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.inStatelessTransaction(s -> {
|
||||||
|
WithCollection loaded = s.get(WithCollection.class, inserted.id);
|
||||||
|
assertNull(loaded);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity(name = "EntityWithCollection")
|
||||||
|
static class WithCollection {
|
||||||
|
@GeneratedValue @Id
|
||||||
|
Long id;
|
||||||
|
String name;
|
||||||
|
@ElementCollection
|
||||||
|
Set<String> elements = new HashSet<>();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue