Mybatis源码之主键、事务、连接池

主键

Mybatis 主键生成策略使用方式为在数据变更语句 insert,update 设置 useGeneratedKeys属性为true(仅对 insert 和 update 有用),这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系数据库管理系统的自动递增字段),默认值:false。

其实现方式为:Mybatis提供了主键生成器接口KeyGenerator,在sql语句执行完毕后会执行keyGenerator.processAfter(),方法执行stmt.getGeneratedKeys(),从statement中取出生成的主键;而insert语句本身是不返回记录的主键值,而是返回插入的记录条数。

事务

Mybatis 事务实现方式为提供了一个 Transaction 接口,其实现类有JdbcTransactionManagedTransaction

JdbcTransaction直接使用JDBC提交和回滚工具的事务。 他依赖于从数据源中获取的连接取管理事务的范围。如果设置了autocommit,则会自动提交和回滚,换而言之,没有设置autocommit则需要手动提交。

ManagedTransaction 不处理提交和回滚,而是让容器管理事务的完整生命周期。

使用事务只要在Mybatis配置文件中每个environment标签内添加子标签

<!--JDBC|MANAGED-->
<transactionManager type="JDBC"/>

即可

连接池

Mybatis 连接池有3种:PooledConnectionUnpooledDataSource 以及JNDI datasource,由对应的工厂类产生:PooledDataSourceFactoryUnpooledDataSourceFactoryJndiDataSourceFactory

连接池的使用在Mybatis配置文件中每个environment子标签dataSource增加属性

<!--UNPOOLED|POOLED|JNDI-->
<dataSource type="POOLED">

即可

下面针对源码分别进行分析

源码分析

主键

BlogDao blogDao = sqlSession.getMapper(BlogDao.class);
Blog blog = new Blog();
blog.setTitle("test");
blog.setContent("Hello World !");
Long count = blogDao.saveBlog(blog);
System.out.println(count);
System.out.println(blog.getId());

上面是一个简单的Mybatis保存数据的代码,saveBlog的statement如下

<insert id="saveBlog" useGeneratedKeys="true" keyProperty="id" parameterType="blog">
    insert into blog (title, content)
    values (#{title}, #{content})
</insert>

也很简单,跟进代码调试在MapperMethod.execute() 方法可以看到进入INSERT case,最后会以result返回,如下

result = rowCountResult(sqlSession.insert(command.getName(), param));

根据方法名可以猜测,insert返回的是执行的行,也就是插入的行。

sqlSession的insert方法会调用其update方法,update方法根据statement名称,取出之前解析mapper文件得到的MappedStatement,然后调用executor的update方法,executor默认是CachingExecutor,具体的执行会委派给其内部的委派对象SimpleExecutor执行

// SimpleExecutor
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
  Statement stmt = null;
  try {
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
    stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.update(stmt);
  } finally {
    closeStatement(stmt);
  }
}

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    // 直到这里才真正的获取连接
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
}

在获取statement后,继续执行handler.update(stmt),这个handlerRoutingStatementHandler的对象,其内部也是一个事件委派,默认为PREPARED

// RoutingStatementHandler构造函数
switch (ms.getStatementType()) {
  case STATEMENT:
    delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
    break;
  case PREPARED:
    delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
    break;
  case CALLABLE:
    delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
    break;
  default:
    throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}

进入delegate.update()

public int update(Statement statement) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;
  ps.execute();
    // 返回执行的行
  int rows = ps.getUpdateCount();
  Object parameterObject = boundSql.getParameterObject();
  KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    // Jdbc3KeyGenerator 会进行主键处理
  keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
  return rows;
}

根据不同的KeyGenerator策略进行不同的操作Jdbc3KeyGenerator.processAfter,会进行主键的处理。

连接池

在上面执行SimpleExecutor.prepareStatement()的时候,会获取连接,根据不同的连接池配置,获取连接的方式也是不一致的。

// BaseExecutor
protected Connection getConnection(Log statementLog) throws SQLException {
  Connection connection = transaction.getConnection();
  if (statementLog.isDebugEnabled()) {
    return ConnectionLogger.newInstance(connection, statementLog, queryStack);
  } else {
    return connection;
  }
}

