搭建环境

本要将相应的工程,放在 github 了,这个是最小的 MyBatis 应用了,导入工程到自己喜欢的IDE里,就可以开始一步一步调试了。

github emacsist mybatis-hello-world

概要

    public static void main(String[] args) throws IOException {
        String resource = "com/github/emacsist/mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession session = sqlSessionFactory.openSession();
        try {
            PersonMapper mapper = session.getMapper(PersonMapper.class);
            Person person = mapper.selectOne(1);
            System.out.println(person.getName());
        } finally {
            session.close();
        }
    }

可以看到,整个流程就是:

创建配置 -> 初始化配置并创建 SqlSessionFactory -> 获取 SqlSession -> 执行 Mapper 的方法 -> 获取返回结果

MyBatis 的配置 Configuration

对应的类为: org.apache.ibatis.session.Configuration

总体:

img

Environment

org.apache.ibatis.mapping.Environment

这个可以根据不同的环境,来设置不同的DB及连接信息。

这个对应的XML配置是:

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://10.0.0.40:3308/test?useUnicode=true"/>
                <property name="username" value="uniweibo"/>
                <property name="password" value="uniweibo.com"/>
            </dataSource>
        </environment>
    </environments>

transactionManager 有两种: JDBCMANAGED dataSource : 数据源,类型可以为

  • UNPOOLED : 即每次都创建一个新的连接,用完后关闭
  • POOLED : 使用数据连接池来管理连接
  • JNDI : 使用 JNDI 来管理

properties

这个可以直接引用外部的 properties 文件,并且可以替换它现有的值

<properties resource="org/mybatis/example/config.properties">
  <property name="username" value="dev_user"/>
  <property name="password" value="F2Fa3!33TYyg"/>
</properties>

settings

这些是用于 Environment 常规字段配置的。

    <settings>
        <!-- changes from the defaults for testing -->
        <setting name="cacheEnabled" value="false" />
        <setting name="useGeneratedKeys" value="true" />
        <setting name="defaultExecutorType" value="REUSE" />
    </settings>

这里重点说一下 defaultExecutorType

defaultExecutorType

Executor 层次图

img

SIMPLE

如果不显式指定的话,那这个是默认值。

REUSE

BATCH

typeAliases

为Java类型设置别名

typeHandlers

类型处理器.

MyBatis 已经自带了不少类型处理器了,在包 org.apache.ibatis.type 下面:

img

也可以在 org.apache.ibatis.type.TypeHandlerRegistry 类中查看当前对应的Java类型及相应的处理器

注意, 在 Mybatis 中, 任何类型的返回值, 都是对象类型. 即使写了 int 作为返回类型. 背后都是通过 IntegerTypeHandler 来处理, 它会返回 null. 通上面的 TypeHandlerRegistry 可知.

databaseIdProvider

MyBatis 会加载不带 databaseId 属性和带有匹配当前数据库 databaseId 属性的所有语句。 如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被舍弃。 为支持多厂商特性只要像下面这样在 mybatis-config.xml 文件中加入 databaseIdProvider 即可

<databaseIdProvider type="DB_VENDOR">
  <property name="SQL Server" value="sqlserver"/>
  <property name="DB2" value="db2"/>        
  <property name="Oracle" value="oracle" />
</databaseIdProvider>

DB_VENDOR 的获取:

System.out.println(session.getConnection().getMetaData().getDatabaseProductName());

databaseId 可以在 Mapper 文件中的语句加上该属性。

    <select id="selectOne" resultType="com.github.emacsist.pojo.Person" databaseId="db2">
        select * from person where id = #{id}
    </select>

这样子,就可以根据不同的数据库提供商,去选择不同的语句了。

mappers

这个配置就是指示 mybatis 如何查找我们的 Mapper 文件的。详情看下面的参考资料。通过配置文件的配置,它会解析及加载到 Configuration 的:

protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

对象中,这时,就可以通过 SqlSession.getMapper(Class) 方法,来获取不同的 Mapper

加载及解析配置文件

mybatis-config.xml 读取并解析为 org.apache.ibatis.session.Configuration 对象

InputStream inputStream = Resources.getResourceAsStream(resource);
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, null, null);
Configuration config = parser.parse();

参考资料

MyBatis 官方文档 XML 映射配置文件

初始化并创建 SqlSessionFactory

// 这个 inputStream 对象,就是上面读取 mybatis-config.xml 后转换成的输入流
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

经过上面的解析后可知,inputStream 最终转换为的是 Configuration 对象,然后利用该对象来构建 SqlSessionFactory

public class DefaultSqlSessionFactory implements SqlSessionFactory {

  private final Configuration configuration;

  public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
  }

获取 SqlSession

