6、MyBatis中連接、會話、事務介紹

1、問題

對於任何和數據庫交互的框架,都會涉及會話、連接、事務,瞭解它們各自的概念及相互關係對於理解框架以及參數配置優化都有很大好處。MyBatis是工做中使用較多的數據庫層解決方案,咱們有必要對會話、連接、事務進行深刻了解一下。java

2、項目配置

咱們使用的樣例項目的配置以下:mysql

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <settings>
        <setting name="localCacheScope" value="SESSION"/>
        <setting name="cacheEnabled" value="true"/>
    </settings>

    <typeAliases>
        <typeAlias type="com.iwill.mybatis.druid.DruidDataSourceFactory" alias="DRUID" />
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <!--使用默認的JDBC事務管理-->
            <transactionManager type="JDBC"/>
            <!--使用鏈接池-->
            <dataSource type="POOLED">
                <!--這裏會替換爲local-mysql.properties中的對應字段的值-->
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/test?useUnicode=true"/>
                <property name="username" value="root"/>
                <property name="password" value="12345678"/>
            </dataSource>

<!--            <dataSource type="DRUID">
              &lt;!&ndash;  &lt;!&ndash;這裏會替換爲local-mysql.properties中的對應字段的值&ndash;&gt;&ndash;&gt;
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/test?useUnicode=true"/>
                <property name="username" value="root"/>
                <property name="password" value="12345678"/>
            </dataSource>-->
        </environment>
    </environments>

    <!--SQL映射文件,mybatis的核心-->
    <mappers>
        <mapper resource="mapper/ext/UserMapperExt.xml"/>
        <mapper resource="mapper/gen/UserMapper.xml"/>
        <mapper resource="mapper/gen/ProductMapper.xml"/>
    </mappers>
</configuration>

測試代碼以下:sql

package com.iwill.mybatis;

import com.iwill.mybatis.dao.mapper.ext.UserMapperExt;
import com.iwill.mybatis.dao.model.UserDTO;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;

public class MyBatisTest {

    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        UserMapperExt userMapperExt1 = sqlSession1.getMapper(UserMapperExt.class);

        userMapperExt1.insert(new UserDTO("houliu",23));
        userMapperExt1.findUserListByName("zhangsan");
        userMapperExt1.update("name" ,"wangwu",22);
        sqlSession1.commit();

        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        UserMapperExt userMapperExt2 = sqlSession2.getMapper(UserMapperExt.class);

        userMapperExt2.insert(new UserDTO("houliu",23));
        userMapperExt2.findUserListByName("zhangsan");
        userMapperExt2.update("name" ,"wangwu",22);
        sqlSession2.commit();

    }

}

3、會話

會話是MyBatis執行SQL的基礎,MyBatis中的會話是SqlSession,默認實現是DefaultSqlSession。能夠經過SqlSessionFactory的openSession來獲取的。數據庫

經過SqlSessionFactory獲取SqlSession的代碼以下:apache

String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();

流程圖以下:session

一、在第一步中,經過new SqlSessionFactoryBuilder().build(inputStream)來構造SqlSessionFactory,參數是配置文件的輸入流。mybatis

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

二、XMLConfigBuilder的parse方法會解析配置文件,解析的結果就是得出一個Configuration對象。其中一步就是根據配置文件中的datasource節點解析出數據源:app

<dataSource type="POOLED">
    <!--這裏會替換爲local-mysql.properties中的對應字段的值-->
    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://127.0.0.1:3306/test?useUnicode=true"/>
    <property name="username" value="root"/>
    <property name="password" value="12345678"/>
    <property name="poolMaximumActiveConnections" value="2"/>
    <property name="poolMaximumIdleConnections" value="2"/>
</dataSource>

根據這裏的type="POOLED"解析出數據源爲PooledDataSource。第1步獲得的SqlSessionFactory爲DefaultSqlSessionFactory。框架

三、第3步中,SqlSessionFactory的openSession會獲取SqlSession。具體以下:oop

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

這裏的autoCommit的默認值是false,即不會自動提交。至此,SqlSession就初始化完成了。

4、連接

MyBatis在第一次執行SQL操做時,在獲取Statement時,會去獲取數據庫連接。

咱們配置的數據源爲POOLED,這裏會使用PooledDataSource來獲取connection。

