Mybatis源码之映射器解析

java宝典

共 18107字,需浏览 37分钟

 ·

2020-12-14 13:46


点击上方蓝字关注我!



Mybatis映射器

映射器是MyBatis最强大的⼯具,也是我们使用MyBatis时⽤得最多的工具,因此熟 练掌握它⼗分必要。MyBatis是针对映射器构造的SQL构建的轻量级框架,并且通过配置 生成对应的JavaBean返回给调用者,⽽这些配置主要便是映射器,在MyBatis中你可以根 据情况定义动态SQL来满足不同场景的需要,它比其他框架灵活得多。MyBatis还支持⾃动绑定JavaBean, 我们只要让SQL返回的字段名和JavaBean 的属性名保持一致(或者采⽤驼峰式命名),便可以省掉这些繁琐的映射配置

目录:

  • Mybatis映射器

  • 映射器的主要元素

  • Select元素

  • insert元素

  • sql元素

  • resultMap元素

  • cache元素

  • 映射器的内部组成


我们先再回顾下映射器的主要元素

映射器的主要元素

映射器是由Java接口和XML文件(或注解)共同组成的,Java接口主要定义调用者接口,XML文件是配置映射器的核心文件,包括以下元素:


  • select 查询语句,可以自定义参数,返回结果集;

  • insert 插入语句,返回一个整数,表示插入的条数;

  • update 更新语句,返回一个整数,表示更新的条数;

  • delete 删除语句,返回一个整数,表示删除的条数;

  • sql 允许定义一部分SQL,然后再各个地方引用;

  • resultMap 用来描述从数据库结果集中来加载对象,还可以配置关联关系,提供映射规则;

  • cache 给定命名空间的缓存配置


Select元素

select元素帮助我们 从数据库中读出数据,组装数据给业务人员。执行select语句前,我们需要定义参数,它 可以是⼀个简单的参数类型,例如int, float , String,也可以是⼀个复杂的参数类型 例如 JavaBean、 Map等,这些都是MyBatis接受的参数类型。

执⾏SQL后,MyBatis也提供了 强⼤的映射规则,自动映射来帮助我们把返回的结果集绑定到JavaBean中。

select 元素的配置众多,下面简单说明下:

  • id: id和Mapper的命名空间组成唯一值,提供给Mybatis调用,如果不唯一将会报错

  • paramterType:传入的参数类型,可以是基本类型、map、自定义的java bean;

  • resultType:返回的结果类型,可以是基本类型、自定义的java bean;

  • resultMap:它是最复杂的元素,可以配置映射规则、级联、typeHandler等,与ResultType不能同时存在;

  • flushCache:在调用SQL后,是否要求清空之前查询的本地缓存和二级缓存,主要用于更新缓存,默认为false;

  • useCache:启动二级缓存的开关,默认只会启动一级缓存;

  • timeout:设置超时参数,等超时的时候将抛出异常,单位为秒;

  • fetchSize:获取记录的总条数设定;


select实例

需求:  查询名称等于JAVA宝典的用户

在UserDao中定义接口方法:

User findIdByName(String name);

定义UserMapper.xml

<select id="findIdByName" parameterType="string" resultType="User">
        select
        u.*
        from t_user u
        where u.name=#{name}
select>
 

对操作步骤进行归纳概括:

  • Id标出了了这条SQL
  • parameterType定义参数类型
  • resuitType定义返回值类型

上面的例子只是传入单个参数,多个参数可以使用Map,JavaBean,使用注解方式,等等,下面我们会单独介绍.


insert元素

insert属性和select大部分都相同, 说下3个不同的属性:

  • keyProperty:指定哪个列是主键,如果是联合主键可以用逗号隔开;

  • keyColumn:指定第几列是主键,不能和keyProperty共用;

  • useGeneratedKeys:是否使用自动增长,默认为false;当useGeneratedKeys设为true时,在插入的时候,会回填Java Bean的id值,通过返回的对象可获取主键值。

如果想根据一些特殊关系设置主键的值,可以在insert标签内使用selectKey标签

<insert id="insertRole" useGeneratedKeys="true" keyProperty="id" >
    <selectKey keyProperty="id" resultType="int" order="before">
        select if(max(id) is null,1,max(id)+2) as newId from t_role    
  selectKey
insert>

update和delete 就不单独过多介绍了

sql元素

sql元素的意义,在于我们可以定义⼀串串SQL语句的组成部分,其他的语句可以通过引⽤来使⽤它。

例如,你有一条SQL需要select⼏⼗个字段映射到JavaBean中去,我的第二 条SQL也是这⼏⼗个字段映射到JavaBean中去,显然这些字段写两遍不太合适。那么我们 就⽤sql元素来完成

定义:

<sql id="columns">
    id,
    name,
    remark,
    tid
sql>

