文档章节

Mybatis之XML如何映射到方法

ksfzhaohui
 ksfzhaohui
发布于 11/10 10:03
字数 2138
阅读 30
收藏 0

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

前言

上文Mybatis之方法如何映射到XML中介绍了Mybatis是如何将方法进行分拆出方法名映射到statementID,参数如何解析成xml中sql所需要的,以及返回类型的处理;本文将从XML端来看是如何同方法端进行映射的。

XML映射类

前两篇文章中了解到通过Mapper类路径+方法名映射xxMapper.xml中的namespace+statementID,而namespace+statementID块其实在初始化的时候在Configuration中保存在MappedStatement中,所以我们在增删改查的时候都会看到如下代码:

MappedStatement ms = configuration.getMappedStatement(statement);

在Configuration中获取指定namespace+statementID的MappedStatement,而在Configuration是通过Map维护了对应关系;已最常见的Select语句块为例,在XML中的配置的结构如下:

<select
  id="selectPerson"
  parameterType="int"
  parameterMap="deprecated"
  resultType="hashmap"
  resultMap="personResultMap"
  flushCache="false"
  useCache="true"
  timeout="10"
  fetchSize="256"
  statementType="PREPARED"
  resultSetType="FORWARD_ONLY">
  ...sql语句...
</select>

其他增删改除了个别的几个关键字比如:keyProperty,keyColumn等,其他和select标签类似;再来看一下MappedStatement类中相关的属性:

public final class MappedStatement {

  private String resource;
  private Configuration configuration;
  private String id;
  private Integer fetchSize;
  private Integer timeout;
  private StatementType statementType;
  private ResultSetType resultSetType;
  private SqlSource sqlSource;
  private Cache cache;
  private ParameterMap parameterMap;
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;
  private boolean resultOrdered;
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;
  ...省略...
}

select标签里面的关键字基本可以在类MappedStatement中找到对应的属性,关于每个属性代表的含义可以参考官方文档:mybatis-3;除了关键字还有sql语句,对应的是MappedStatement中的SqlSource,sql语句有动态和静态的区别,对应的SqlSource也提供了相关的子类:StaticSqlSource和DynamicSqlSource,相关的sql解析类在XMLScriptBuilder中:

  public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

具体哪种sql是动态的,哪种是静态的,相关逻辑在parseDynamicTags中判断的,此处大致说一下其中的原理:遇到${}和动态标签如<if>,<set>,<foreach>则为DynamicSqlSource,否则为StaticSqlSource也就是常见的#{};在解析动态sql的时候Mybatis为每个标签专门提供了处理类NodeHandler,初始化信息如下:

  private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
  }

处理完之后会生成对应的SqlNode如下图所示:
xx.jpg

不管是动态还是静态的SqlSource,最终都是为了获取BoundSql,如SqlSource接口中定义的:

public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

这里的parameterObject就是上文中通过方法参数生成的sql参数对象,这样BoundSql包含了sql语句,客户端传来的参数,以及XML中配置的参数,直接可以进行映射处理;

参数映射

上节中将到了BoundSql,本节重点来介绍一下,首先可以看一下其相关属性:

public class BoundSql {

  private final String sql;
  private final List<ParameterMapping> parameterMappings;
  private final Object parameterObject;
  private final Map<String, Object> additionalParameters;
  private final MetaObject metaParameters;
  ...省略...
}

几个属性大致含义:要执行的sql语句,xml配置的参数映射,客户端传来的参数以及额外参数;已一个常见的查询为例可以看一下大致的内容:

    <select id="selectBlog3" parameterType="hashmap" resultType="blog">
        select * from blog where id = #{id} and author=#{author,jdbcType=VARCHAR,javaType=string}
    </select>

此时sql对应就是:

select * from blog where id = ? and author=?

parameterMappings对应的是:

ParameterMapping{property='id', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
ParameterMapping{property='author', mode=IN, javaType=class java.lang.String, jdbcType=VARCHAR, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}

parameterObject对应的是:

{author=zhaohui, id=158, param1=158, param2=zhaohui}

如果知道以上参数,我们就可以直接使用原生的PreparedStatement来操作数据库了:

PreparedStatement prestmt = conn.prepareStatement("select * from blog where id = ? and author=?");
prestmt.setLong(1,id);
prestmt.setString(2,author);

其实Mybatis本质上和上面的语句没有区别,可以看一下Mybatis是如何处理参数的,具体实现在DefaultParameterHandler中,如下所示:

 public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          } catch (SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

大致就是遍历parameterMappings,然后通过propertyName到客户端参数parameterObject中获取对应的值,获取到值之后就面临一个问题一个就是客户端参数的类型,另一个就是xml配置的类型,如何进行转换,Mybatis提供了TypeHandler来进行转换,这是一个接口类,其实现包括了常用的基本类型,Map,对象,时间等;具体使用哪种类型的TypeHandler,根据我们在xml中配置的<javaType=类型>来决定,如果没有配置则使用UnknownTypeHandler,UnknownTypeHandler内部会根据value的类型来决定使用具体的TypeHandler;Mybatis内部所有的类型都注册在TypeHandlerRegistry中,所以获取的时候直接根据value类型直接去TypeHandlerRegistry获取即可;获取之后直接调用typeHandler.setParameter(ps, i + 1, value, jdbcType),已StringTypeHandler为例,可以看一下具体实现:

  public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setString(i, parameter);
  }

使用了原生的PreparedStatement,往指定位置设置参数值;设置完参数之后就执行execute方法,返回结果;

