Skip to content

Commit ea154f4

Browse files
committed
HHH-19708 prototype support for read/write replicas
1. allow a session to be created in a read-only mode 2. pass that mode through to the MultiTenantConnectionProvider
1 parent a8cc9e1 commit ea154f4

13 files changed

+174
-31
lines changed

hibernate-core/src/main/java/org/hibernate/Session.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,8 @@ public interface Session extends SharedSessionContract, EntityManager {
440440
*
441441
* @param readOnly {@code true}, the default for loaded entities/proxies is read-only;
442442
* {@code false}, the default for loaded entities/proxies is modifiable
443+
* @throws SessionException if the session was originally
444+
* {@linkplain SessionBuilder#readOnly created in read-only mode}
443445
*/
444446
void setDefaultReadOnly(boolean readOnly);
445447

hibernate-core/src/main/java/org/hibernate/SessionBuilder.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,29 @@ public interface SessionBuilder {
165165
*/
166166
SessionBuilder tenantIdentifier(Object tenantIdentifier);
167167

168+
/**
169+
* Specify a {@linkplain Session#isDefaultReadOnly read-only mode}
170+
* for the session.
171+
* <p>
172+
* If read/write replication is in use, then:
173+
* <ul>
174+
* <li>a read-only session will connect to a read-only replica, but
175+
* <li>a non-read-only session will connect to a writable replica.
176+
* </ul>
177+
* <p>
178+
* If a session is created in read-only mode, then it cannot be
179+
* changed to read-write mode, and any call to
180+
* {@link Session#setDefaultReadOnly(boolean)} with fail. On the
181+
* other hand, if a session is created in read-write mode, then it
182+
* may later be switched to read-only mode, but all database access
183+
* is directed to the writable replica.
184+
*
185+
* @return {@code this}, for method chaining
186+
* @since 7.2
187+
*/
188+
@Incubating
189+
SessionBuilder readOnly(boolean readOnly);
190+
168191
/**
169192
* Add one or more {@link SessionEventListener} instances to the list of
170193
* listeners for the new session to be built.

hibernate-core/src/main/java/org/hibernate/StatelessSessionBuilder.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public interface StatelessSessionBuilder {
4141
* @return {@code this}, for method chaining
4242
* @deprecated Use {@link #tenantIdentifier(Object)} instead
4343
*/
44-
@Deprecated(forRemoval = true)
44+
@Deprecated(since = "6.4", forRemoval = true)
4545
StatelessSessionBuilder tenantIdentifier(String tenantIdentifier);
4646

4747
/**
@@ -54,13 +54,28 @@ public interface StatelessSessionBuilder {
5454
*/
5555
StatelessSessionBuilder tenantIdentifier(Object tenantIdentifier);
5656

57+
/**
58+
* Specify a read-only mode for the stateless session.
59+
* <p>
60+
* If read/write replication is in use, then:
61+
* <ul>
62+
* <li>a read-only session will connect to a read-only replica, but
63+
* <li>a non-read-only session will connect to a writable replica.
64+
* </ul>
65+
*
66+
* @return {@code this}, for method chaining
67+
* @since 7.2
68+
*/
69+
@Incubating
70+
StatelessSessionBuilder readOnly(boolean readOnly);
71+
5772
/**
5873
* Applies the given statement inspection function to the session.
5974
*
6075
* @param operator An operator which accepts a SQL string, returning
6176
* a processed SQL string to be used by Hibernate
62-
* instead of the given original SQL. Alternatively.
63-
* the operator may work by side effect, and simply
77+
* instead of the given original SQL. Alternatively,
78+
* the operator may work by side effect and simply
6479
* return the original SQL.
6580
*
6681
* @return {@code this}, for method chaining

hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/MultiTenantConnectionProvider.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,55 @@ public interface MultiTenantConnectionProvider<T> extends Service, Wrapped {
5757
* @return The obtained JDBC connection
5858
*
5959
* @throws SQLException Indicates a problem opening a connection
60-
* @throws org.hibernate.HibernateException Indicates a problem otherwise obtaining a connection.
60+
* @throws org.hibernate.HibernateException Indicates a problem obtaining a connection
6161
*/
6262
Connection getConnection(T tenantIdentifier) throws SQLException;
6363

64+
/**
65+
* Obtains a connection to a read-only replica for use according to the underlying
66+
* strategy of this provider.
67+
*
68+
* @param tenantIdentifier The identifier of the tenant for which to get a connection
69+
*
70+
* @return The obtained JDBC connection
71+
*
72+
* @throws SQLException Indicates a problem opening a connection
73+
* @throws org.hibernate.HibernateException Indicates a problem obtaining a connection
74+
*
75+
* @since 7.2
76+
*/
77+
default Connection getReadOnlyConnection(T tenantIdentifier)
78+
throws SQLException {
79+
throw new UnsupportedOperationException( "No read-only replica is available" );
80+
}
81+
6482
/**
6583
* Release a connection from Hibernate use.
6684
*
6785
* @param connection The JDBC connection to release
6886
* @param tenantIdentifier The identifier of the tenant.
6987
*
7088
* @throws SQLException Indicates a problem closing the connection
71-
* @throws org.hibernate.HibernateException Indicates a problem otherwise releasing a connection.
89+
* @throws org.hibernate.HibernateException Indicates a problem releasing a connection
7290
*/
7391
void releaseConnection(T tenantIdentifier, Connection connection) throws SQLException;
7492

93+
/**
94+
* Release a connection to a read-only replica from Hibernate use.
95+
*
96+
* @param connection The JDBC connection to release
97+
* @param tenantIdentifier The identifier of the tenant.
98+
*
99+
* @throws SQLException Indicates a problem closing the connection
100+
* @throws org.hibernate.HibernateException Indicates a problem releasing a connection
101+
*
102+
* @since 7.2
103+
*/
104+
default void releaseReadOnlyConnection(T tenantIdentifier, Connection connection)
105+
throws SQLException {
106+
throw new UnsupportedOperationException( "No read-only replica is available" );
107+
}
108+
75109
/**
76110
* Does this connection provider support aggressive release of JDBC connections and later
77111
* re-acquisition of those connections if needed?

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ public SessionBuilder tenantIdentifier(Object tenantIdentifier) {
100100
return this;
101101
}
102102

103+
@Override
104+
public SessionBuilder readOnly(boolean readOnly) {
105+
delegate.readOnly( readOnly );
106+
return this;
107+
}
108+
103109
@Override
104110
public SessionBuilder eventListeners(SessionEventListener... listeners) {
105111
delegate.eventListeners( listeners );

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ public SharedSessionBuilder tenantIdentifier(Object tenantIdentifier) {
137137
return this;
138138
}
139139

140+
@Override
141+
public SessionBuilder readOnly(boolean readOnly) {
142+
delegate.readOnly( readOnly );
143+
return this;
144+
}
145+
140146
@Override
141147
public SharedSessionBuilder eventListeners(SessionEventListener... listeners) {
142148
delegate.eventListeners( listeners );

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont
161161
private final Interceptor interceptor;
162162

163163
private final Object tenantIdentifier;
164+
private final boolean readOnly;
164165
private final TimeZone jdbcTimeZone;
165166

166167
// mutable state
@@ -190,6 +191,7 @@ public AbstractSharedSessionContract(SessionFactoryImpl factory, SessionCreation
190191

191192
cacheTransactionSynchronization = factory.getCache().getRegionFactory().createTransactionContext( this );
192193
tenantIdentifier = getTenantId( factoryOptions, options );
194+
readOnly = options.isReadOnly();
193195
interceptor = interpret( options.getInterceptor() );
194196
jdbcTimeZone = options.getJdbcTimeZone();
195197
sessionEventsManager = createSessionEventsManager( factoryOptions, options );
@@ -296,9 +298,13 @@ private static Object getTenantId( SessionFactoryOptions factoryOptions, Session
296298
return tenantIdentifier;
297299
}
298300

301+
boolean isReadOnly() {
302+
return readOnly;
303+
}
304+
299305
private static SessionEventListenerManager createSessionEventsManager(
300306
SessionFactoryOptions factoryOptions, SessionCreationOptions options) {
301-
final List<SessionEventListener> customListeners = options.getCustomSessionEventListener();
307+
final var customListeners = options.getCustomSessionEventListener();
302308
return customListeners == null
303309
? new SessionEventListenerManagerImpl( factoryOptions.buildSessionEventListeners() )
304310
: new SessionEventListenerManagerImpl( customListeners );
@@ -694,6 +700,7 @@ public JdbcConnectionAccess getJdbcConnectionAccess() {
694700
// we're using datasource-based multitenancy
695701
jdbcConnectionAccess = new ContextualJdbcConnectionAccess(
696702
tenantIdentifier,
703+
readOnly,
697704
sessionEventsManager,
698705
factory.multiTenantConnectionProvider,
699706
this

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,26 @@
1313
import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess;
1414
import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
1515
import org.hibernate.engine.spi.SharedSessionContractImplementor;
16-
import org.hibernate.event.monitor.spi.EventMonitor;
17-
import org.hibernate.event.monitor.spi.DiagnosticEvent;
1816

1917
/**
2018
* @author Steve Ebersole
2119
*/
2220
public class ContextualJdbcConnectionAccess implements JdbcConnectionAccess, Serializable {
2321
private final Object tenantIdentifier;
22+
private final boolean readOnly;
2423
private final SessionEventListener listener;
2524
private final MultiTenantConnectionProvider<Object> connectionProvider;
2625
private final SharedSessionContractImplementor session;
2726

2827

2928
public ContextualJdbcConnectionAccess(
3029
Object tenantIdentifier,
30+
boolean readOnly,
3131
SessionEventListener listener,
3232
MultiTenantConnectionProvider<Object> connectionProvider,
3333
SharedSessionContractImplementor session) {
3434
this.tenantIdentifier = tenantIdentifier;
35+
this.readOnly = readOnly;
3536
this.listener = listener;
3637
this.connectionProvider = connectionProvider;
3738
this.session = session;
@@ -43,11 +44,13 @@ public Connection obtainConnection() throws SQLException {
4344
throw new HibernateException( "Tenant identifier required" );
4445
}
4546

46-
final EventMonitor eventMonitor = session.getEventMonitor();
47-
final DiagnosticEvent connectionAcquisitionEvent = eventMonitor.beginJdbcConnectionAcquisitionEvent();
47+
final var eventMonitor = session.getEventMonitor();
48+
final var connectionAcquisitionEvent = eventMonitor.beginJdbcConnectionAcquisitionEvent();
4849
try {
4950
listener.jdbcConnectionAcquisitionStart();
50-
return connectionProvider.getConnection( tenantIdentifier );
51+
return readOnly
52+
? connectionProvider.getReadOnlyConnection( tenantIdentifier )
53+
: connectionProvider.getConnection( tenantIdentifier );
5154
}
5255
finally {
5356
eventMonitor.completeJdbcConnectionAcquisitionEvent( connectionAcquisitionEvent, session, tenantIdentifier );
@@ -61,11 +64,16 @@ public void releaseConnection(Connection connection) throws SQLException {
6164
throw new HibernateException( "Tenant identifier required" );
6265
}
6366

64-
final EventMonitor eventMonitor = session.getEventMonitor();
65-
final DiagnosticEvent connectionReleaseEvent = eventMonitor.beginJdbcConnectionReleaseEvent();
67+
final var eventMonitor = session.getEventMonitor();
68+
final var connectionReleaseEvent = eventMonitor.beginJdbcConnectionReleaseEvent();
6669
try {
6770
listener.jdbcConnectionReleaseStart();
68-
connectionProvider.releaseConnection( tenantIdentifier, connection );
71+
if ( readOnly ) {
72+
connectionProvider.releaseReadOnlyConnection( tenantIdentifier, connection );
73+
}
74+
else {
75+
connectionProvider.releaseConnection( tenantIdentifier, connection );
76+
}
6977
}
7078
finally {
7179
eventMonitor.completeJdbcConnectionReleaseEvent( connectionReleaseEvent, session, tenantIdentifier );

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
1414
import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess;
1515
import org.hibernate.engine.spi.SharedSessionContractImplementor;
16-
import org.hibernate.event.monitor.spi.EventMonitor;
17-
import org.hibernate.event.monitor.spi.DiagnosticEvent;
1816

1917
/**
2018
* @author Steve Ebersole
@@ -37,8 +35,8 @@ public NonContextualJdbcConnectionAccess(
3735

3836
@Override
3937
public Connection obtainConnection() throws SQLException {
40-
final EventMonitor eventMonitor = session.getEventMonitor();
41-
final DiagnosticEvent connectionAcquisitionEvent = eventMonitor.beginJdbcConnectionAcquisitionEvent();
38+
final var eventMonitor = session.getEventMonitor();
39+
final var connectionAcquisitionEvent = eventMonitor.beginJdbcConnectionAcquisitionEvent();
4240
try {
4341
listener.jdbcConnectionAcquisitionStart();
4442
return connectionProvider.getConnection();
@@ -51,8 +49,8 @@ public Connection obtainConnection() throws SQLException {
5149

5250
@Override
5351
public void releaseConnection(Connection connection) throws SQLException {
54-
final EventMonitor eventMonitor = session.getEventMonitor();
55-
final DiagnosticEvent connectionReleaseEvent = eventMonitor.beginJdbcConnectionReleaseEvent();
52+
final var eventMonitor = session.getEventMonitor();
53+
final var connectionReleaseEvent = eventMonitor.beginJdbcConnectionReleaseEvent();
5654
try {
5755
listener.jdbcConnectionReleaseStart();
5856
connectionProvider.closeConnection( connection );

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public interface SessionCreationOptions {
4646

4747
Object getTenantIdentifierValue();
4848

49+
boolean isReadOnly();
50+
4951
boolean isIdentifierRollbackEnabled();
5052

5153
TimeZone getJdbcTimeZone();

0 commit comments

Comments
 (0)