HHH-17954 initial implementation of collection persistence for StatelessSession

Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
Gavin King 2024-04-13 14:31:16 +02:00
parent adec141a7f
commit 7f89c6260e
7 changed files with 147 additions and 19 deletions

View File

@ -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

View File

@ -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) );
} }

View File

@ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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 );
} }

View File

@ -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();
/** /**

View File

@ -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 );
} }

View File

@ -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<>();
}
}