Обсуждение: revisiting transaction isolation
Currently, this type of code will fail: > conn.setAutoCommit(false); > if (conn.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE) > conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); The problem is that getTransactionIsolation() issues a query and thus starts a new transaction, and then setTransactionIsolation() complains you can't change isolation level mid-transaction. I'm not sure this is reasonable behaviour. One option is to make getTransactionIsolation (and what other methods too?) not cause a BEGIN to occur if there is no transaction in progress and autocommit is off. ... On a related topic I just took a look at the JDBC3 spec and it says: > The result of invoking the method setTransactionIsolation in the middle > of a transaction is implementation-defined. > > The return value of the method getTransactionIsolation should reflect > the change in isolation level when it actually occurs. It is recommended > that drivers implement the setTransactionIsolation method to change the > isolation level starting with the next transaction. Committing the > current transaction to make the effect immediate is also a valid > implementation. Should we follow the recommendation and have setTransactionIsolation() defer until the next transaction when called mid-transaction? i.e. store "current" vs "requested" isolation level; changing isolation level mid-transaction only changes the requested level, and after commit/rollback we issue an appropriate SET if current != expected. -O
On Sat, 17 Jul 2004, Oliver Jowett wrote: > Currently, this type of code will fail: > > > conn.setAutoCommit(false); > > if (conn.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE) > > conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); > > The problem is that getTransactionIsolation() issues a query and thus > starts a new transaction, and then setTransactionIsolation() complains > you can't change isolation level mid-transaction. > > I'm not sure this is reasonable behaviour. One option is to make > getTransactionIsolation (and what other methods too?) not cause a BEGIN > to occur if there is no transaction in progress and autocommit is off. I see no reason for getTransactionIsolation or any driver call to start a transaction, these are only SELECTs and won't be rolled back anyway. > On a related topic I just took a look at the JDBC3 spec and it says: > > > The return value of the method getTransactionIsolation should reflect > > the change in isolation level when it actually occurs. It is recommended > > that drivers implement the setTransactionIsolation method to change the > > isolation level starting with the next transaction. Committing the > > current transaction to make the effect immediate is also a valid > > implementation. This seems confusing and error prone. I would expect a command I issue to take effect immediately or throw an Exception, not do nothing now, but alter later behavior. Kris Jurka
Kris Jurka wrote: > > On Sat, 17 Jul 2004, Oliver Jowett wrote: > > >>Currently, this type of code will fail: >> >> >>> conn.setAutoCommit(false); >>> if (conn.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE) >>> conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); >> >>The problem is that getTransactionIsolation() issues a query and thus >>starts a new transaction, and then setTransactionIsolation() complains >>you can't change isolation level mid-transaction. >> >>I'm not sure this is reasonable behaviour. One option is to make >>getTransactionIsolation (and what other methods too?) not cause a BEGIN >>to occur if there is no transaction in progress and autocommit is off. > > > I see no reason for getTransactionIsolation or any driver call to start a > transaction, these are only SELECTs and won't be rolled back anyway. It's ok for getTransactionIsolation(), but what about, say, metadata queries -- we do want transaction isolation to apply there even if it's just a SELECT, right? A quick skim of AbstractJdbc2Connection turns up these methods as candidates for ignoring autocommit: - getTransactionIsolation() - getPGType (both versions) on a cache miss It seems reasonable to suppress BEGINs for both of those cases. I can put together a patch to do that. I'll leave the metadata queries alone for the moment. >>>The return value of the method getTransactionIsolation should reflect >>>the change in isolation level when it actually occurs. [...] > > This seems confusing and error prone. Ok -- I'll leave that alone too. -O
The code bellow throw an exception and I can see any transaction in progress.
Dario.
String url = getJdbcUrl();
try {
isCon = false;
con = DriverManager.getConnection( url, usr, pwd );
if ( con == null ) return false;
isCon = true;
con.setAutoCommit( autoCommit );
int tiso = con.getTransactionIsolation();
try {
dmd = con.getMetaData();
int defTxIsolation = -1;
if ( dmd != null && dmd.supportsTransactionIsolationLevel( tranIsolation ) ) {
defTxIsolation = dmd.getDefaultTransactionIsolation();
}
if ( tranIsolation >= 0 && tranIsolation != defTxIsolation ) {
tranIsolation = defTxIsolation;
}
}
if ( tranIsolation >= 0 ) {
con.setTransactionIsolation( tranIsolation );
tiso = con.getTransactionIsolation();
}
} catch ( SQLException se ) {
se.printStackTrace();
lastErrMsg = "\n ERROR: TransactionIsolation No Soportado = " + Tools.InfoException( se );
}
Oliver Jowett wrote:
Kris Jurka wrote:
On Sat, 17 Jul 2004, Oliver Jowett wrote:Currently, this type of code will fail:conn.setAutoCommit(false);
if (conn.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE)
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
The problem is that getTransactionIsolation() issues a query and thus starts a new transaction, and then setTransactionIsolation() complains you can't change isolation level mid-transaction.
I'm not sure this is reasonable behaviour. One option is to make getTransactionIsolation (and what other methods too?) not cause a BEGIN to occur if there is no transaction in progress and autocommit is off.
I see no reason for getTransactionIsolation or any driver call to start a transaction, these are only SELECTs and won't be rolled back anyway.
It's ok for getTransactionIsolation(), but what about, say, metadata queries -- we do want transaction isolation to apply there even if it's just a SELECT, right?
A quick skim of AbstractJdbc2Connection turns up these methods as candidates for ignoring autocommit:
- getTransactionIsolation()
- getPGType (both versions) on a cache miss
It seems reasonable to suppress BEGINs for both of those cases. I can put together a patch to do that. I'll leave the metadata queries alone for the moment.The return value of the method getTransactionIsolation should reflect
the change in isolation level when it actually occurs. [...]
This seems confusing and error prone.
Ok -- I'll leave that alone too.
-O
Oliver Jowett wrote:
> A quick skim of AbstractJdbc2Connection turns up these methods as
> candidates for ignoring autocommit:
>
> - getTransactionIsolation()
> - getPGType (both versions) on a cache miss
>
> It seems reasonable to suppress BEGINs for both of those cases. I can
> put together a patch to do that. I'll leave the metadata queries alone
> for the moment.
And here is the patch.
I reworked execSQLQuery / execSQLUpdate to always ignore autocommit,
since all places they're called from want this behaviour. Also tweaked
them to detect unexpected cases (no resultset from query, resultset from
update).
-O
Index: org/postgresql/core/BaseConnection.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/core/BaseConnection.java,v
retrieving revision 1.8
diff -u -c -r1.8 BaseConnection.java
*** org/postgresql/core/BaseConnection.java 29 Jun 2004 06:43:24 -0000 1.8
--- org/postgresql/core/BaseConnection.java 20 Jul 2004 08:35:05 -0000
***************
*** 31,36 ****
--- 31,37 ----
/**
* Execute a SQL query that returns a single resultset.
+ * Never causes a new transaction to be started regardless of the autocommit setting.
*
* @param s the query to execute
* @return the (non-null) returned resultset
***************
*** 40,45 ****
--- 41,47 ----
/**
* Execute a SQL query that does not return results.
+ * Never causes a new transaction to be started regardless of the autocommit setting.
*
* @param s the query to execute
* @throws SQLException if something goes wrong.
Index: org/postgresql/core/BaseStatement.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/core/BaseStatement.java,v
retrieving revision 1.13
diff -u -c -r1.13 BaseStatement.java
*** org/postgresql/core/BaseStatement.java 16 Jul 2004 09:07:53 -0000 1.13
--- org/postgresql/core/BaseStatement.java 20 Jul 2004 08:35:05 -0000
***************
*** 53,56 ****
--- 53,65 ----
* @throws SQLException if something goes wrong.
*/
public boolean executeWithFlags(String p_sql, int flags) throws SQLException;
+
+ /**
+ * Execute a prepared query, passing additional query flags.
+ *
+ * @param flags additional {@link QueryExecutor} flags for execution; these
+ * are bitwise-ORed into the default flags.
+ * @throws SQLException if something goes wrong.
+ */
+ public boolean executeWithFlags(int flags) throws SQLException;
}
Index: org/postgresql/jdbc2/AbstractJdbc2Connection.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/jdbc2/AbstractJdbc2Connection.java,v
retrieving revision 1.13
diff -u -c -r1.13 AbstractJdbc2Connection.java
*** org/postgresql/jdbc2/AbstractJdbc2Connection.java 16 Jul 2004 09:07:57 -0000 1.13
--- org/postgresql/jdbc2/AbstractJdbc2Connection.java 20 Jul 2004 08:35:06 -0000
***************
*** 175,184 ****
/**
* Simple query execution.
*/
! public ResultSet execSQLQuery(String s) throws SQLException
! {
! Statement stat = (Statement) createStatement();
! boolean hasResultSet = stat.execute(s);
// Transfer warnings to the connection, since the user never
// has a chance to see the statement itself.
--- 175,189 ----
/**
* Simple query execution.
*/
! public ResultSet execSQLQuery(String s) throws SQLException {
! BaseStatement stat = (BaseStatement) createStatement();
! boolean hasResultSet = stat.executeWithFlags(s, QueryExecutor.QUERY_SUPPRESS_BEGIN);
!
! while (!hasResultSet && stat.getUpdateCount() != -1)
! hasResultSet = stat.getMoreResults();
!
! if (!hasResultSet)
! throw new PSQLException("postgresql.stat.noresult", PSQLState.NO_DATA);
// Transfer warnings to the connection, since the user never
// has a chance to see the statement itself.
***************
*** 186,206 ****
if (warnings != null)
addWarning(warnings);
- while (!hasResultSet && stat.getUpdateCount() != -1)
- hasResultSet = stat.getMoreResults();
-
return stat.getResultSet();
}
! public void execSQLUpdate(String s) throws SQLException
! {
! execSQLUpdate(s, 0);
! }
!
! private void execSQLUpdate(String s, int flags) throws SQLException
! {
BaseStatement stmt = (BaseStatement) createStatement();
! stmt.executeWithFlags(s, QueryExecutor.QUERY_NO_METADATA | QueryExecutor.QUERY_NO_RESULTS | flags);
// Transfer warnings to the connection, since the user never
// has a chance to see the statement itself.
--- 191,203 ----
if (warnings != null)
addWarning(warnings);
return stat.getResultSet();
}
! public void execSQLUpdate(String s) throws SQLException {
BaseStatement stmt = (BaseStatement) createStatement();
! if (stmt.executeWithFlags(s, QueryExecutor.QUERY_NO_METADATA | QueryExecutor.QUERY_NO_RESULTS |
QueryExecutor.QUERY_SUPPRESS_BEGIN))
! throw new PSQLException("postgresql.stat.result");
// Transfer warnings to the connection, since the user never
// has a chance to see the statement itself.
***************
*** 526,532 ****
if (haveMinimumServerVersion("7.4") && readOnly != this.readOnly) {
String readOnlySql = "SET SESSION CHARACTERISTICS AS TRANSACTION " + (readOnly ? "READ ONLY" : "READ
WRITE");
! execSQLUpdate(readOnlySql, QueryExecutor.QUERY_SUPPRESS_BEGIN); // No BEGIN regardles of our autocommit
setting
}
this.readOnly = readOnly;
--- 523,529 ----
if (haveMinimumServerVersion("7.4") && readOnly != this.readOnly) {
String readOnlySql = "SET SESSION CHARACTERISTICS AS TRANSACTION " + (readOnly ? "READ ONLY" : "READ
WRITE");
! execSQLUpdate(readOnlySql); // nb: no BEGIN triggered.
}
this.readOnly = readOnly;
***************
*** 633,658 ****
public int getTransactionIsolation() throws SQLException
{
String level = null;
- Statement stmt = createStatement();
! if (haveMinimumServerVersion("7.3")) {
! ResultSet rs = stmt.executeQuery("SHOW TRANSACTION ISOLATION LEVEL");
if (rs.next())
level = rs.getString(1);
!
! // Transfer warnings to the connection, since the user never
! // has a chance to see the statement itself.
! SQLWarning warnings = stmt.getWarnings();
! if (warnings != null)
! addWarning(warnings);
} else {
! stmt.executeUpdate("SHOW TRANSACTION ISOLATION LEVEL");
! SQLWarning warning = stmt.getWarnings();
if (warning != null)
level = warning.getMessage();
- }
! stmt.close();
// XXX revisit: throw exception instead of silently eating the error in unkwon cases?
if (level == null)
--- 630,661 ----
public int getTransactionIsolation() throws SQLException
{
String level = null;
! if (haveMinimumServerVersion("7.3")) {
! // 7.3+ returns the level as a query result.
! ResultSet rs = execSQLQuery("SHOW TRANSACTION ISOLATION LEVEL"); // nb: no BEGIN triggered
if (rs.next())
level = rs.getString(1);
! rs.close();
} else {
! // 7.2 returns the level as an INFO message. Ew.
! // We juggle the warning chains a bit here.
!
! // Swap out current warnings.
! SQLWarning saveWarnings = getWarnings();
! clearWarnings();
!
! // Run the query any examine any resulting warnings.
! execSQLUpdate("SHOW TRANSACTION ISOLATION LEVEL"); // nb: no BEGIN triggered
! SQLWarning warning = getWarnings();
if (warning != null)
level = warning.getMessage();
! // Swap original warnings back.
! clearWarnings();
! if (saveWarnings != null)
! addWarning(saveWarnings);
! }
// XXX revisit: throw exception instead of silently eating the error in unkwon cases?
if (level == null)
***************
*** 694,700 ****
throw new PSQLException("postgresql.con.isolevel", PSQLState.TRANSACTION_STATE_INVALID, new
Integer(level));
String isolationLevelSQL = "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL " +
isolationLevelName;
! execSQLUpdate(isolationLevelSQL, QueryExecutor.QUERY_SUPPRESS_BEGIN); // No BEGIN regardles of our autocommit
setting
}
protected String getIsolationLevelName(int level)
--- 697,703 ----
throw new PSQLException("postgresql.con.isolevel", PSQLState.TRANSACTION_STATE_INVALID, new
Integer(level));
String isolationLevelSQL = "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL " +
isolationLevelName;
! execSQLUpdate(isolationLevelSQL); // nb: no BEGIN triggered
}
protected String getIsolationLevelName(int level)
***************
*** 887,893 ****
query.setString(1, typeName);
! BaseResultSet result = (BaseResultSet)query.executeQuery();
if (result.next()) {
oid = result.getInt(1);
oidTypeCache.put(new Integer(oid), typeName);
--- 890,899 ----
query.setString(1, typeName);
! if (! ((BaseStatement)query).executeWithFlags(QueryExecutor.QUERY_SUPPRESS_BEGIN) )
! throw new PSQLException("postgresql.stat.noresult", PSQLState.NO_DATA);
!
! ResultSet result = query.getResultSet();
if (result.next()) {
oid = result.getInt(1);
oidTypeCache.put(new Integer(oid), typeName);
***************
*** 926,932 ****
query.setInt(1, oid);
! ResultSet result = query.executeQuery();
if (result.next()) {
typeName = result.getString(1);
typeOidCache.put(typeName, new Integer(oid));
--- 932,941 ----
query.setInt(1, oid);
! if (! ((BaseStatement)query).executeWithFlags(QueryExecutor.QUERY_SUPPRESS_BEGIN) )
! throw new PSQLException("postgresql.stat.noresult", PSQLState.NO_DATA);
!
! ResultSet result = query.getResultSet();
if (result.next()) {
typeName = result.getString(1);
typeOidCache.put(typeName, new Integer(oid));
Index: org/postgresql/jdbc2/AbstractJdbc2ResultSet.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/jdbc2/AbstractJdbc2ResultSet.java,v
retrieving revision 1.43
diff -u -c -r1.43 AbstractJdbc2ResultSet.java
*** org/postgresql/jdbc2/AbstractJdbc2ResultSet.java 16 Jul 2004 09:07:59 -0000 1.43
--- org/postgresql/jdbc2/AbstractJdbc2ResultSet.java 20 Jul 2004 08:35:06 -0000
***************
*** 158,163 ****
--- 158,171 ----
// Fetch all results.
String cursorName = getString(columnIndex);
String fetchSql = "FETCH ALL IN \"" + cursorName + "\"";
+
+ // nb: no BEGIN triggered here. This is fine. If someone
+ // committed, and the cursor was not holdable (closing the
+ // cursor), we avoid starting a new xact and promptly causing
+ // it to fail. If the cursor *was* holdable, we don't want a
+ // new xact anyway since holdable cursor state isn't affected
+ // by xact boundaries. If our caller didn't commit at all, or
+ // autocommit was on, then we wouldn't issue a BEGIN anyway.
ResultSet rs = connection.execSQLQuery(fetchSql);
((AbstractJdbc2ResultSet)rs).setRefCursor(cursorName);
return rs;
Index: org/postgresql/jdbc2/AbstractJdbc2Statement.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/jdbc2/AbstractJdbc2Statement.java,v
retrieving revision 1.27
diff -u -c -r1.27 AbstractJdbc2Statement.java
*** org/postgresql/jdbc2/AbstractJdbc2Statement.java 16 Jul 2004 09:08:00 -0000 1.27
--- org/postgresql/jdbc2/AbstractJdbc2Statement.java 20 Jul 2004 08:35:06 -0000
***************
*** 282,288 ****
return executeWithFlags(0);
}
! private boolean executeWithFlags(int flags) throws SQLException
{
checkClosed();
if (isFunction && !returnTypeSet)
--- 282,288 ----
return executeWithFlags(0);
}
! public boolean executeWithFlags(int flags) throws SQLException
{
checkClosed();
if (isFunction && !returnTypeSet)
Index: org/postgresql/test/jdbc2/ConnectionTest.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/test/jdbc2/ConnectionTest.java,v
retrieving revision 1.14
diff -u -c -r1.14 ConnectionTest.java
*** org/postgresql/test/jdbc2/ConnectionTest.java 10 Apr 2004 13:53:11 -0000 1.14
--- org/postgresql/test/jdbc2/ConnectionTest.java 20 Jul 2004 08:35:06 -0000
***************
*** 242,249 ****
assertEquals(Connection.TRANSACTION_READ_COMMITTED,
con.getTransactionIsolation());
-
- Statement stmt = con.createStatement();
// Now run some tests with autocommit enabled.
con.setAutoCommit(true);
--- 242,247 ----
***************
*** 274,280 ****
--- 272,297 ----
con.setAutoCommit(false);
assertEquals(Connection.TRANSACTION_READ_COMMITTED,
con.getTransactionIsolation());
+ con.commit();
+
+ // Test that getTransactionIsolation() does not actually start a new txn.
+ con.getTransactionIsolation(); // Shouldn't start a new transaction.
+ con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); // Should be ok -- we're not in a
transaction.
+ con.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // Should still be ok.
+
+ // Test that we can't change isolation mid-transaction
+ Statement stmt = con.createStatement();
+ stmt.executeQuery("SELECT 1"); // Start transaction.
+ stmt.close();
+
+ try {
+ con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
+ fail("Expected an exception when changing transaction isolation mid-transaction");
+ } catch (SQLException e) {
+ // Ok.
+ }
+ con.rollback();
TestUtil.closeDB(con);
}
On Tue, 20 Jul 2004, Oliver Jowett wrote: > Oliver Jowett wrote: > > > A quick skim of AbstractJdbc2Connection turns up these methods as > > candidates for ignoring autocommit: > > > > - getTransactionIsolation() > > - getPGType (both versions) on a cache miss > > > > It seems reasonable to suppress BEGINs for both of those cases. I can > > put together a patch to do that. I'll leave the metadata queries alone > > for the moment. > Applied.