14、MyBatis 源码分析 - mapping 包

在上一篇文章中,我们学习了 builder 包,它的作用就是借助解析器将 XML 进行解析,然后构建为解析实体类。可以认为解析实体类就是用来存储相关信息的类。

mapping 包中就定义了很多解析实体类。从总的来看,其实大都是和 XML Mapper 文件中的一个数据库操作节点相关的实体类,还有当前环境相关的实体类。

数据库操作节点相关实体类

  • MappedStatement

  • StatementType

  • SqlSource

  • ResultMap

    • ResultMapping
  • ParameterMap

    • ParameterMapping
  • Discriminator

  • ResultSetType

环境相关实体类

  • Environment
  • DatabaseIdProvider

数据库操作节点相关实体类

数据库操作节点相关实体类如果再细分的话,其实还可以细分为 SQL输入参数处理输出结果处理

SQL 语句处理

mapping 包中和 SQL 语句处理相关的类主要有三个,分别是 MappedStatementSqlSourceBoundSqlMappedStatement 保存了数据库操作节点的相关信息,其中就包含了 SQL 语句也就是 SqlSource,但是 SqlSource 只是一个接口,它还有四个实现类,通过不同的情况选用不用的实现类,但最终得到的都是 BoundSql

MappedStatement

由于MappedStatement 保存了数据库操作节点的全部信息,所以其中有很多成员变量,但是根据操作节点的类型不同(select、insert、update、delete),参数不一定都是必须的,所以在 MappedStatement 的内部就声明了静态内部类,也就是 MappedStatement.Builder,专门用来创建 MappedStatement

核心成员变量

  // 操作节点所属的 XML Mapper 文件
  private String resource;
  // 全局配置信息
  private Configuration configuration;
  // 当前操作节点的ID
  private String id;
  // 这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。 默认值为未设置(unset)(依赖驱动)。
  private Integer fetchSize;
  // 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。
  private Integer timeout;
  // 可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement。
  // 默认值:PREPARED。
  private StatementType statementType;
  // FORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等价于 unset) 中的一个,默认值为 unset (依赖数据库驱动)。
  private ResultSetType resultSetType;
  // SQL 信息
  private SqlSource sqlSource;
  // 二级缓存
  private Cache cache;
  // 请求参数映射
  private ParameterMap parameterMap;
  // 结果映射列表,由于可能是多结果集,所以是列表形式。例如 resultMap="aa,bb"
  private List<ResultMap> resultMaps;
  // 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。
  private boolean flushCacheRequired;
  // 将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。
  private boolean useCache;
  // 这个设置仅针对嵌套结果 select 语句:如果为 true,将会假设包含了嵌套结果集或是分组,当返回一个主结果行时,就不会产生对前面结果集的引用。
  // 这就使得在获取嵌套结果集的时候不至于内存不够用。默认值:false。
  private boolean resultOrdered;
  // SQL 语句类型 UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
  private SqlCommandType sqlCommandType;

  /* 以下三个成员变量用于 insert 和 update 类型的语句*/
  // keyGenerator
  private KeyGenerator keyGenerator;
  // 主键对应的成员变量名称
  private String[] keyProperties;
  // 主键列名称
  private String[] keyColumns;

  // 是否有关联的 ResultMap
  private boolean hasNestedResultMaps;
  // 数据库ID
  private String databaseId;
  // 用于打印日志
  private Log statementLog;
  // 语言驱动。默认值:RawLanguageDriver。
  private LanguageDriver lang;
  // 这个设置仅适用于多结果集的情况,每个结果集的名称
  private String[] resultSets;

可以看到,其实是和数据库操作节点十分相近的,由于很多成员变量之前有讲过,或者是后面才会慢慢讲到,所以这里就不会展开的去解释了,大家对照着操作操作节点看一遍有个印象就好了。

SqlSource

SqlSource 是一个解析实体接口,它对应了数据库操作节点的 SQL 语句。例如下面的代码中,2、3、4 行都是属于 SqlSource。