使用:

 <select id="getOne" resultType="com.liangtengyu.entity.Ttest">
  select  <include refid="columns"/>  from t_test where id  = #{id}
 select>

resultMap元素

resultMap是MyBatis里面最复杂的元素,它的作用是定义映射规则、级联的更新、定制类型转换器等。

由以下元素构成:

<resultMap>
    <constructor> 
        <idArg/>
        <arg/>
    constructor>
    <id/> 
    <result/> 
    <association/> 
    <collection/> 
    <discriminator> 
        <case/>
    discriminator>
resultMap>

有的实体不存在没有参数的构造方法,需要使用constructor配置有参数的构造方法:

<resultMap id="role" type="com.liangtengyu.entity.Role">
    <constructor>
        <idArg column="id" javaType="int"/>
        <arg column="role_name" javaType="string"/>
    constructor>
resultMap>

id指明主键列,result配置数据库字段和POJO属性的映射规则:

association、collection用于配置级联关系的,分别为一对一和一对多,实际中,多对多关系的应用不多,因为比较复杂,会用一对多的关系把它分解为双向关系。

discriminator用于这样一种场景:比如我们去体检,男和女的体检项目不同,如果让男生去检查妇科项目,是不合理的, 通过discriminator可以根据性别,返回不同的对象。

级联关系的配置比较多,就不在此演示了,可查看文档进行了解。

cache元素

在没有显示配置缓存时,只开启一级缓存,一级缓存是相对于同一个SqlSession而言的,在参数和SQL完全一样的情况下,使用同一个SqlSession对象调用同一个Mapper的方法,只会执行一次SQL。如果是不同的SqlSession对象,因为不同SqlSession是相互隔离的,即使用相同的Mapper、参数和方法,还是会再次发送SQL到数据库去执行。

二级缓存是SqlSessionFactory层面上的,需要进行显示配置

这样很多设置是默认的,有如下属性可以配置:

  • eviction:代表缓存回收策略,可选值有LRU最少使用、FIFO先进先出、SOFT软引用,WEAK弱引用;

  • flushInterval:刷新间隔时间,单位为毫秒,如果不配置,当SQL被执行时才会刷新缓存;

  • size:引用数目,代表缓存最多可以存储多少对象,不宜设置过大,设置过大会导致内存溢出;

  • readOnly:只读,意味着缓存数据只能读取不能修改;

在大型服务器上,可能会使用专用的缓存服务器,比如Redis缓存,可以通过实现org.apache.ibatis.cache.Cache接口很方便的实现:

public interface Cache {    
    String getId()//缓存编号
    void putObject(Object var1, Object var2)//保存对象
    Object getObject(Object var1)//获取对象
    Object removeObject(Object var1)//移除对象
    void clear()//清空缓存
    int getSize()//获取缓存对象大小
    ReadWriteLock getReadWriteLock()//获取缓存的读写锁
    }

映射器的内部组成

一般而言,一个映射器是由3个部分组成:

打开Mybatis源码,在mapping包中可以找到他们

  • MappedStatement,它保存映射器的一个节点(select|insert|delete|update)并且包括许多我们配置的sql,sql的id、缓存信息,resultMap,parameterType、resultType、languageDriver等重要的配置内容。

  • SqlSource,它是提供BoundSql对象的地方,它是MappedStatement的一个属性。它的主要作用是根据参数和其他规则组装sql。

  • BoundSql,它是建立SQL和参数的地方,他有3个常用的属性:SQLparameterObjectparameterMappings 这3个等会介绍.

idea生成的依赖图

MappedStatement创建过程

首先我们介绍一下MappedStatement

在SqlSessionFactoryBuilder.build()方法,它会帮我们创建一个SqlSessionFactory,它的build方法:

 public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());//此处使用解析XML返回的数据构建 SqlSessionFactory 跟进去..
    } 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.
      }
    }
  }

进入到parser.parse()方法看看:

 public Configuration parse() {
    if (parsed) { //如果一个xml文件解析两次 就会报错 否则才去解析
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));//解析节点configuration,跟进去...
    return configuration;
  }

跟进代码parseConfiguration(parser.evalNode("/configuration"));

 private void parseConfiguration(XNode root) {
    try {
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));//解析了很多,但是今天我们的主角是这个..跟进去.
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");//获取三种方式指定的路径 进行判断 告诉MyBatis 到哪里去找映射文件
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();//调用 mapperParser.parse();解析xml  跟进去...
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();//调用 mapperParser.parse();解析xml
          } else if (resource == null && url == null && mapperClass != null) { 
            Class mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

可以看到在解析mapper标签

  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));//解析xml的mapper标签,并返回调用configurationElement()方法
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();//缺少资源的再尝试一下.具体逻辑我们不深入走这里了,有兴趣的可以看看源码.
    parsePendingCacheRefs();
    parsePendingStatements();
  }

