Skip to content

Commit cc6fc2a

Browse files
committed
HHH-19849 Add an SPI that allows attaching session-scoped "extension storage" to the session/statelesssession implementors
1 parent 2b39aae commit cc6fc2a

File tree

6 files changed

+165
-0
lines changed

6 files changed

+165
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.engine.spi;
6+
7+
import org.hibernate.Incubating;
8+
9+
import java.util.function.Supplier;
10+
11+
/**
12+
* Marker interface for extensions to register themselves within a session instance.
13+
*
14+
* @see SharedSessionContractImplementor#getExtensionStorage(Class, Supplier)
15+
*/
16+
@Incubating
17+
public interface ExtensionStorage {
18+
}

hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
import java.util.Set;
7676
import java.util.TimeZone;
7777
import java.util.UUID;
78+
import java.util.function.Supplier;
7879

7980
/**
8081
* A wrapper class that delegates all method invocations to a delegate instance of
@@ -517,6 +518,11 @@ public RootGraphImplementor<?> getEntityGraph(String graphName) {
517518
return delegate.getEntityGraph( graphName );
518519
}
519520

521+
@Override
522+
public <T extends ExtensionStorage> T getExtensionStorage(Class<T> extension, Supplier<T> createIfMissing) {
523+
return delegate.getExtensionStorage( extension, createIfMissing );
524+
}
525+
520526
@Override
521527
public <T> QueryImplementor<T> createQuery(CriteriaSelect<T> selectQuery) {
522528
return delegate.createQuery( selectQuery );

hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import java.util.Set;
88
import java.util.UUID;
9+
import java.util.function.Supplier;
10+
911
import jakarta.persistence.TransactionRequiredException;
1012
import org.checkerframework.checker.nullness.qual.Nullable;
1113

@@ -621,4 +623,16 @@ default boolean isStatelessSession() {
621623

622624
@Override
623625
RootGraphImplementor<?> getEntityGraph(String graphName);
626+
627+
/**
628+
* Allows accessing session scoped extension storages of the particular session instance.
629+
*
630+
* @param extension The extension storage attached to the current session.
631+
* @param createIfMissing Creates a storage extension using the supplier,
632+
* if the current session does not yet have the particular storage type attached to this session.
633+
* @param <T> The type of the extension storage.
634+
*/
635+
@Incubating
636+
<T extends ExtensionStorage> T getExtensionStorage(Class<T> extension, Supplier<T> createIfMissing);
637+
624638
}

hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import java.util.Set;
5353
import java.util.TimeZone;
5454
import java.util.UUID;
55+
import java.util.function.Supplier;
5556

