Skip to content

Commit 6d99568

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 6d99568

File tree

10 files changed

+162
-28
lines changed

10 files changed

+162
-28
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/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 & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ public NonContextualJdbcConnectionAccess(
3737

3838
@Override
3939
public Connection obtainConnection() throws SQLException {
40-
final EventMonitor eventMonitor = session.getEventMonitor();
41-
final DiagnosticEvent connectionAcquisitionEvent = eventMonitor.beginJdbcConnectionAcquisitionEvent();
40+
final var eventMonitor = session.getEventMonitor();
41+
final var connectionAcquisitionEvent = eventMonitor.beginJdbcConnectionAcquisitionEvent();
4242
try {
4343
listener.jdbcConnectionAcquisitionStart();
4444
return connectionProvider.getConnection();
@@ -51,8 +51,8 @@ public Connection obtainConnection() throws SQLException {
5151

5252
@Override
5353
public void releaseConnection(Connection connection) throws SQLException {
54-
final EventMonitor eventMonitor = session.getEventMonitor();
55-
final DiagnosticEvent connectionReleaseEvent = eventMonitor.beginJdbcConnectionReleaseEvent();
54+
final var eventMonitor = session.getEventMonitor();
55+
final var connectionReleaseEvent = eventMonitor.beginJdbcConnectionReleaseEvent();
5656
try {
5757
listener.jdbcConnectionReleaseStart();
5858
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();

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import org.hibernate.HibernateException;
3535
import org.hibernate.Interceptor;
3636
import org.hibernate.Session;
37-
import org.hibernate.SessionBuilder;
3837
import org.hibernate.SessionEventListener;
3938
import org.hibernate.SessionFactory;
4039
import org.hibernate.SessionFactoryObserver;
@@ -1130,6 +1129,7 @@ public static class SessionBuilderImpl implements SessionBuilderImplementor, Ses
11301129
private boolean autoClose;
11311130
private boolean autoClear;
11321131
private Object tenantIdentifier;
1132+
private boolean readOnly;
11331133
private boolean identifierRollback;
11341134
private TimeZone jdbcTimeZone;
11351135
private boolean explicitNoInterceptor;
@@ -1238,6 +1238,11 @@ public Object getTenantIdentifierValue() {
12381238
return tenantIdentifier;
12391239
}
12401240

1241+
@Override
1242+
public boolean isReadOnly() {
1243+
return readOnly;
1244+
}
1245+
12411246
@Override
12421247
public boolean isIdentifierRollbackEnabled() {
12431248
return identifierRollback;
@@ -1283,7 +1288,7 @@ public SessionBuilderImpl statementInspector(StatementInspector statementInspect
12831288
}
12841289

12851290
@Override
1286-
public SessionBuilder statementInspector(UnaryOperator<String> operator) {
1291+
public SessionBuilderImpl statementInspector(UnaryOperator<String> operator) {
12871292
this.statementInspector = operator::apply;
12881293
return this;
12891294
}
@@ -1301,7 +1306,7 @@ public SessionBuilderImpl connectionHandlingMode(PhysicalConnectionHandlingMode
13011306
}
13021307

13031308
@Override
1304-
public SessionBuilder connectionHandling(ConnectionAcquisitionMode acquisitionMode, ConnectionReleaseMode releaseMode) {
1309+
public SessionBuilderImpl connectionHandling(ConnectionAcquisitionMode acquisitionMode, ConnectionReleaseMode releaseMode) {
13051310
this.connectionHandlingMode = PhysicalConnectionHandlingMode.interpret( acquisitionMode, releaseMode);
13061311
return this;
13071312
}
@@ -1343,7 +1348,13 @@ public SessionBuilderImpl tenantIdentifier(Object tenantIdentifier) {
13431348
}
13441349

13451350
@Override
1346-
public SessionBuilder identifierRollback(boolean identifierRollback) {
1351+
public SessionBuilderImpl readOnly(boolean readOnly) {
1352+
this.readOnly = readOnly;
1353+
return this;
1354+
}
1355+
1356+
@Override
1357+
public SessionBuilderImpl identifierRollback(boolean identifierRollback) {
13471358
this.identifierRollback = identifierRollback;
13481359
return this;
13491360
}
@@ -1384,6 +1395,7 @@ public static class StatelessSessionBuilderImpl implements StatelessSessionBuild
13841395
private StatementInspector statementInspector;
13851396
private Connection connection;
13861397
private Object tenantIdentifier;
1398+
private boolean readOnly;
13871399

13881400
public StatelessSessionBuilderImpl(SessionFactoryImpl sessionFactory) {
13891401
this.sessionFactory = sessionFactory;
@@ -1418,6 +1430,12 @@ public StatelessSessionBuilder tenantIdentifier(Object tenantIdentifier) {
14181430
return this;
14191431
}
14201432

1433+
@Override
1434+
public StatelessSessionBuilder readOnly(boolean readOnly) {
1435+
this.readOnly = readOnly;
1436+
return this;
1437+
}
1438+
14211439
@Override @Deprecated
14221440
public StatelessSessionBuilder statementInspector(StatementInspector statementInspector) {
14231441
this.statementInspector = statementInspector;
@@ -1493,6 +1511,11 @@ public String getTenantIdentifier() {
14931511
: sessionFactory.getTenantIdentifierJavaType().toString( tenantIdentifier );
14941512
}
14951513

1514+
@Override
1515+
public boolean isReadOnly() {
1516+
return readOnly;
1517+
}
1518+
14961519
@Override
14971520
public Object getTenantIdentifierValue() {
14981521
return tenantIdentifier;

0 commit comments

Comments
 (0)