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>nor interact with any second-level cache,
* <li>nor does it implement transactional write-behind or automatic dirty
* checking, and
* <li>nor do operations cascade to associated instances.
* checking.
* </ul>
* <p>
* Furthermore:
* Furthermore, operations performed via a stateless session:
* <ul>
* <li>collections are completely ignored by a stateless session, and
* <li>operations performed via a stateless session bypass Hibernate's event
* model, lifecycle callbacks, and interceptors.
* <li>never cascade to associated instances, no matter what the
* {@link jakarta.persistence.CascadeType}, and
* <li>bypass Hibernate's {@linkplain org.hibernate.event.spi event model},
* lifecycle callbacks, and {@linkplain Interceptor interceptors}.
* </ul>
* <p>
* 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
// it's own up-to-date snapshot?
final CollectionPersister loadedPersister = getLoadedPersister();
Serializable snapshot = getSnapshot();
return collection.wasInitialized() &&
( loadedPersister == null || loadedPersister.isMutable() ) &&
(snapshot != null ? collection.isSnapshotEmpty( snapshot ) : true);
final Serializable snapshot = getSnapshot();
return collection.wasInitialized()
&& ( loadedPersister == null || loadedPersister.isMutable() )
&& ( snapshot == null || collection.isSnapshotEmpty(snapshot) );
}

View File

@ -7,6 +7,7 @@
package org.hibernate.internal;
import java.util.Set;
import java.util.function.BiConsumer;
import org.hibernate.CacheMode;
import org.hibernate.FlushMode;
@ -19,6 +20,7 @@ import org.hibernate.UnresolvableObjectException;
import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor;
import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata;
import org.hibernate.cache.spi.access.EntityDataAccess;
import org.hibernate.collection.spi.CollectionSemantics;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.engine.internal.StatefulPersistenceContext;
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.spi.RootGraphImplementor;
import org.hibernate.loader.ast.spi.CascadingFetchProfile;
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.proxy.LazyInitializer;
@ -118,10 +121,11 @@ public class StatelessSessionImpl extends AbstractSharedSessionContract implemen
id = castNonNull( generatedValues ).getGeneratedValue( persister.getIdentifierMapping() );
}
persister.setIdentifier( entity, id, this );
forEachOwnedCollection( entity, id, persister,
(descriptor, collection) -> descriptor.recreate( collection, id, this) );
return id;
}
// deletes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@Override
@ -136,6 +140,8 @@ public class StatelessSessionImpl extends AbstractSharedSessionContract implemen
final EntityPersister persister = getEntityPersister( entityName, entity );
final Object id = persister.getIdentifier( entity, this );
final Object version = persister.getVersion( entity );
forEachOwnedCollection( entity, id, persister,
(descriptor, collection) -> descriptor.remove(id, this) );
persister.getDeleteCoordinator().delete( entity, id, version, this );
}
@ -171,6 +177,11 @@ public class StatelessSessionImpl extends AbstractSharedSessionContract implemen
oldVersion = null;
}
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
@ -181,6 +192,11 @@ public class StatelessSessionImpl extends AbstractSharedSessionContract implemen
final Object[] state = persister.getValues( entity );
final Object oldVersion = versionToUpsert( entity, persister, state );
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) {
@ -223,6 +239,45 @@ public class StatelessSessionImpl extends AbstractSharedSessionContract implemen
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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -437,11 +437,8 @@ public interface EntityMappingType
/**
* Visit the mappings, but limited to just attributes defined
* 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 );
}

View File

@ -145,7 +145,7 @@ public interface CollectionPersister extends Restrictable {
Class<?> getElementClass();
/**
* Is this an array or primitive values?
* Is this an array of primitive values?
*/
boolean isPrimitiveArray();
/**

View File

@ -6253,9 +6253,7 @@ public abstract class AbstractEntityPersister
}
@Override
public void visitAttributeMappings(
Consumer<? super AttributeMapping> action,
EntityMappingType targetType) {
public void visitAttributeMappings(Consumer<? super AttributeMapping> 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<>();
}
}