SqlSessionFactory 负责与DB的一次会话机制,以及打开会话时设置的会话级别的配置(比如事务级别、是否自动提交、选择 Executor 的类型等)

public interface SqlSessionFactory {

  SqlSession openSession();

  SqlSession openSession(boolean autoCommit);
  SqlSession openSession(Connection connection);
  SqlSession openSession(TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType);
  SqlSession openSession(ExecutorType execType, boolean autoCommit);
  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType, Connection connection);

  Configuration getConfiguration();

}

通过 SqlSessionFactory 的不同方法,可以选择不同的 SqlSession 的配置。

执行 Mapper 的方法

通过 SqlSession.getMapper(Class) 方法获取要执行的 Mapper 。 执行到这里,就已经可以决定一条SQL是如何执行的了。之前的 SqlSession 构建完成后,就可以知道它的属性了:

    this.configuration = configuration;
    this.executor = executor;
    this.dirty = false;
    this.autoCommit = autoCommit;

其中最重要的,就是它的 Executor 的类型(执行器的类型)。

这个 Mapper ,它使用了动态代理的机制:

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

可以看到,它使用了 MapperProxyFactory 代理工厂方法的设计模式,它的代理类就是: org.apache.ibatis.binding.MapperProxy

可以看到,这个就是整个执行时,MyBatis 为我们代理的统一抽象的DB处理。

调用代理方法时,它首先从 MapperMethod 缓存中查找是否已经存在,否则加入本地缓存后再返回(加快查找及构造对象),然后执行 MapperMethod.execute() 方法来执行相应的 CRUD 操作,以及自动映射结果集为我们为 mapper 语句定义的返回类型。

杂项

MyBatis 是如何处理动态SQL的?

img

这在在解析配置文件为 Configuration 对象时,所有的 Mapper 文件,以及它对应的 SQL 文本,都已经被解析为相应的对象了。而动态SQL,则是通过 DynamicSqlSource 对象来保存它的表达式及该表达式所对应的 SQL 语句。

表达式的 eval 是通过 org.apache.ibatis.scripting.xmltags.ExpressionEvaluator 类来进行表达式解析的。

组装动态SQL完成后,就到了解析SQL了。因为我们在写SQL的时候,一般是使用 #{} 的命名参数的形式,但是对于JDBC来说,参数它是必须 ? 的形式的,然后以相应的 value 来进行替换。所以,这里 MyBatis 又要进行一些转换了

SqlSourceBuilder.parse() 方法,就是用来进行SQL解析的。逻辑如下:

判断动态SQL满足的条件的分支,生成最终版的命名的参数 SQL -> #{} 这些的占位符进行分词 -> 将 #{} 替换为 ? -> 将参数对象按顺序,再将 ? 进行 PrepareStatement (防SQL注入)来替换值,这时就形成了最终的可执行的 SQL 语句了。

注意,如果使用的是 ${} 而不是 #{} ,则不会进行将 ${} 转换为 ? ,而是直接将 ${} 替换为相应的值(这可能会造成SQL注入)

img

唯一性

Mybatis 中是通过 mapperInterface.getName() + "." + methodName 来确定唯一性的, 而不考虑参数的情况.

所有的Mapper statement

保存在 org.apache.ibatis.session.Configuration.mappedStatements 成员中.

映射结果

映射 ResultMap

org.apache.ibatis.executor.resultset.ResultSetWrapper

// 将 DB 的元数据: 列名, 列的数据类型, 及对应的Java class 类型保存起来
public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException {
  super();
  this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
  this.resultSet = rs;
  final ResultSetMetaData metaData = rs.getMetaData();
  final int columnCount = metaData.getColumnCount();
  for (int i = 1; i <= columnCount; i++) {
    columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i));
    jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i)));
    classNames.add(metaData.getColumnClassName(i));
  }
}

//处理 resultMap 
org.apache.ibatis.executor.resultset.DefaultResultSetHandler
  
  private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
      if (parentMapping != null) {
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
        if (resultHandler == null) {
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
      // issue #228 (close resultsets)
      closeResultSet(rsw.getResultSet());
    }
  }

默认所有的 Java 类型的映射实际逻辑, 都继承了 BaseTypeHandler. 3.5.2 版本的有如下(在 org.apache.ibatis.type 包)