5657
/**
5758
* A wrapper class that delegates all method invocations to a delegate instance of
@@ -663,6 +664,11 @@ public RootGraphImplementor<?> getEntityGraph(String graphName) {
663664
return delegate.getEntityGraph( graphName );
664665
}
665666

667+
@Override
668+
public <T extends ExtensionStorage> T getExtensionStorage(Class<T> extension, Supplier<T> createIfMissing) {
669+
return delegate.getExtensionStorage( extension, createIfMissing );
670+
}
671+
666672
@Override
667673
public <T> List<EntityGraph<? super T>> getEntityGraphs(Class<T> entityClass) {
668674
return delegate.getEntityGraphs( entityClass );

hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.hibernate.engine.jdbc.spi.JdbcServices;
4242
import org.hibernate.engine.spi.EntityKey;
4343
import org.hibernate.engine.spi.ExceptionConverter;
44+
import org.hibernate.engine.spi.ExtensionStorage;
4445
import org.hibernate.engine.spi.LoadQueryInfluencers;
4546
import org.hibernate.engine.spi.SessionEventListenerManager;
4647
import org.hibernate.engine.spi.SessionFactoryImplementor;
@@ -111,12 +112,15 @@
111112
import java.io.Serial;
112113
import java.sql.Connection;
113114
import java.sql.SQLException;
115+
import java.util.HashMap;
114116
import java.util.List;
115117
import java.util.Locale;
118+
import java.util.Map;
116119
import java.util.Objects;
117120
import java.util.TimeZone;
118121
import java.util.UUID;
119122
import java.util.function.Function;
123+
import java.util.function.Supplier;
120124

121125
import static java.lang.Boolean.TRUE;
122126
import static org.hibernate.boot.model.naming.Identifier.toIdentifier;
@@ -186,6 +190,8 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont
186190
private transient ExceptionConverter exceptionConverter;
187191
private transient SessionAssociationMarkers sessionAssociationMarkers;
188192

193+
private transient Map<Class<?>, Object> extensionStorages;
194+
189195
public AbstractSharedSessionContract(SessionFactoryImpl factory, SessionCreationOptions options) {
190196
this.factory = factory;
191197

@@ -1704,6 +1710,20 @@ public SessionAssociationMarkers getSessionAssociationMarkers() {
17041710
return sessionAssociationMarkers;
17051711
}
17061712

1713+
@Override
1714+
public <T extends ExtensionStorage> T getExtensionStorage(Class<T> extension, Supplier<T> createIfMissing) {
1715+
if ( extensionStorages == null ) {
1716+
extensionStorages = new HashMap<>();
1717+
}
1718+
Object storage = extensionStorages.get( extension );
1719+
if ( storage == null ) {
1720+
storage = createIfMissing.get();
1721+
extensionStorages.put( extension, storage );
1722+
}
1723+
1724+
return extension.cast( storage );
1725+
}
1726+
17071727
@Serial
17081728
private void writeObject(ObjectOutputStream oos) throws IOException {
17091729
SESSION_LOGGER.serializingSession( getSessionIdentifier() );
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.engine.spi;
6+
7+
import jakarta.persistence.Id;
8+
import org.hibernate.engine.spi.ExtensionStorage;
9+
import org.hibernate.testing.orm.junit.DomainModel;
10+
import org.hibernate.testing.orm.junit.SessionFactory;
11+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
12+
import org.junit.jupiter.api.Test;
13+
14+
import java.util.HashMap;
15+
import java.util.Map;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
19+
20+
@DomainModel(annotatedClasses = {
21+
SessionExtensionTest.UselessEntity.class,
22+
})
23+
@SessionFactory
24+
public class SessionExtensionTest {
25+
26+
@Test
27+
public void failing(SessionFactoryScope scope) {
28+
scope.inSession( sessionImplementor -> {
29+
assertThatThrownBy(
30+
() -> sessionImplementor.getExtensionStorage( MySometimesFailingExtensionStorage.class, MySometimesFailingExtensionStorage::new ) )
31+
.isInstanceOf( UnsupportedOperationException.class );
32+
} );
33+
34+
scope.inStatelessSession( sessionImplementor -> {
35+
assertThatThrownBy(
36+
() -> sessionImplementor.getExtensionStorage( MySometimesFailingExtensionStorage.class, MySometimesFailingExtensionStorage::new ) )
37+
.isInstanceOf( UnsupportedOperationException.class );
38+
} );
39+
}
40+
41+
@Test
42+
public void supplier(SessionFactoryScope scope) {
43+
scope.inSession( sessionImplementor -> {
44+
sessionImplementor.getExtensionStorage( MySometimesFailingExtensionStorage.class,
45+
() -> new MySometimesFailingExtensionStorage( new HashMap<>() ) )
46+
.add( new Extension( 1 ) );
47+
48+
assertThat( sessionImplementor.getExtensionStorage( MySometimesFailingExtensionStorage.class, MySometimesFailingExtensionStorage::new ).get( 1 ) )
49+
.isNotNull()
50+
.isEqualTo( new Extension( 1 ) );
51+
52+
assertThat( sessionImplementor.getExtensionStorage( MySometimesFailingExtensionStorage.class,
53+
() -> new MySometimesFailingExtensionStorage( new HashMap<>() ) ).get( 1 ) )
54+
.isNotNull()
55+
.isEqualTo( new Extension( 1 ) );
56+
} );
57+
58+
scope.inStatelessSession( sessionImplementor -> {
59+
sessionImplementor.getExtensionStorage( MySometimesFailingExtensionStorage.class,
60+
() -> new MySometimesFailingExtensionStorage( new HashMap<>() ) )
61+
.add( new Extension( 1 ) );
62+
63+
assertThat( sessionImplementor.getExtensionStorage( MySometimesFailingExtensionStorage.class, MySometimesFailingExtensionStorage::new ).get( 1 ) )
64+
.isNotNull()
65+
.isEqualTo( new Extension( 1 ) );
66+
67+
assertThat( sessionImplementor.getExtensionStorage( MySometimesFailingExtensionStorage.class,
68+
() -> new MySometimesFailingExtensionStorage( new HashMap<>() ) ).get( 1 ) )
69+
.isNotNull()
70+
.isEqualTo( new Extension( 1 ) );
71+
} );
72+
}
73+
74+
public static class MySometimesFailingExtensionStorage implements ExtensionStorage {
75+
Map<Integer, Extension> extensions = new HashMap<>();
76+
77+
public MySometimesFailingExtensionStorage() {
78+
throw new UnsupportedOperationException();
79+
}
80+
81+
MySometimesFailingExtensionStorage(Map<Integer, Extension> extensions) {
82+
this.extensions = extensions;
83+
}
84+
85+
public void add(Extension extension) {
86+
extensions.put( extension.number, extension );
87+
}
88+
89+
public Extension get(int number) {
90+
return extensions.get( number );
91+
}
92+
}
93+
94+
public record Extension(int number) {
95+
}
96+
97+
static class UselessEntity {
98+
@Id
99+
Long id;
100+
}
101+
}

0 commit comments

Comments
 (0)