private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
    PooledConnection conn = null;
    long t = System.currentTimeMillis();
    int localBadConnectionCount = 0;

    while (conn == null) {
      synchronized (state) {
        if (!state.idleConnections.isEmpty()) {
          // Pool has available connection
          conn = state.idleConnections.remove(0);
          if (log.isDebugEnabled()) {
            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
          }
        } else {
          // Pool does not have available connection
          if (state.activeConnections.size() < poolMaximumActiveConnections) {
            // Can create new connection
            conn = new PooledConnection(dataSource.getConnection(), this);
            if (log.isDebugEnabled()) {
              log.debug("Created connection " + conn.getRealHashCode() + ".");
            }
          } else {
            // Cannot create new connection
            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
            if (longestCheckoutTime > poolMaximumCheckoutTime) {
              // Can claim overdue connection
              state.claimedOverdueConnectionCount++;
              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
              state.accumulatedCheckoutTime += longestCheckoutTime;
              state.activeConnections.remove(oldestActiveConnection);
              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                try {
                  oldestActiveConnection.getRealConnection().rollback();
                } catch (SQLException e) {
                  /*
                     Just log a message for debug and continue to execute the following
                     statement like nothing happend.
                     Wrap the bad connection with a new PooledConnection, this will help
                     to not intterupt current executing thread and give current thread a
                     chance to join the next competion for another valid/good database
                     connection. At the end of this loop, bad {@link @conn} will be set as null.
                   */
                  log.debug("Bad connection. Could not roll back");
                }  
              }
              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
              conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
              conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
              oldestActiveConnection.invalidate();
              if (log.isDebugEnabled()) {
                log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
              }
            } else {
              // Must wait
              try {
                if (!countedWait) {
                  state.hadToWaitCount++;
                  countedWait = true;
                }
                if (log.isDebugEnabled()) {
                  log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
                }
                long wt = System.currentTimeMillis();
                state.wait(poolTimeToWait);
                state.accumulatedWaitTime += System.currentTimeMillis() - wt;
              } catch (InterruptedException e) {
                break;
              }
            }
          }
        }
        if (conn != null) {
          // ping to server and check the connection is valid or not
          if (conn.isValid()) {
            if (!conn.getRealConnection().getAutoCommit()) {
              conn.getRealConnection().rollback();
            }
            conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
            conn.setCheckoutTimestamp(System.currentTimeMillis());
            conn.setLastUsedTimestamp(System.currentTimeMillis());
            state.activeConnections.add(conn);
            state.requestCount++;
            state.accumulatedRequestTime += System.currentTimeMillis() - t;
          } else {
            if (log.isDebugEnabled()) {
              log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
            }
            state.badConnectionCount++;
            localBadConnectionCount++;
            conn = null;
            if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
              if (log.isDebugEnabled()) {
                log.debug("PooledDataSource: Could not get a good connection to the database.");
              }
              throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
            }
          }
        }
      }

    }

    if (conn == null) {
      if (log.isDebugEnabled()) {
        log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
      }
      throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    }

    return conn;
  }

這裏進行了數據庫的連接進行了池化管理:若是idle的connection,就直接取出一個返回。數據庫連接的獲取底層代碼以下:

獲取連接後的第一件事,就是設置connection的autoCommit屬性。這裏能夠看出MyBatis經過自身的數據源PooledDataSource來進行數據庫連接的管理。

若是想要經過druid來管理數據庫連接,只須要作一下事情:

一、實現org.apache.ibatis.datasource.DataSourceFactory接口便可:

public class DruidDataSourceFactory implements DataSourceFactory {

    private Properties props;

    public DataSource getDataSource() {
        DruidDataSource dds = new DruidDataSource();
        dds.setDriverClassName(this.props.getProperty("driver"));
        dds.setUrl(this.props.getProperty("url"));
        dds.setUsername(this.props.getProperty("username"));
        dds.setPassword(this.props.getProperty("password"));
        // 其餘配置能夠根據MyBatis主配置文件進行配置
        try {
            dds.init();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return dds;
    }

    public void setProperties(Properties props) {
        this.props = props;
    }
}

二、配置alias:

在mybatis-config.xml中添加以下代碼:

<typeAliases>
        <typeAlias type="com.iwill.mybatis.druid.DruidDataSourceFactory" alias="DRUID" />
    </typeAliases>

三、配置數據源:

<dataSource type="DRUID">
              <!--  &lt;!&ndash;這裏會替換爲local-mysql.properties中的對應字段的值&ndash;&gt;-->
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/test?useUnicode=true"/>
                <property name="username" value="root"/>
                <property name="password" value="12345678"/>
            </dataSource>

這樣,在獲取數據庫連接的時候,就是從DruidDataSource裏面獲取了:

5、事務

在執行sqlSession.commit時,會去提交事務。

UserMapperExt userMapperExt = sqlSession.getMapper(UserMapperExt.class);

        userMapperExt.insert(new UserDTO("houliu",23));
        userMapperExt.findUserListByName("zhangsan");
        userMapperExt.update("name" ,"wangwu",22);
        sqlSession.commit();

執行commit後,會調用以下代碼:

一個sqlSession中能夠進行多個事務提交:

SqlSession sqlSession1 = sqlSessionFactory.openSession();
        UserMapperExt userMapperExt1 = sqlSession1.getMapper(UserMapperExt.class);

        userMapperExt1.insert(new UserDTO("houliu",23));
        userMapperExt1.findUserListByName("zhangsan");
        userMapperExt1.update("name" ,"wangwu",22);
        sqlSession1.commit();

        //SqlSession sqlSession2 = sqlSessionFactory.openSession();
        UserMapperExt userMapperExt2 = sqlSession1.getMapper(UserMapperExt.class);

        userMapperExt2.insert(new UserDTO("houliu",23));
        userMapperExt2.findUserListByName("zhangsan");
        userMapperExt2.update("name" ,"wangwu",22);
        sqlSession1.commit();

原生jdbc中一個connection能夠執行屢次commit:

Class.forName("com.mysql.cj.jdbc.Driver"); //classLoader,加載對應驅動
        Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?useUnicode=true", "root", "12345678");
        connection.setAutoCommit(false);
        PreparedStatement preparedStatement = connection.prepareStatement("update cnt_user set age = 201 where name = 'zhangsan'");
        preparedStatement.execute();

        connection.commit();
        preparedStatement = connection.prepareStatement("update cnt_user set age = 233 where name = 'zhangsan'");
        preparedStatement.execute();

        preparedStatement = connection.prepareStatement("insert into cnt_user (age , name) values(100 ,'liusi')");
        preparedStatement.execute();

        connection.commit();

能夠看出,事務是依附在SqlSession上的。

6、相互關係

連接能夠經過數據庫連接池被複用。在MyBatis中,不一樣時刻的SqlSession能夠複用同一個Connection,同一個SqlSession中能夠提交多個事務。所以,連接---會話---事務的關係以下:

相關文章
相關標籤/搜索