调用transaction.getConnection,根据配置Mybatis的事务管理有两种JdbcTransactionManagedTransaction,分别是JDBC的方式和交给容器管理,这里我的配置是JDBC

// JdbcTransaction
public Connection getConnection() throws SQLException {
  if (connection == null) {
    openConnection();
  }
  return connection;
}
protected void openConnection() throws SQLException {
    if (log.isDebugEnabled()) {
        log.debug("Opening JDBC Connection");
    }
    connection = dataSource.getConnection();
    if (level != null) {
        connection.setTransactionIsolation(level.getLevel());
    }
    setDesiredAutoCommit(autoCommmit);
}

JdbcTransaction 的构造函数传参有数据连接池、事务隔离级别、是否自动提交

public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {
  dataSource = ds;
  level = desiredLevel;
  autoCommmit = desiredAutoCommit;
}

到这已经很清楚了,transaction 通过从连接池中获取连接,并设置相应的隔离级别和是否自动提交,返回给executor。

因为我配置连接池是POOLED,这里可以进入PooledDataSource.getConnection() 方法

public Connection getConnection() throws SQLException {
    // 注意返回的是getProxyConnection()方法获取的connection
  return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}

主要通过内部对象PoolState对象state来管理,popConnection方法内部synchronized (state)保证线程安全。

可以看到新建连接的对象为PooledConnection,而返回的是PooledConnection.getProxyConnection()获取的connection。

conn = new PooledConnection(dataSource.getConnection(), this);

PooledConnection 内部有个UnpooledDataSource dataSource,而dataSource.getConnection()方法真正获取一个数据库连接,使用获取的连接构造一个PooledConnection对象,PooledConnection构造函数如下

// PooledConnection
public PooledConnection(Connection connection, PooledDataSource dataSource) {
  this.hashCode = connection.hashCode();
  this.realConnection = connection;
  this.dataSource = dataSource;
  this.createdTimestamp = System.currentTimeMillis();
  this.lastUsedTimestamp = System.currentTimeMillis();
  this.valid = true;
  this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}

有两点:一个realConnection,一个proxyConnection,而上面返回的是proxyConnection,为什么要有一个proxyConnection呢?可以看到InvocationHandler是this,那便看PooledConnection.invoke方法

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  String methodName = method.getName();
  if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
    dataSource.pushConnection(this);
    return null;
  } else {
    try {
      if (!Object.class.equals(method.getDeclaringClass())) {
        // issue #579 toString() should never fail
        // throw an SQLException instead of a Runtime
        checkConnection();
      }
      return method.invoke(realConnection, args);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
}

原来之所以返回proxyConnection,是因为proxyConnection屏蔽了close方法,取而代之使用了dataSource.pushConnection(this),将连接放回连接池,而realConnection在真正需要释放连接的时候,才调用。

事务

事务在前面多少提到了,获取连接的时候,根据配置设置连接的隔离级别和是否自动提交。在需要手动提交或者回滚的时候,调用sqlSession.rollback() 或者 sqlSession.commit()

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,527评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,314评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,535评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,006评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,961评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,220评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,664评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,351评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,481评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,397评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,443评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,123评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,713评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,801评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,010评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,494评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,075评论 2 341

推荐阅读更多精彩内容

  • 1. 简介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的...
    笨鸟慢飞阅读 5,422评论 0 4
  • Java数据持久化之mybatis 一. mybatis简介 1.1 原始的JDBC操作: Java 通过 Jav...
    小Q逛逛阅读 4,890评论 0 16
  • MyBatis 理论篇 [TOC] 什么是MyBatis  MyBatis是支持普通SQL查询,存储过程和高级映射...
    有_味阅读 2,871评论 0 26
  • 001配置第一个网页填表 具体工具如上图 1、查看端口 netstat -ano 绑定填表页面相关代码 绑定支付...
    半数的年阅读 814评论 0 0
  • 每个人生来不同,我们不必去理解,只需要尊重
    慵懒的猫阅读 130评论 0 0