1  <select id="selectById" resultType="user">
2    SELECT id, name, age, email
3    FROM user
4    WHERE id = ${userId}
5  </select>

由于Mybatis 中的 SQL 是特别灵活的,所以为了高效的解析,将它们的解析类做了区分。SqlSource 接口有四个实现类,如下图所示。

 

  • DynamicSqlSource:动态 SQL 语句。所谓动态 SQL 是指含有动态 SQL 节点(如 “if” 节点)或者含有 “${}” 占位符的语句。
  • RawSqlSource:原生 SQL 语句。指非动态语句,语句中可能含有 “#{}” 占位符,但不含有动态 SQL 节点,也不含有 “${}” 占位符的语句。
  • StaticSqlSource:静态语句。语句中可能含有 “?”,可以直接提交给数据库执行。
  • ProviderSqlSource:上面的三种都是从 XML 文件中提取出来的。而这个是从注解中提取出来的。由于在上篇文章中已经大概讲过了,这里就不再重复的说明了。

它们关系转换如下图所示。
 

BoundSql

BoundSqlSqlSource 层层转换得到的重要的产物,它存储的基本上就是最终形式的 SQL,并且还有实参信息。

核心成员变量

  // 可能有 "?" 占位符的 SQL 语句
  private final String sql;
  // 参数映射列表
  private final List<ParameterMapping> parameterMappings;
  // 实参对象本身
  private final Object parameterObject;
  // 实参
  private final Map<String, Object> additionalParameters;
  // additionalParameters 的包装对象
  private final MetaObject metaParameters;

输入参数的处理

主要是为了处理请求参数类型的转换,格式,保留小数位数等问题。

ParameterMap

对应的是 XML Mapper 文件中的 。例如:

  <parameterMap id="user" type="User">
    <parameter property="id" javaType="Long"/>
    <parameter property="name" javaType="String"/>
  </parameterMap>

这是老式风格的参数映射。此元素已被废弃,并可能在将来被移除!请使用行内参数映射。文档中不会介绍此元素。—— Mybatis 官网。

新式的行内参数映射格式为:#{id, javaType=Long, …}

ParameterMapping

不管是老式参数映射还是行内参数映射,每个属性对应的参数映射都是由一个 ParameterMapping 来表示的。只是存放的位置不同了,老式参数映射是存放到 ParameterMap 中的,而行内参数映射的是保存在 BoundSql 中的。

核心成员变量

  // 全局配置
  private Configuration configuration;
  // 成员变量名称
  private String property;
  // 请求参数模式 IN, OUT, INOUT
  private ParameterMode mode;
  // java类型
  private Class<?> javaType = Object.class;
  // jdbc类型
  private JdbcType jdbcType;
  // 保留小数的位数
  private Integer numericScale;
  // 类型处理器
  private TypeHandler<?> typeHandler;
  // 结果映射ID
  private String resultMapId;
  // jdbc类型名称
  private String jdbcTypeName;
  // 表达式(在当前版本的 Mybatis 中还没有使用到)
  private String expression;

输出结果处理

在映射文件的数据库操作节点中,可以直接设置 resultType 属性将输出结果映射为 Java 对象。但是更为灵活也更为强大的方式为设置 resultMap 属性,它支持输出结果的组装、判断、懒加载等。

例如:

  <resultMap id="userMap" type="User" autoMapping="false"> -- ResultMap 开始
    <id property="id" column="id"/> -- ResultMapping
    <result property="name" column="name"/> -- ResultMapping
    <discriminator javaType="int" column="sex"> -- Discriminator 开始
      <case value="0" resultMap="boyUserMap"/>
      <case value="1" resultMap="girlUserMap"/>
    </discriminator> -- Discriminator 结束
  </resultMap> -- ResultMap 结束

  <resultMap id="girlUserMap" type="Girl" extends="userMap">
    <result property="email" column="email" />
  </resultMap>

  <resultMap id="boyUserMap" type="Boy" extends="userMap">
    <result property="age" column="age" />
  </resultMap>

ResultMap

