对于任何和数据库交互的框架,都会涉及会话、连接、事务,了解它们各自的概念及相互关系对于理解框架以及参数配置优化都有很大好处。MyBatis是工做中使用较多的数据库层解决方案,咱们有必要对会话、连接、事务进行深刻了解一下。java
咱们使用的样例项目的配置以下: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"> <!– <!–这里会替换为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>--> </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(); } }
会话是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就初始化完成了。
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"> <!-- <!–这里会替换为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>
这样,在获取数据库连接的时候,就是从DruidDataSource里面获取了:
在执行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上的。
连接能够经过数据库连接池被复用。在MyBatis中,不一样时刻的SqlSession能够复用同一个Connection,同一个SqlSession中能够提交多个事务。所以,连接---会话---事务的关系以下: