Index: /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/builder/xml/SqlStatementParser.java =================================================================== --- /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/builder/xml/SqlStatementParser.java (revision 663510) +++ /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/builder/xml/SqlStatementParser.java (working copy) @@ -5,7 +5,6 @@ import com.ibatis.sqlmap.engine.config.*; import com.ibatis.sqlmap.engine.mapping.statement.*; import com.ibatis.sqlmap.client.*; -import org.w3c.dom.CharacterData; import org.w3c.dom.*; import java.util.Properties; @@ -108,7 +107,18 @@ String keyPropName = attributes.getProperty("keyProperty"); String resultClassName = attributes.getProperty("resultClass"); String type = attributes.getProperty("type"); - config.setSelectKeyStatement(new XMLSqlSource(state, child), resultClassName, keyPropName, foundSQLFirst, type); + String contents = null; + try + { + // The contents is passed so that it can be determined whether it + // contains sql or a key column for jdbc3 generated keys + // see SelectKeyStatement class + contents = ((CharacterData) child.getFirstChild( )).getData(); + } + catch( DOMException e ) + { + } + config.setSelectKeyStatement(new XMLSqlSource(state, child), resultClassName, keyPropName, foundSQLFirst, type, contents); break; } } Index: /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/config/MappedStatementConfig.java =================================================================== --- /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/config/MappedStatementConfig.java (revision 663510) +++ /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/config/MappedStatementConfig.java (working copy) @@ -114,7 +114,7 @@ delegate.addMappedStatement(mappedStatement); } - public void setSelectKeyStatement(SqlSource processor, String resultClassName, String keyPropName, boolean runAfterSQL, String type) { + public void setSelectKeyStatement(SqlSource processor, String resultClassName, String keyPropName, boolean runAfterSQL, String type, String contents) { if (rootStatement instanceof InsertStatement) { InsertStatement insertStatement = ((InsertStatement) rootStatement); Class parameterClass = insertStatement.getParameterClass(); @@ -129,6 +129,19 @@ selectKeyStatement.setResource(errorContext.getResource()); selectKeyStatement.setKeyProperty(keyPropName); selectKeyStatement.setRunAfterSQL(runAfterSQL); + // contents holds the textual contents of the selectKey config element + if( contents != null ) + { + // check if keyColumn is defined for getting jdbc3 keys + // see SelectKeyStatement class + contents = contents.trim( ); + if( contents.startsWith( "KEY_COLUMN:" ) ) + { + contents = contents.replace( "KEY_COLUMN:", "" ).trim( ); + selectKeyStatement.setKeyColumn( contents ); + } + } + // process the type (pre or post) attribute if (type != null) { selectKeyStatement.setRunAfterSQL("post".equals(type)); Index: /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/execution/GeneratedKeyThreadLocal.java =================================================================== --- /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/execution/GeneratedKeyThreadLocal.java (revision 0) +++ /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/execution/GeneratedKeyThreadLocal.java (revision 0) @@ -0,0 +1,31 @@ +package com.ibatis.sqlmap.engine.execution; + +import com.ibatis.sqlmap.engine.mapping.statement.SelectKeyStatement; + +/** + * This class stores a jdbc3 generated key per thread using a {@link ThreadLocal}. + * In this way the key can be passed to lower layers of interfaces without making + * any changes to current method signatures. + * + * @see SelectKeyStatement + */ +public class GeneratedKeyThreadLocal +{ + private static final ThreadLocal tl = new ThreadLocal( ); + + public static final Object getKey( ) + { + return tl.get( ); + } + + public static final void setKey( Object key ) + { + tl.set( key ); + } + + public static final void clear( ) + { + tl.remove( ); + } + +} Index: /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/execution/SqlExecutor.java =================================================================== --- /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/execution/SqlExecutor.java (revision 663510) +++ /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/execution/SqlExecutor.java (working copy) @@ -19,8 +19,10 @@ import com.ibatis.sqlmap.engine.mapping.parameter.ParameterMap; import com.ibatis.sqlmap.engine.mapping.result.ResultMap; import com.ibatis.sqlmap.engine.mapping.result.ResultObjectFactoryUtil; +import com.ibatis.sqlmap.engine.mapping.statement.InsertStatement; import com.ibatis.sqlmap.engine.mapping.statement.MappedStatement; import com.ibatis.sqlmap.engine.mapping.statement.RowHandlerCallback; +import com.ibatis.sqlmap.engine.mapping.statement.StatementType; import com.ibatis.sqlmap.engine.scope.ErrorContext; import com.ibatis.sqlmap.engine.scope.StatementScope; import com.ibatis.sqlmap.engine.scope.SessionScope; @@ -72,7 +74,27 @@ int rows = 0; try { errorContext.setMoreInfo("Check the SQL Statement (preparation failed)."); - ps = prepareStatement(statementScope.getSession(), conn, sql); + /* + * Check whether JDBC 3 feature Statement.getGeneratedKeys is to be used. + * This is only relevant for insert statements and will only be done if + * the selectKeyStatement has a keyColumn defined. + * see SelectKeyStatement class + */ + boolean getJdbc3GeneratedKey = false; + if( statementScope.getStatement( ) instanceof InsertStatement ) + { + InsertStatement is = (InsertStatement) statementScope.getStatement( ); + if( is.getSelectKeyStatement( ) != null && is.getSelectKeyStatement( ).getKeyColumn( ) != null ) + { + getJdbc3GeneratedKey = true; + ps = prepareStatement(statementScope.getSession(), conn, sql, is.getSelectKeyStatement( ).getKeyColumn( ) ); + } + } + + // Check if all the jdbc3 key checks produced a value for ps + if( ps == null ) + ps = prepareStatement(statementScope.getSession(), conn, sql); + setStatementTimeout(statementScope.getStatement(), ps); errorContext.setMoreInfo("Check the parameters (set parameters failed)."); statementScope.getParameterMap().setParameters(statementScope, ps, parameters); @@ -78,6 +100,26 @@ statementScope.getParameterMap().setParameters(statementScope, ps, parameters); errorContext.setMoreInfo("Check the statement (update failed)."); ps.execute(); + + // Check if we need to get the generated keys, any failure will lead + // to it being ignored + if( getJdbc3GeneratedKey ) + { + try + { + ResultSet rs = ps.getGeneratedKeys( ); + if( rs.next( ) ) + { + Object obj = rs.getObject( 1 ); + // This threadlocal carries the generatedkey value across the + // layers of interfaces to be as little intrusive as possible + GeneratedKeyThreadLocal.setKey( obj ); + } + } + catch( Exception e ) + { } + } + rows = ps.getUpdateCount(); } finally { closeStatement(statementScope.getSession(), ps); @@ -489,7 +531,22 @@ } } - private static PreparedStatement prepareStatement(SessionScope sessionScope, Connection conn, String sql) throws SQLException { + /** + * This method supports statements which generate keys. The keyColumn is + * passed to the prepareStatement(String sql, String columnNames[]) + * method as a single value String[]. + * + * If the keyColumn is null, it reverts to a normal + * prepareStatement(String sql) method. + * + * @param sessionScope + * @param conn + * @param sql + * @param keyColumn + * @return + * @throws SQLException + */ + private static PreparedStatement prepareStatement(SessionScope sessionScope, Connection conn, String sql, String keyColumn) throws SQLException { SqlMapExecutorDelegate delegate = ((SqlMapClientImpl) sessionScope.getSqlMapExecutor()).getDelegate(); if (sessionScope.hasPreparedStatementFor(sql)) { return sessionScope.getPreparedStatement((sql)); @@ -494,7 +551,11 @@ if (sessionScope.hasPreparedStatementFor(sql)) { return sessionScope.getPreparedStatement((sql)); } else { - PreparedStatement ps = conn.prepareStatement(sql); + PreparedStatement ps; + if( keyColumn == null ) + ps = conn.prepareStatement(sql); + else + ps = conn.prepareStatement(sql, new String[] { keyColumn } ); sessionScope.putPreparedStatement(delegate, sql, ps); return ps; } @@ -500,6 +561,10 @@ } } + private static PreparedStatement prepareStatement(SessionScope sessionScope, Connection conn, String sql) throws SQLException { + return prepareStatement( sessionScope, conn, sql, (String) null ); + } + private CallableStatement prepareCall(SessionScope sessionScope, Connection conn, String sql) throws SQLException { SqlMapExecutorDelegate delegate = ((SqlMapClientImpl) sessionScope.getSqlMapExecutor()).getDelegate(); if (sessionScope.hasPreparedStatementFor(sql)) { Index: /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/impl/SqlMapExecutorDelegate.java =================================================================== --- /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/impl/SqlMapExecutorDelegate.java (revision 663510) +++ /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/impl/SqlMapExecutorDelegate.java (working copy) @@ -25,6 +25,7 @@ import com.ibatis.sqlmap.engine.cache.CacheModel; import com.ibatis.sqlmap.engine.exchange.DataExchangeFactory; import com.ibatis.sqlmap.engine.execution.BatchException; +import com.ibatis.sqlmap.engine.execution.GeneratedKeyThreadLocal; import com.ibatis.sqlmap.engine.execution.SqlExecutor; import com.ibatis.sqlmap.engine.mapping.parameter.ParameterMap; import com.ibatis.sqlmap.engine.mapping.result.ResultMap; @@ -381,7 +382,9 @@ Object oldKeyValue = null; String keyProperty = null; boolean resetKeyValueOnFailure = false; - if (selectKeyStatement != null && !selectKeyStatement.isRunAfterSQL()) { + // check if keyColumn is null, if it isn't then jdbc 3 generated keys will + // be used and isRunAfterSQL is irrelevant + if (selectKeyStatement != null && !selectKeyStatement.isRunAfterSQL() && selectKeyStatement.getKeyColumn( ) == null) { keyProperty = selectKeyStatement.getKeyProperty(); oldKeyValue = PROBE.getObject(param, keyProperty); generatedKey = executeSelectKey(sessionScope, trans, ms, param); @@ -401,8 +404,20 @@ endStatementScope(statementScope); } - if (selectKeyStatement != null && selectKeyStatement.isRunAfterSQL()) { - generatedKey = executeSelectKey(sessionScope, trans, ms, param); + // need the extra keyColumn null check to effectively ignore isRunAfterSQL + // when getting jdbc3 generated keys + if (selectKeyStatement != null && (selectKeyStatement.isRunAfterSQL() || selectKeyStatement.getKeyColumn( ) != null)) { + if( selectKeyStatement.getKeyColumn( ) != null ) + { + // So check the thread local whether we retrieved a jdbc3 generated key + // if we didn't then revert to the selectKey statement. + Object key = getJdbc3Key( selectKeyStatement, param ); + if( key != null ) + generatedKey = key; + } + // if it is null then we didn't get a jdbc3 key, so use the selectKey + if( generatedKey == null ) + generatedKey = executeSelectKey(sessionScope, trans, ms, param); } autoCommitTransaction(sessionScope, autoStart); @@ -413,6 +428,26 @@ return generatedKey; } + /** + * Retrieves the jdbc3 generated key from a thread local and sets it on the + * PROBE. + * + * @param selectKeyStatement + * @param param + * @return null if no key was set on the thread local. + */ + private Object getJdbc3Key( SelectKeyStatement selectKeyStatement, Object param ) + { + Object generatedKey = GeneratedKeyThreadLocal.getKey( ); + String keyProp = selectKeyStatement.getKeyProperty(); + if( keyProp != null && generatedKey != null ) + { + PROBE.setObject(param, keyProp, generatedKey); + } + GeneratedKeyThreadLocal.clear( ); + return generatedKey; + } + private Object executeSelectKey(SessionScope sessionScope, Transaction trans, MappedStatement ms, Object param) throws SQLException { Object generatedKey = null; StatementScope statementScope; Index: /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/mapping/statement/SelectKeyStatement.java =================================================================== --- /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/mapping/statement/SelectKeyStatement.java (revision 663510) +++ /home/zachv/devel/workspace/ibatis/src/com/ibatis/sqlmap/engine/mapping/statement/SelectKeyStatement.java (working copy) @@ -16,6 +16,7 @@ package com.ibatis.sqlmap.engine.mapping.statement; import com.ibatis.sqlmap.client.event.RowHandler; +import com.ibatis.sqlmap.engine.execution.GeneratedKeyThreadLocal; import com.ibatis.sqlmap.engine.scope.StatementScope; import com.ibatis.sqlmap.engine.transaction.Transaction; @@ -22,6 +23,28 @@ import java.sql.SQLException; import java.util.List; +/** + * Regarding jdbc 3 generated keys: + *