ResultMap 对应的就是 resultMap 节点,其属性和 resultMap 节点的信息基本一致。

核心成员变量

  // 全局配置信息
  private Configuration configuration;
  // resultMap 的编号
  private String id;
  // 最终输出结果对应的 Java 类
  private Class<?> type;

  /*
  * 根据属性是否在 <constructor> 中,将属性分为了两类:构造方法中的属性和非构造方法中的属性
  * 根据属性是否为 <id> 或 <idArg>,将属性分为了两类:id 属性和非 id 属性
  * */
  // XML 中的 <result> 的列表,即 ResultMapping 的列表
  private List<ResultMapping> resultMappings;
  // XML 中 <id> 和 <idArg> 的列表
  private List<ResultMapping> idResultMappings;
  // XML 中 <constructor> 中各个属性的列表
  private List<ResultMapping> constructorResultMappings;
  // XML 中非 <constructor> 相关的属性列表
  private List<ResultMapping> propertyResultMappings;
  // 所有参与映射的数据库中字段的集合
  private Set<String> mappedColumns;
  // 所有参与映射的 Java 对象属性集合
  private Set<String> mappedProperties;
  // 鉴别器
  private Discriminator discriminator;
  // 是否存在嵌套映射
  private boolean hasNestedResultMaps;
  // 是否存在嵌套查询
  private boolean hasNestedQueries;
  // 是否启动自动映射
  private Boolean autoMapping;

其中稍微难以理解的就是四个 *ResultMappings 列表。不过我相信通过下面的例子你应该就能明白了。

  <resultMap id="testResultMap" type="User">
    <constructor>
      <idArg column="id" name="id"/>  -- 是ID,也是构造器参数,保存在 idResultMappings 和 constructorResultMappings。
      <arg column="name" name="name"/> -- 是构造器参数,但不是ID,只保存在 constructorResultMappings 中。
    </constructor>
    <id column="pId" property="pId" /> -- 是ID,但不是构造器参数,保存在 idResultMappings 和 propertyResultMappings 中。
    <result column="pName" property="pName"/> -- 既不是ID,也不是构造器参数,只保存在 propertyResultMappings 中。
  </resultMap>

ResultMapping 类

<resultMap> 中的 idArgargidresult 标签都对应一个 ResultMap 对象。

核心成员变量

  // 全局配置信息
  private Configuration configuration;
  // Java Bean 成员变量名称
  private String property;
  // SQL 查询返回结果的列名称
  private String column;
  // Java 类型
  private Class<?> javaType;
  // JDBC 类型
  private JdbcType jdbcType;
  // 类型处理器
  private TypeHandler<?> typeHandler;
  // 关联的结果集ID
  private String nestedResultMapId;
  // 关联的查询语句ID
  private String nestedQueryId;
  // 不为 NULL 的列
  private Set<String> notNullColumns;
  // SQL 查询返回结果的列名称前缀
  private String columnPrefix;
  // 结果标志,枚举类型 ID, CONSTRUCTOR
  // 只会有四种情况
  // 1. 既不是ID,也不是构造器参数
  // 2. 是ID,但不是构造器参数
  // 3. 是构造器参数,但不是ID
  // 4. 是ID,也是构造器参数
  private List<ResultFlag> flags;
  // 聚合的结果集映射
  private List<ResultMapping> composites;
  // ResultSet
  private String resultSet;
  // 外键
  private String foreignColumn;
  // 懒加载
  private boolean lazy;

Discriminator

Discriminator 是 resultMap 内部的鉴别器,类似于 case when 语句,它能够根据查询结果的某些条件的不同,从而选用不同的映射。

在前面的例子中,根据 sex 字段的值来决定返回 Boy 还是 Girl。即,<discriminator> 其实就是为了选择出最终要返回的 ResultMap。接下来让我们来看看它的选择逻辑。

// DefaultResultSetHandler.java
  public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix) throws SQLException {
   
     
    // 已经处理过的鉴别器
    Set<String> pastDiscriminators = new HashSet<>();
    Discriminator discriminator = resultMap.getDiscriminator();
    while (discriminator != null) {
   
     
      // 通过 column 找到 value
      final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix);
      // 通过 value 找到对应的 resultMapId
      final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value));
      if (configuration.hasResultMap(discriminatedMapId)) {
   
     
        resultMap = configuration.getResultMap(discriminatedMapId);
        // 继续分析下一层,因为当前的 ResultMap 也可能有鉴别器
        Discriminator lastDiscriminator = discriminator;
        discriminator = resultMap.getDiscriminator();
        // 如果辨别器出现了环路就停止继续递归向下查找
        if (discriminator == lastDiscriminator || !pastDiscriminators.add(discriminatedMapId)) {
   
     
          break;
        }
      } else {
   
     
        break;
      }
    }
    return resultMap;
  }
  
  private Object getDiscriminatorValue(ResultSet rs, Discriminator discriminator, String columnPrefix) throws SQLException {
   
     
    // 要判断的列信息,记录的信息包含 <discriminator javaType="int" column="sex">
    final ResultMapping resultMapping = discriminator.getResultMapping();
    final TypeHandler<?> typeHandler = resultMapping.getTypeHandler();
    // prependPrefix() 给列名拼接前缀。然后获取对应列的值
    return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
  }

多数据库厂商处理功能

Mybatis 是可以支持多种数据库厂商的,可以根据不同的数据厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。Mybatis 会加载带有匹配当前数据库 databaseId 属性和所有不带 databaseId 属性的语句。但是如果找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被抛弃。

假设当前使用的是 mysql 数据库,样例如下:

<databaseIdProvider type="DB_VENDOR">
  <property name="Oracle" value="oracle" />
  <property name="MySQL" value="mysql" />
</databaseIdProvider>

// 有效:databaseId 匹配
<select id="selectById" resultType="user" databaseId="mysql">
  SELECT id, name, age, email
  FROM user
  WHERE id = ${userId}
</select>

// 无效:databaseId 不匹配
<select id="selectById" resultType="user" databaseId="oracle">
  SELECT id, name, age, email
  FROM user
  WHERE id = ${userId}
</select>

// 有效:没有设置 databaseId
<select id="selectMapById" resultType="Map">
  SELECT id, name, age, email
  FROM user
  WHERE id = ${userId}
</select>

DB_VENDORVendorDatabaseIdProvider 的别名,代表使用的是 VendorDatabaseIdProvider 作为数据库ID提供者,它是 DatabaseIdProvider 的子类,可以得到当前的 databaseId。代码如下:

  // 实现的 DatabaseIdProvider 的抽象方法
  @Override
  public String getDatabaseId(DataSource dataSource) {
   
     
    if (dataSource == null) {
   
     
      throw new NullPointerException("dataSource cannot be null");
    }
    try {
   
     
      return getDatabaseName(dataSource);
    } catch (Exception e) {
   
     
    }
    return null;
  }
  
  private String getDatabaseName(DataSource dataSource) throws SQLException {
   
     
    // 通过 DataSource 获取到数据库的连接,从连接中就能得到数据库厂商的信息
    String productName = getDatabaseProductName(dataSource);
    // 如果没有声明了 <databaseIdProvider/> 节点,则直接返回数据库厂商名称
      // 如果设置了,则返回预设的数据库厂商名称的别名
    if (this.properties != null) {
   
     
      for (Map.Entry<Object, Object> property : properties.entrySet()) {
   
     
        if (productName.contains((String) property.getKey())) {
   
     
          return (String) property.getValue();
        }
      }
      // 没有匹配的,返回 null
      return null;
    }
    return productName;
  }

Environment

Environment 也是一个解析实体类,对应了 Mybatis 配置文件中的 <environments/> 节点。对应关系如下:

mybatis-config.xml 的 environments 节点配置

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC"/>
    <dataSource type="UNPOOLED">
      <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
      <property name="url" value="jdbc:mysql://127.0.0.1:3306/test1"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
  </environment>
</environments>