结果映射

上一节执行execute之后,返回的是ResultSet结果集,如果直接用原生的读取方式,你会看到如下代码:

ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
      Long id = resultSet.getLong("id");
      String title = resultSet.getString("title");
      String author = resultSet.getString("author");
      String content = resultSet.getString("content");
      ......
}

获取到每个字段的数据之后,然后通过反射的方式生成一个对象;Mybatis内部其实也是这样实现的,封装好通过简单的配置即可获取结果集,常见的结果集配置如resultMap,resultType等;Mybatis内部处理ResultSet是ResultSetHandler,其具体实现是DefaultResultSetHandler类:

public interface ResultSetHandler {

  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  void handleOutputParameters(CallableStatement cs) throws SQLException;

}

处理接口中一共定一个了三个方法分别是:处理普通的结果集,处理游标结果集,以及处理输出参数,可以大致看一下最常用的handleResultSets实现:

public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<Object>();

    int resultSetCount = 0;
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }
    ...以下省略...
  }

while循环遍历结果集,ResultMap就是在XML中定义的结果集,比如定一个的是一个类型Blog,那么会在处理结果的时候,首先创建一个对象,然后给对象属性分配类型处理器TypeHandler,然后根据实际类型调用处理器的getResult方法:

public interface TypeHandler<T> {

  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}

可以看到类型处理器分别用来处理设置参数和从结果集获取参数,也就是分别处理了输入和输出;处理完之后其实就生成了XML中配置的结果集,可能是一个对象,列表,hashMap等;另外一个需要注意的地方就是xxMapper接口中定义的返回值需要保证和XML中配的结果集一致,不然当我们通过代理对象返回结果集的时候会出现类型转换异常;

总结

XML的映射本文分三块来介绍的,分别从Statement块映射MappedStatement,参数映射ParameterMapping,以及结果集是如何通过DefaultResultSetHandler处理的;当然本文只是介绍了一个大概的映射流程,很多细节没有讲到,比如ResultHandler,RowBounds,缓存等;后面每个细节都会单独的写一篇文章来介绍。

示例代码地址

Github

© 著作权归作者所有

ksfzhaohui

ksfzhaohui

粉丝 412
博文 160
码字总数 238834
作品 3
南京
高级程序员
私信 提问
Java面试----2018年MyBatis常见实用面试题整理

Java面试----2018年MyBatis常见实用面试题整理 1、什么是MyBatis? 答:MyBatis是一个可以自定义SQL、存储过程和高级映射的持久层框架。 2、讲下MyBatis的缓存 答:MyBatis的缓存分为一级缓存...

优惠券活动
2018/04/29
0
0
Mybatis常见的面试题总结

什么是Mybatis? 1. mybatis是一个半ORM框架,它内部封装了JDBC,开发时只需要关乎sql语句本身,不需要花费精力去处理驱动,创建连接,创建1statement等繁复过程。2. mybatis可以使用xml或注...

薛小二
05/28
1K
0
精心整理了15道面试官喜欢问的MyBatis面试题

1、什么是 MyBatis? 答:MyBatis 是一个可以自定义 SQL、存储过程和高级映射的持久层框架。 2、讲下 MyBatis 的缓存 答:MyBatis 的缓存分为一级缓存和二级缓存,一级缓存放在 session 里面,...

Java架构师追风
07/24
0
0
10 个 MyBatis 常见面试题

#{}和${}的区别是什么? #{}是预编译处理,${}是字符串替换。 Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值; Mybatis在处理${}时,就是把${}替换成...

蚂蚁-Declan
2018/10/25
105
0
mybatis-Java API

Java API 既然你已经知道如何配置 MyBatis 和创建映射文件,你就已经准备好来提升技能了。 MyBatis 的 Java API 就是你收获你所做的努力的地方。正如你即将看到的,和 JDBC 相比, MyBatis 很大...

狼族盟约元
2016/11/28
32
0

没有更多内容

加载失败,请刷新页面

加载更多

oracle查杀连接会话

由于频繁强制启停tomcat不清理连接等情况时可能导致oracle连接爆满,此时可以使用这个方式清理 --查询select sess.sid,sess.serial#,sess.machine,lo.oracle_username,lo.os_user_name,...

孑竹三秋
12分钟前
5
0
为什么互联网公司天天都在招人?

互联网公司招聘是很重要的环节,互联网公司离职率普遍较高,传统企业离职率较低,所以对于公司招聘是很重要的环节,同样一句“很重要”我看到许多人理解其程度实际上大相径庭。在很多互联网公...

码农突围
13分钟前
4
0
001-open-falcon的单机版安装

open-falcon 每台服务器,都有安装falcon-agent,falcon-agent是一个golang开发的daemon程序,用于自发现的采集单机的各种数据和指标 单机安装 redis mkdir /home/redis && cd /home/redis...

伟大源于勇敢的开始
15分钟前
3
0
人工智能领跑的未来,智能CRM未来可期

现在,几乎每个技术预测故事都以同样的方式开始:人工智能AI正在开辟新的可能性。这种趋势同样发生在CRM领域中。 人工智能正在通过访问和分析来改变CRM。它正在通过添加语音助手、同时改善工...

怡海软件-CRM
16分钟前
3
0
mysql-5.7.28-linux-glibc2.12-x86_64配置(参考)

[client]socket                                            = /data/mysql/var/mysql.sockport                                         ...

Wybaron
20分钟前
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部