HibernateTransactionManager lazily acquires JDBC Connection

Aligned with HibernateJpaDialect, using ConnectionHandle as functional interface now. Also, LazyConnectionDataSourceProxy supports Connection holdability as applied by HibernateTransactionManager, for use with prepareConnection=true.

Issue: SPR-17216
This commit is contained in:
Juergen Hoeller 2018-08-31 12:40:04 +02:00
parent a689daadf9
commit 78cad0fdd3
8 changed files with 76 additions and 69 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -27,6 +27,7 @@ import java.sql.Connection;
* @see SimpleConnectionHandle * @see SimpleConnectionHandle
* @see ConnectionHolder * @see ConnectionHolder
*/ */
@FunctionalInterface
public interface ConnectionHandle { public interface ConnectionHandle {
/** /**
@ -36,8 +37,11 @@ public interface ConnectionHandle {
/** /**
* Release the JDBC Connection that this handle refers to. * Release the JDBC Connection that this handle refers to.
* <p>The default implementation is empty, assuming that the lifecycle
* of the connection is managed externally.
* @param con the JDBC Connection to release * @param con the JDBC Connection to release
*/ */
void releaseConnection(Connection con); default void releaseConnection(Connection con) {
}
} }

View File

@ -21,6 +21,7 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Proxy; import java.lang.reflect.Proxy;
import java.sql.Connection; import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import javax.sql.DataSource; import javax.sql.DataSource;
@ -256,14 +257,16 @@ public class LazyConnectionDataSourceProxy extends DelegatingDataSource {
@Nullable @Nullable
private String password; private String password;
private Boolean readOnly = Boolean.FALSE;
@Nullable @Nullable
private Boolean autoCommit; private Boolean autoCommit;
@Nullable @Nullable
private Integer transactionIsolation; private Integer transactionIsolation;
private boolean readOnly = false;
private int holdability = ResultSet.CLOSE_CURSORS_AT_COMMIT;
private boolean closed = false; private boolean closed = false;
@Nullable @Nullable
@ -319,11 +322,15 @@ public class LazyConnectionDataSourceProxy extends DelegatingDataSource {
if (method.getName().equals("toString")) { if (method.getName().equals("toString")) {
return "Lazy Connection proxy for target DataSource [" + getTargetDataSource() + "]"; return "Lazy Connection proxy for target DataSource [" + getTargetDataSource() + "]";
} }
else if (method.getName().equals("isReadOnly")) { else if (method.getName().equals("getAutoCommit")) {
return this.readOnly; if (this.autoCommit != null) {
return this.autoCommit;
}
// Else fetch actual Connection and check there,
// because we didn't have a default specified.
} }
else if (method.getName().equals("setReadOnly")) { else if (method.getName().equals("setAutoCommit")) {
this.readOnly = (Boolean) args[0]; this.autoCommit = (Boolean) args[0];
return null; return null;
} }
else if (method.getName().equals("getTransactionIsolation")) { else if (method.getName().equals("getTransactionIsolation")) {
@ -337,15 +344,18 @@ public class LazyConnectionDataSourceProxy extends DelegatingDataSource {
this.transactionIsolation = (Integer) args[0]; this.transactionIsolation = (Integer) args[0];
return null; return null;
} }
else if (method.getName().equals("getAutoCommit")) { else if (method.getName().equals("isReadOnly")) {
if (this.autoCommit != null) { return this.readOnly;
return this.autoCommit;
}
// Else fetch actual Connection and check there,
// because we didn't have a default specified.
} }
else if (method.getName().equals("setAutoCommit")) { else if (method.getName().equals("setReadOnly")) {
this.autoCommit = (Boolean) args[0]; this.readOnly = (Boolean) args[0];
return null;
}
else if (method.getName().equals("getHoldability")) {
return this.holdability;
}
else if (method.getName().equals("setHoldability")) {
this.holdability = (Integer) args[0];
return null; return null;
} }
else if (method.getName().equals("commit")) { else if (method.getName().equals("commit")) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2012 the original author or authors. * Copyright 2002-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -49,14 +49,6 @@ public class SimpleConnectionHandle implements ConnectionHandle {
return this.connection; return this.connection;
} }
/**
* This implementation is empty, as we're using a standard
* Connection handle that does not have to be released.
*/
@Override
public void releaseConnection(Connection con) {
}
@Override @Override
public String toString() { public String toString() {

View File

@ -197,9 +197,16 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana
* manager needs to work on the underlying target DataSource. If there's * manager needs to work on the underlying target DataSource. If there's
* nevertheless a TransactionAwareDataSourceProxy passed in, it will be * nevertheless a TransactionAwareDataSourceProxy passed in, it will be
* unwrapped to extract its target DataSource. * unwrapped to extract its target DataSource.
* <p><b>NOTE: For scenarios with many transactions that just read data from
* Hibernate's cache (and do not actually access the database), consider using
* a {@link org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy}
* for the actual target DataSource. Alternatively, consider switching
* {@link #setPrepareConnection "prepareConnection"} to {@code false}.</b>
* In both cases, this transaction manager will not eagerly acquire a
* JDBC Connection for each Hibernate Session anymore (as of Spring 5.1).
* @see #setAutodetectDataSource * @see #setAutodetectDataSource
* @see TransactionAwareDataSourceProxy * @see TransactionAwareDataSourceProxy
* @see DataSourceUtils * @see org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy
* @see org.springframework.jdbc.core.JdbcTemplate * @see org.springframework.jdbc.core.JdbcTemplate
*/ */
public void setDataSource(@Nullable DataSource dataSource) { public void setDataSource(@Nullable DataSource dataSource) {
@ -462,33 +469,37 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana
session = txObject.getSessionHolder().getSession(); session = txObject.getSessionHolder().getSession();
if (this.prepareConnection && isSameConnectionForEntireSession(session)) { boolean holdabilityNeeded = this.allowResultAccessAfterCompletion && !txObject.isNewSession();
// We're allowed to change the transaction settings of the JDBC Connection. boolean isolationLevelNeeded = (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT);
if (logger.isDebugEnabled()) { if (holdabilityNeeded || isolationLevelNeeded || definition.isReadOnly()) {
logger.debug("Preparing JDBC Connection of Hibernate Session [" + session + "]"); if (this.prepareConnection && isSameConnectionForEntireSession(session)) {
} // We're allowed to change the transaction settings of the JDBC Connection.
Connection con = ((SessionImplementor) session).connection(); if (logger.isDebugEnabled()) {
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); logger.debug("Preparing JDBC Connection of Hibernate Session [" + session + "]");
txObject.setPreviousIsolationLevel(previousIsolationLevel); }
if (this.allowResultAccessAfterCompletion && !txObject.isNewSession()) { Connection con = ((SessionImplementor) session).connection();
int currentHoldability = con.getHoldability(); Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
if (currentHoldability != ResultSet.HOLD_CURSORS_OVER_COMMIT) { txObject.setPreviousIsolationLevel(previousIsolationLevel);
txObject.setPreviousHoldability(currentHoldability); if (this.allowResultAccessAfterCompletion && !txObject.isNewSession()) {
con.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT); int currentHoldability = con.getHoldability();
if (currentHoldability != ResultSet.HOLD_CURSORS_OVER_COMMIT) {
txObject.setPreviousHoldability(currentHoldability);
con.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT);
}
} }
} }
} else {
else { // Not allowed to change the transaction settings of the JDBC Connection.
// Not allowed to change the transaction settings of the JDBC Connection. if (isolationLevelNeeded) {
if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { // We should set a specific isolation level but are not allowed to...
// We should set a specific isolation level but are not allowed to... throw new InvalidIsolationLevelException(
throw new InvalidIsolationLevelException( "HibernateTransactionManager is not allowed to support custom isolation levels: " +
"HibernateTransactionManager is not allowed to support custom isolation levels: " + "make sure that its 'prepareConnection' flag is on (the default) and that the " +
"make sure that its 'prepareConnection' flag is on (the default) and that the " + "Hibernate connection release mode is set to 'on_close' (the default for JDBC).");
"Hibernate connection release mode is set to 'on_close' (the default for JDBC)."); }
} if (logger.isDebugEnabled()) {
if (logger.isDebugEnabled()) { logger.debug("Not preparing JDBC Connection of Hibernate Session [" + session + "]");
logger.debug("Not preparing JDBC Connection of Hibernate Session [" + session + "]"); }
} }
} }
@ -529,13 +540,13 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana
// Register the Hibernate Session's JDBC Connection for the DataSource, if set. // Register the Hibernate Session's JDBC Connection for the DataSource, if set.
if (getDataSource() != null) { if (getDataSource() != null) {
Connection con = ((SessionImplementor) session).connection(); SessionImplementor sessionImpl = (SessionImplementor) session;
ConnectionHolder conHolder = new ConnectionHolder(con); ConnectionHolder conHolder = new ConnectionHolder(sessionImpl::connection);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
conHolder.setTimeoutInSeconds(timeout); conHolder.setTimeoutInSeconds(timeout);
} }
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Exposing Hibernate transaction as JDBC transaction [" + con + "]"); logger.debug("Exposing Hibernate transaction as JDBC [" + conHolder.getConnectionHandle() + "]");
} }
TransactionSynchronizationManager.bindResource(getDataSource(), conHolder); TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);
txObject.setConnectionHolder(conHolder); txObject.setConnectionHolder(conHolder);