Environment 核心成员变量

  // 数据源ID,在当前配置下为:development
  private final String id;
  // 事务工厂类
  private final TransactionFactory transactionFactory;
  // 数据源
  private final DataSource dataSource;

CacheBuilder

是用于构建 Cache 的建造器类。Mybatis 的缓存策略使用的是装饰者模式,所以有一个基础的实现类也就是 implementation,再其上通过 decorators 来实现缓存功能。Mybatis 的默认基础实现类为 PerpetualCache 其中的记录都是永久保存的。默认的装饰者为 LruCache 它会通过记录每次所有缓存的 key,在 key 的数量超过限定的时候可以通过调用代理类的删除方法,删除最近最少使用的缓存,防止缓存记录无限增长。

核心成员变量

  // 映射文件的命名空间
  private final String id;
  // 缓存的实现类
  private Class<? extends Cache> implementation;
  // 装饰者
  private final List<Class<? extends Cache>> decorators;
  // 缓存大小
  private Integer size;
  // 定时清除缓存的间隔
  private Long clearInterval;
  // 允许读和写
  private boolean readWrite;
  // 需要向 Cache 中设置的配置
  private Properties properties;
  // 是否并发阻塞
  private boolean blocking;

核心方法

  public Cache build() {
   
     
    // 设置默认的基础实现类和装饰者
    setDefaultImplementations();
    // 创建一个基础的 Cache 实现类
    Cache cache = newBaseCacheInstance(implementation, id);
    // 为参数设置属性
    setCacheProperties(cache);
    if (PerpetualCache.class.equals(cache.getClass())) {
   
     
      // 设置用户声明的装饰器类
      for (Class<? extends Cache> decorator : decorators) {
   
     
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
      }
      // 设置标准的装饰器类
      cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
   
     
      // 如果基础实现类不是 PerpetualCache 说明是用户自定义的基础实现类,则装饰类就只有 Logging
      cache = new LoggingCache(cache);
    }
    return cache;
  }
  
  private void setDefaultImplementations() {
   
     
    if (implementation == null) {
   
     
      // 设置默认的基础实现类
      implementation = PerpetualCache.class;
      if (decorators.isEmpty()) {
   
     
        // 默认的装饰者类
        decorators.add(LruCache.class);
      }
    }
  }

  private Cache setStandardDecorators(Cache cache) {
   
     
    MetaObject metaCache = SystemMetaObject.forObject(cache);
    // 如果有 size 属性,则需要设置属性值
    if (size != null && metaCache.hasSetter("size")) {
   
     
      metaCache.setValue("size", size);
    }
    // 如果设置了缓存的清理间隔,则包装定期删除缓存的装饰类
    if (clearInterval != null) {
   
     
      cache = new ScheduledCache(cache);
      ((ScheduledCache) cache).setClearInterval(clearInterval);
    }
    // 如果允许读写缓存,则包装序列化的装饰类
    if (readWrite) {
   
     
      cache = new SerializedCache(cache);
    }
    // 包装能打印缓存命中率的装饰类
    cache = new LoggingCache(cache);
    // 包装能让方法修饰了 synchronized 的装饰类
    cache = new SynchronizedCache(cache);
    // 如果允许阻塞的话,就要包装支持阻塞的装饰类
    if (blocking) {
   
     
      cache = new BlockingCache(cache);
    }
    return cache;
  }

可以看到使用了很多的装饰类,由于后面都会讲到,所以就不在这里详细展开了。可以发现,通过装饰者模式可以灵活的进行功能扩展

辅助枚举类

在mapping 包中还有一些枚举类。作用如下:

  • FetchType:延迟加载设置;
  • ParameterMode:参数类型,指输入参数、输出参数等;
  • ResultFlag:返回结果中属性的特殊标志,表示是否为ID属性、是否为构造器属性;
  • ResultSetType:结果集支持的访问方式;
  • SqlCommandType:SQL 命令类型,指增、删、改、查等;
  • StatementType:SQL 语句种类,指是否为预编译的语句,是否为存储过程等。