configurationElement()方法

  private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.isEmpty()) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

buildStatementFromContext()方法:


  private void buildStatementFromContext(List list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();//会调用此处,继续跟进
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

statementParser.parseStatementNode();

  public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered"false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    String parameterType = context.getStringAttribute("parameterType");
    Class parameterTypeClass = resolveClass(parameterType);

    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre:  and  were parsed and removed)
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //SqlSource是在这里创建的 记住这里等会我们再来分析她.先继续走完流程.
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");
    //对各种属性进行了解析并且调用addMappedStatement方法.
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, 
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

addMappedStatement()方法:

public MappedStatement addMappedStatement(
      String id,
      SqlSource sqlSource,
      StatementType statementType,
      SqlCommandType sqlCommandType,
      Integer fetchSize,
      Integer timeout,
      String parameterMap,
      Class parameterType,
      String resultMap,
      Class resultType,
      ResultSetType resultSetType,
      boolean flushCache,
      boolean useCache,
      boolean resultOrdered,
      KeyGenerator keyGenerator,
      String keyProperty,
      String keyColumn,
      String databaseId,
      LanguageDriver lang,
      String resultSets)
 
{

    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);//configuration内部放入了一个Map
    return statement;//最终返回MappedStatement
  }

至此,MappedStatement创建完毕


SqlSource的创建过程

回到parseStatementNode()方法找到:

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

createSqlSource方法是LanguageDriver接口的一个方法 她有4个实现

我们看一下xml实现

public class XMLLanguageDriver implements LanguageDriver {

  @Override
  public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
  }

  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();//进行解析,我们跟进..
  }
  ....
}

  public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);//对动态标签的处理
    SqlSource sqlSource;//初始化Null
    if (isDynamic) {//判断是否动态,来调用不同的SqlSource实例.
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {//跟进RawSqlSource()
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }
  public RawSqlSource(Configuration configuration, String sql, Class parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class clazz = parameterType == null ? Object.class : parameterType;
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
  }

sqlSourceParser.parse()方法要对sql进行处理,这里我们打个断点,可以看到原始参数sql为

insert into t_test set context = #{context}

parse方法处理完毕返回:insert into t_test set context = ?并且调用了StaticSqlSource的构造方法

StaticSqlSource类继实现了SqlSource接口,

public class StaticSqlSource implements SqlSource {

  private final String sql;
  private final List parameterMappings;
  private final Configuration configuration;

  public StaticSqlSource(Configuration configuration, String sql) {
    this(configuration, sql, null);
  }

  public StaticSqlSource(Configuration configuration, String sql, List parameterMappings) {
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.configuration = configuration;
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {//这个方法是不是眼熟,BoundSql的构建
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

}

至此,SqlSource创建完毕.


BoundSql

public class BoundSql {

  private final String sql;
  private final List parameterMappings;
  private final Object parameterObject;
  private final Map additionalParameters;
  private final MetaObject metaParameters;

........
}

创建SqlSource时,就把参数放入BoundSql类中来构建一个new BoundSql

public BoundSql getBoundSql(Object parameterObject) {
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

configuration, sql, parameterMappings, parameterObject这些参数是非常重要的我们逐个分析下

configuration

这个不用多说了吧 如果不认识她,那直接打回新手村

sql

这个属性保存的是我们在映射文件xml中写的sql语句

例子就是刚刚解析出来的Sql语句:insert into t_test set context = ?

parameterMappings

List parameterMappings;

它是一个list,每一个元素都是parameterMapping对象,这个对象会描述包括属性、名称、表达式、javaType、jdbcType、typeHandler等重要信息。

ParameterMapping
public class ParameterMapping {

  private Configuration configuration;

  private String property;
  private ParameterMode mode;
  private Class javaType = Object.class;
  private JdbcType jdbcType;
  private Integer numericScale;
  private TypeHandler typeHandler;
  private String resultMapId;
  private String jdbcTypeName;
  private String expression;
  ......
  }

parameterObject

parameterObject是参数,可以传递简单对象,pojo、map或者@param注解的参数

传入简单对象 (包括int、 float、 double等),⽐如当我们传递int类型时,MyBatis 会把参数包装成为Integer对象传递,类似的long、 float、 double也是如此.

传入pojo、map那么这个parameterObject参数就是你传入的POJO或者Map不变

传入多个参数,不带@Param注解那么MyBatis就会把 parameterObject变为一个Map

传入多个参数,带@Param注解如果我们使⽤@Param注解,那么MyBatis就会把parameterObject变为一个Map

⽐如我们注解 (@Param{"name"} String var1,@Param{"age"} int var2)数字的键值对应置换为了name和age

  (完)



扫描二维码

加学习群

扫码加群




浏览 16
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报