View File

@ -416,7 +416,7 @@ public class JpaTransactionManager extends AbstractPlatformTransactionManager
conHolder.setTimeoutInSeconds(timeoutToUse); conHolder.setTimeoutInSeconds(timeoutToUse);
} }
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Exposing JPA transaction as JDBC transaction [" + conHandle + "]"); logger.debug("Exposing JPA transaction as JDBC [" + conHandle + "]");
} }
TransactionSynchronizationManager.bindResource(getDataSource(), conHolder); TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);
txObject.setConnectionHolder(conHolder); txObject.setConnectionHolder(conHolder);

View File

@ -131,10 +131,6 @@ public class EclipseLinkJpaDialect extends DefaultJpaDialect {
} }
return this.connection; return this.connection;
} }
@Override
public void releaseConnection(Connection con) {
}
} }
} }

View File

@ -75,7 +75,7 @@ import org.springframework.util.ReflectionUtils;
/** /**
* {@link org.springframework.orm.jpa.JpaDialect} implementation for * {@link org.springframework.orm.jpa.JpaDialect} implementation for
* Hibernate EntityManager. Developed against Hibernate 5.0/5.1/5.2. * Hibernate EntityManager. Developed against Hibernate 5.1/5.2/5.3.
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Costin Leau * @author Costin Leau
@ -119,10 +119,8 @@ public class HibernateJpaDialect extends DefaultJpaDialect {
* Hibernate Session, that is, whether to apply a transaction-specific * Hibernate Session, that is, whether to apply a transaction-specific
* isolation level and/or the transaction's read-only flag to the underlying * isolation level and/or the transaction's read-only flag to the underlying
* JDBC Connection. * JDBC Connection.
* <p>Default is "true" on Hibernate EntityManager 4.x (with its 'on-close' * <p>Default is "true". If you turn this flag off, JPA transaction management
* connection release mode. * will not support per-transaction isolation levels anymore. It will not call
* <p>If you turn this flag off, JPA transaction management will not support
* per-transaction isolation levels anymore. It will not call
* {@code Connection.setReadOnly(true)} for read-only transactions anymore either. * {@code Connection.setReadOnly(true)} for read-only transactions anymore either.
* If this flag is turned off, no cleanup of a JDBC Connection is required after * If this flag is turned off, no cleanup of a JDBC Connection is required after
* a transaction, since no Connection settings will get modified. * a transaction, since no Connection settings will get modified.
@ -415,10 +413,6 @@ public class HibernateJpaDialect extends DefaultJpaDialect {
return doGetConnection(this.session); return doGetConnection(this.session);
} }
@Override
public void releaseConnection(Connection con) {
}
public static Connection doGetConnection(Session session) { public static Connection doGetConnection(Session session) {
try { try {
Method methodToUse = connectionMethodToUse; Method methodToUse = connectionMethodToUse;

View File

@ -41,7 +41,7 @@ import org.springframework.lang.Nullable;
/** /**
* {@link org.springframework.orm.jpa.JpaVendorAdapter} implementation for Hibernate * {@link org.springframework.orm.jpa.JpaVendorAdapter} implementation for Hibernate
* EntityManager. Developed and tested against Hibernate 5.0, 5.1 and 5.2; * EntityManager. Developed and tested against Hibernate 5.0, 5.1, 5.2 and 5.3;
* backwards-compatible with Hibernate 4.3 at runtime on a best-effort basis. * backwards-compatible with Hibernate 4.3 at runtime on a best-effort basis.
* *
* <p>Exposes Hibernate's persistence provider and EntityManager extension interface, * <p>Exposes Hibernate's persistence provider and EntityManager extension interface,