Mybatis之XML如何映射到方法

编程

前言

上文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如下图所示:

不管是动态还是静态的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

以上是 Mybatis之XML如何映射到方法 的全部内容, 来源链接: utcz.com/z/510587.html

回到顶部