spring+ibatis插入操作简析

来源:互联网 发布:java 反射获取运行时 编辑:程序博客网 时间:2024/06/09 15:54

一、常用使用方式

在使用ibatis时,常常看到很多配置sql的xml文件,里面的描述如下所示:
 <insert id="insert" parameterClass="MarginCount" > INSERT  INTO  margincount   (  ID ,TRUSTCODE ,STATISTICDATE ,TRANSFERAMOUNT ,REPAYAMOUNT ,CHANNELFEERATE ,ISPUSH ,STATUS ,LASTUPDATE ,CREATETIME)values (  #id# ,#trustCode# ,#statisticDate# ,#transferAmount# ,#repayAmount# ,#channelFeeRate# ,#isPush# ,#status# ,now() ,now())</insert>
这条语句的功能是向数据库表margincount 中插入一条数据。在大多数场景下该语句可以很好的工作,但当后面业务流程需要新记录主键时,基于Ibatis机制,该方法并不能返回主键值。因此我们需要稍微修改下sql语句,如下所示:
  <insert id="insert" parameterClass="MarginCount" > INSERT  INTO  margincount   (  ID ,TRUSTCODE ,STATISTICDATE ,TRANSFERAMOUNT ,REPAYAMOUNT ,CHANNELFEERATE ,ISPUSH ,STATUS ,LASTUPDATE ,CREATETIME)values (  #id# ,#trustCode# ,#statisticDate# ,#transferAmount# ,#repayAmount# ,#channelFeeRate# ,#isPush# ,#status# ,now() ,now())<!--修改处-->                         <selectKey resultClass="long" keyProperty="id">                SELECT LAST_INSERT_ID() AS ID           </selectKey>   </span></insert>
在加入selectkey语句后,就能在插入记录后返回新纪录主键。
这样使用之后,已经能够解决问题,但是,在会让人思考在并发情况下这样使用会不会有问题。假设两个用户A和B,同时分别插入一条数据,这样返回的主键有可能并非是各自想要主键值。
实际情况中,这种问题并不会出现,因为SELECT LAST_INSERT_ID() 在mysql(本人环境使用的是mysql)中返回的是同一客户端对数据库insert操作后的主键值,所以两个不同用户不会互相影响selectkey语句的查询。
现在业务项目中,ibatis往往与spring集成使用,这些代码在经过多层封装后,开发人员很难理解这里面的实现, ibatis是如何控制一个用户的?上面的sql实际上是两条语句,是一次性提交还是分开提交的?这就是我们接下来需要考虑的。

二、ibatis源码分析

ibatis执行插入语句主要涉及一下几个类:
1) org.springframework.orm.ibatis.SqlMapClientTemplate:spring中用来集成ibatis的SqlMapClient对象,该对象是通过数据库配置文件生成,自动装配到spring中。
2)com.ibatis.sqlmap.engine.impl. SqlMapSessionImpl:是由SqlMapClient创建,每次请求都会创建一个session,即用session来区分是否是一次新的数据库操作。该对象包含SqlMapExecutorDelegate、SessionScope对象。
3) com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate:该对象比较重要,里面封装jdbc的接口,用来实际发送数据库请求。该对象是服务器启动时自动创建,只有一个,所有数据库操作均使用该对象完成。
4) com.ibatis.sqlmap.engine.scope.SessionScope:每个SqlMapSessionImpl中都会有一个SessionScope对象,即每次调用sql ID时都会创建一个SqlMapSessionImpl对象,这个对象会再创建一个SessionScope对象。该对象中包含了事务对象,并记录该sql ID 执行时的大部分信息。
5) com.ibatis.sqlmap.engine.transaction.user.UserProvidedTransaction:事务操作对象,集成在SessionScope中,是在创建Connection时一并创建的。
了解以上五个类的基本功能,下面看源码。SqlMapExecutorDelegate对象的insert方法:
  /**   * Call an insert statement by ID   *   * @param sessionScope - the session   * @param id      - the statement ID   * @param param   - the parameter object   * @return - the generated key (or null)   * @throws SQLException - if the insert fails   */  public Object insert(SessionScope sessionScope, String id, Object param) throws SQLException {    Object generatedKey = null;    MappedStatement ms = getMappedStatement(id);    Transaction trans = getTransaction(sessionScope);    boolean autoStart = trans == null;    try {      trans = autoStartTransaction(sessionScope, autoStart, trans);      SelectKeyStatement selectKeyStatement = null;      if (ms instanceof InsertStatement) {        selectKeyStatement = ((InsertStatement) ms).getSelectKeyStatement();      }      // Here we get the old value for the key property. We'll want it later if for some reason the      // insert fails.      Object oldKeyValue = null;      String keyProperty = null;      boolean resetKeyValueOnFailure = false;      if (selectKeyStatement != null && !selectKeyStatement.isRunAfterSQL()) {        keyProperty = selectKeyStatement.getKeyProperty();        oldKeyValue = PROBE.getObject(param, keyProperty);        generatedKey = executeSelectKey(sessionScope, trans, ms, param);        resetKeyValueOnFailure = true;      }      StatementScope statementScope = beginStatementScope(sessionScope, ms);      try {       //----------------------------insert语句执行        ms.executeUpdate(statementScope, trans, param);</span>      }catch (SQLException e){        // uh-oh, the insert failed, so if we set the reset flag earlier, we'll put the old value        // back...        if(resetKeyValueOnFailure) PROBE.setObject(param, keyProperty, oldKeyValue);        // ...and still throw the exception.        throw e;      } finally {        endStatementScope(statementScope);      }      if (selectKeyStatement != null && selectKeyStatement.isRunAfterSQL()) {           <pre name="code" class="java">           //------------------------------select语句执行
generatedKey = executeSelectKey(sessionScope, trans, ms, param);</span> } autoCommitTransaction(sessionScope, autoStart); } finally { autoEndTransaction(sessionScope, autoStart); } return generatedKey; }
改函数主要有两个sql执行操作,从名字上就能区分一个执行insert操作,另一个执行查询操作,所以在xml配置的sql语句实际上是分成两步进行执行的,ibatis分别提交。
再来分别看下这条语句执行时是如何保证一个客户端提交的。
executeUpdate中:
 public int executeUpdate(StatementScope statementScope, Transaction trans, Object parameterObject)      throws SQLException {    ErrorContext errorContext = statementScope.getErrorContext();    errorContext.setActivity("preparing the mapped statement for execution");    errorContext.setObjectId(this.getId());    errorContext.setResource(this.getResource());    statementScope.getSession().setCommitRequired(true);    try {      parameterObject = validateParameter(parameterObject);      Sql sql = getSql();      errorContext.setMoreInfo("Check the parameter map.");      ParameterMap parameterMap = sql.getParameterMap(statementScope, parameterObject);      errorContext.setMoreInfo("Check the result map.");      ResultMap resultMap = sql.getResultMap(statementScope, parameterObject);      statementScope.setResultMap(resultMap);      statementScope.setParameterMap(parameterMap);      int rows = 0;      errorContext.setMoreInfo("Check the parameter map.");      Object[] parameters = parameterMap.getParameterObjectValues(statementScope, parameterObject);      errorContext.setMoreInfo("Check the SQL statement.");      String sqlString = sql.getSql(statementScope, parameterObject);      errorContext.setActivity("executing mapped statement");      errorContext.setMoreInfo("Check the statement or the result map.");      //----------------------获取connection对象,实际执行insert语句     rows = sqlExecuteUpdate(statementScope, trans.getConnection(), sqlString, parameters);</span>      errorContext.setMoreInfo("Check the output parameters.");      if (parameterObject != null) {        postProcessParameterObject(statementScope, parameterObject, parameters);      }      errorContext.reset();      sql.cleanup(statementScope);      notifyListeners();      return rows;    } catch (SQLException e) {      errorContext.setCause(e);      throw new NestedSQLException(errorContext.toString(), e.getSQLState(), e.getErrorCode(), e);    } catch (Exception e) {      errorContext.setCause(e);      throw new NestedSQLException(errorContext.toString(), e);    }  }
executeSelectKey的方法中有如下语句electKeyStatement.executeQueryForObject(statementScope, trans, param, null);
该方法的执行过程:
 public Object executeQueryForObject(StatementScope statementScope, Transaction trans, Object parameterObject, Object resultObject)      throws SQLException {    try {      Object object = null;      DefaultRowHandler rowHandler = new DefaultRowHandler();      //-------------------获取connection对象,实际执行查询语句      executeQueryWithCallback(statementScope, trans.getConnection(), parameterObject, resultObject, rowHandler, SqlExecutor.NO_SKIPPED_RESULTS, SqlExecutor.NO_MAXIMUM_RESULTS);</span>      List list = rowHandler.getList();      if (list.size() > 1) {        throw new SQLException("Error: executeQueryForObject returned too many results.");      } else if (list.size() > 0) {        object = list.get(0);      }      return object;    } catch (TransactionException e) {      throw new NestedSQLException("Error getting Connection from Transaction.  Cause: " + e, e);    }  }
对比两个绿色标记的执行函数,都使用trans.getConnection()获取的是同一个connection对象,即是SqlMapClientTemplate中拿到的,代码如下绿色标记所示:
public <T> T execute(SqlMapClientCallback<T> action) throws DataAccessException {Assert.notNull(action, "Callback object must not be null");Assert.notNull(this.sqlMapClient, "No SqlMapClient specified");// We always need to use a SqlMapSession, as we need to pass a Spring-managed// Connection (potentially transactional) in. This shouldn't be necessary if// we run against a TransactionAwareDataSourceProxy underneath, but unfortunately// we still need it to make iBATIS batch execution work properly: If iBATIS// doesn't recognize an existing transaction, it automatically executes the// batch for every single statement...SqlMapSession session = this.sqlMapClient.openSession();if (logger.isDebugEnabled()) {logger.debug("Opened SqlMapSession [" + session + "] for iBATIS operation");}Connection ibatisCon = null;try {Connection springCon = null;DataSource dataSource = getDataSource();boolean transactionAware = (dataSource instanceof TransactionAwareDataSourceProxy);// Obtain JDBC Connection to operate on...try {ibatisCon = session.getCurrentConnection();if (ibatisCon == null) {                               //------------------没有session都有一个connection对象                               springCon = (transactionAware ?dataSource.getConnection() : DataSourceUtils.doGetConnection(dataSource));session.setUserConnection(springCon);</span>if (logger.isDebugEnabled()) {logger.debug("Obtained JDBC Connection [" + springCon + "] for iBATIS operation");}}else {if (logger.isDebugEnabled()) {logger.debug("Reusing JDBC Connection [" + ibatisCon + "] for iBATIS operation");}}}catch (SQLException ex) {throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);}// Execute given callback...try {return action.doInSqlMapClient(session);}catch (SQLException ex) {throw getExceptionTranslator().translate("SqlMapClient operation", null, ex);}finally {try {if (springCon != null) {if (transactionAware) {springCon.close();}else {DataSourceUtils.doReleaseConnection(springCon, dataSource);}}}catch (Throwable ex) {logger.debug("Could not close JDBC Connection", ex);}}// Processing finished - potentially session still to be closed.}finally {// Only close SqlMapSession if we know we've actually opened it// at the present level.if (ibatisCon == null) {session.close();}}}
因此,虽然是两次提交,但用的是一个connection对象,所以mysql会识别出来,并返回第一节中的正确结果。
结论:
1) 在ibatis 配置文件中的sql语句会分别执行,但使用的是一个connction对象
2)每次选择一个sql id执行时,都会创建一个session






0 0
原创粉丝点击