NClobTypeHandler (org.apache.ibatis.type)
ClobReaderTypeHandler (org.apache.ibatis.type)
OffsetTimeTypeHandler (org.apache.ibatis.type)
ByteObjectArrayTypeHandler (org.apache.ibatis.type)
DateOnlyTypeHandler (org.apache.ibatis.type)
BlobTypeHandler (org.apache.ibatis.type)
DateTypeHandler (org.apache.ibatis.type)
IntegerTypeHandler (org.apache.ibatis.type)
SqlTimeTypeHandler (org.apache.ibatis.type)
NStringTypeHandler (org.apache.ibatis.type)
CharacterTypeHandler (org.apache.ibatis.type)
ArrayTypeHandler (org.apache.ibatis.type)
StringTypeHandler (org.apache.ibatis.type)
BigDecimalTypeHandler (org.apache.ibatis.type)
EnumOrdinalTypeHandler (org.apache.ibatis.type)
BooleanTypeHandler (org.apache.ibatis.type)
SqlTimestampTypeHandler (org.apache.ibatis.type)
BlobInputStreamTypeHandler (org.apache.ibatis.type)
BlobByteObjectArrayTypeHandler (org.apache.ibatis.type)
MonthTypeHandler (org.apache.ibatis.type)
EnumTypeHandler (org.apache.ibatis.type)
FloatTypeHandler (org.apache.ibatis.type)
TimeOnlyTypeHandler (org.apache.ibatis.type)
ByteTypeHandler (org.apache.ibatis.type)
YearMonthTypeHandler (org.apache.ibatis.type)
InstantTypeHandler (org.apache.ibatis.type)
ObjectTypeHandler (org.apache.ibatis.type)
ClobTypeHandler (org.apache.ibatis.type)
SqlxmlTypeHandler (org.apache.ibatis.type)
DoubleTypeHandler (org.apache.ibatis.type)
ShortTypeHandler (org.apache.ibatis.type)
LongTypeHandler (org.apache.ibatis.type)
LocalDateTypeHandler (org.apache.ibatis.type)
UnknownTypeHandler (org.apache.ibatis.type)
BigIntegerTypeHandler (org.apache.ibatis.type)
ByteArrayTypeHandler (org.apache.ibatis.type)
OffsetDateTimeTypeHandler (org.apache.ibatis.type)
JapaneseDateTypeHandler (org.apache.ibatis.type)
LocalDateTimeTypeHandler (org.apache.ibatis.type)
ZonedDateTimeTypeHandler (org.apache.ibatis.type)
SqlDateTypeHandler (org.apache.ibatis.type)
YearTypeHandler (org.apache.ibatis.type)
LocalTimeTypeHandler (org.apache.ibatis.type)

常见的类型, 在 MySQL JDBC 实现中的处理逻辑, 在包 com.mysql.cj.result 中. 例如 BooleanValueFactory

    @Override
    public Boolean createFromLong(long l) {
        // Goes back to ODBC driver compatibility, and VB/Automation Languages/COM, where in Windows "-1" can mean true as well.
        return (l == -1 || l > 0);
    }

自动映射 POJO

默认情况下, pojo 的字段名与列名一致, 则在返回 resultType=xxx.yyy.zzPojo 时自动映射到相应的值.

默认情况下 mapUnderscoreToCamelCase 配置参数是 false, 即不将数据库列名风格为下划线的字段名, 转换为驼峰式. 这个看自己需求.

默认的配置参数值在类 org.apache.ibatis.session.Configuration 中. 相应的处理代码在

org.apache.ibatis.executor.resultset.DefaultResultSetHandler 中的 getRowValue() 方法. 然后为 pojo 设置映射值在 applyAutomaticMappings() 方法中.

性能优化

开启SQL压缩

img

可以看到,我们在 XML 文件格式化的换行、空格等这些符号,在保留后的SQL里还是存在的,但这些了占了额外的大小,在数据传输时,也要占用一定的数据大小(或者许多人觉得,这个影响并不大,但如果你在执行批量处理的时候,这个放大的倍数就比较可观了)

这时,可以在 JDBC 连接里,加上 useCompression=true ,添加个压缩选项。

指量处理的SQL 导致的性能问题

在 MyBatis 执行批量操作时,过多的命名参数的话,MyBatis 要进行解析,然后转换为 PrepareStatement ,这需要花费不少的时间的,可参考另一篇 Blog: 记录一次 MySQL 批量插入的优化

在 MyBatis / JDBC ,批量处理的情况为:

  • 开启事务,执行多条 SQL ,然后提交事务。即 insert …values (xxx); insert …values(yyyy)
  • 或 执行一条 insert … values (xxx), (yyy)

这个建议使用第二种,因为可以大量减少数据的传输。

可能在 JDBC 连接上,加上 rewriteBatchedStatements=true 这个改写的选项。开启后,它会将第一种的SQL语句,改写为第二种的SQL语句后,再进行发送到DB。

映射结果

  • List 的话, 如果没有数据, 返回的是空的 List 而不是 null
  • 对象的话, 如果没有数据, 返回的是 null

参考资料