一、问题
对于任何和数据库交互的框架,都会涉及会话、链接、事务,了解它们各自的概念及相互关系对于理解框架以及参数配置优化都有很大好处。MyBatis是工作中使用较多的数据库层解决方案,我们有必要对会话、链接、事务进行深入了解一下。
二、项目配置
我们使用的样例项目的配置如下:
<?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>
测试代码如下:
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的代码如下:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
流程图如下:
1、在第一步中,通过new SqlSessionFactoryBuilder().build(inputStream)来构造SqlSessionFactory,参数是配置文件的输入流。
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);
}
2、XMLConfigBuilder的parse方法会解析配置文件,解析的结果就是得出一个Configuration对象。其中一步就是根据配置文件中的datasource节点解析出数据源:
<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、第3步中,SqlSessionFactory的openSession会获取SqlSession。具体如下:
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来管理数据库链接,只需要做一下事情:
1、实现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;
}
}
2、配置alias:
在mybatis-config.xml中添加如下代码:
<typeAliases>
<typeAlias type="com.iwill.mybatis.druid.DruidDataSourceFactory" alias="DRUID" />
</typeAliases>
3、配置数据源:
<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中可以提交多个事务。因此,链接---会话---事务的关系如下: