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