Mybatis技术原理与实践——读书笔记(五)

原创
2017/02/17 21:20
阅读数 1.9K

本文是读 杨开振老师的《深入浅出 Mybatis技术原理与实践》第五章所记录的笔记,本文绝对没有推荐书的意思,只是看着记录学习,同时分享自己的观点而已。

 

1、动态SQL重要元素

1)if

2)choose(when、otherwise)

3)trim

4)where

5)set

6)bind

7)foreach

 

 

2、动态SQL解析与参数传递

可以参考文章:

1)http://www.cnblogs.com/fangjian0423/p/mybaits-dynamic-sql-analysis.html

2)http://blog.csdn.net/isea533/article/details/44002219

动态SQL我觉得最值得看的两个类是 BoundSql 和 MappedStatement ,通过他们深入下去,必定收获不少。同时我觉得参数传递同样有学习价值,毕竟在项目中接触最多,所以重点记录一下参数的传递。

参数传递有两种,一种是使用的接口形式,另一种是通过SqlSession调用命名空间,因为我更喜欢使用后者,虽然后者不是面向对象编程,但是后者可以节省更多的代码,提高开发效率,所以这里只说通过SqlSession调用命名空间的方式。

 

主要类

Configuration

SqlSessionTemplate

SqlSessionProxy

SqlSession

DefaultSqlSession

MappedStatement

BaseExecutor CachingExecutor

BoundSql

SqlSource

DynamicSqlSource StaticSqlSource

DynamicContext

SqlSourceBuilder

SqlNode TextSqlNode

BindingTokenParser

GenericTokenParser

ParameterMappingTokenHandler

SimpleExecutor

DefaultParameterHandler

MetaObject

TypeHandler

 

从源码出发

1)Spring 配置文件,通过 SqlSessionFactoryBean 初始化 SqlSessionTemplate,为什么要使用SqlSessionTemplate , 是因为 Spring 可以帮我们管理事务。

 

2)SqlSessionTemplate.selectList(statement, params) ; 无论是selectMap 还是 selectOne 最后执行的都是selectList,无论是新增还是删除,调用的都是 update。我们以 selectList 为入口分析代码,另外系统启动时,Mybatis已经把很多基础数据加载好。

 

3)SqlSessionTemplate 内部采用 sqlSessionProxy 调用,他就是 SqlSession 的代理,代理主要就是帮助 SqlSession 执行时提交事务,遇到异常时回滚事务,以及关闭 SqlSession

this.sqlSessionProxy.<E> selectList(statement, parameter);

 

4)sqlSessionProxy 默认使用 DefaultSqlSession,这个可以在 SqlSessionFactoryBean 那里可以看到如何初始化他的,然后就是通过添加一些默认参数调用执行。

this.selectList(statement, parameter, RowBounds.DEFAULT)

 

5)DefaultSqlSession 的 selectList 主要两步

// 先从系统中获取系统启动时加载的 MappedStatement

MappedStatement ms = configuration.getMappedStatement(statement);

// 然后把前端传过来的参数和MappedStatement执行。

return executor.query(ms,

wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

 

6)wrapCollection(parameter) 告知我们传入集合参数时应该如何在Mapper中调用参数

A:如果参数是List,那么在Mapper文件中使用 list

B:如果参数是 Set,那么在Mapper文件中使用 collection

C:如果参数是Array,那么在Mapper文件中使用 array

private Object wrapCollection(final Object object) {

if (object instanceof Collection) {

StrictMap<Object> map = new StrictMap<Object>();

map.put("collection", object);

if (object instanceof List) {

map.put("list", object);

}

return map;

} else if (object != null && object.getClass().isArray()) {

StrictMap<Object> map = new StrictMap<Object>();

map.put("array", object);

return map;

}

return object;

}

 

7)BaseExecutor 和 CachingExecutor 是执行设置参数和执行SQL的核心, CachingExecutor 是带有二级缓存的,这里只记录SQL的参数设置与执行过程,所以先看看 BaseExecutor 的 query

// 获取boundSql,动态SQL,参数解析就是这

BoundSql boundSql = ms.getBoundSql(parameter);

// Mybatis 一级缓存 Key

CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);

// 执行SQL

return query(ms, parameter, rowBounds, resultHandler, key, boundSql);

 

8)MappedStatement 获取Mapper文件配置的SQL,然后根据参数解析动态SQL

MappedStatement.getBoundSql(Object parameterObject){

// SqlSource 就是从Mapper文件中获取SQL之后根据外部参数解析动态SQL并返回

BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

// 这个返回值非常重要,集合的index表示参数的位置,集合的值就是SQL执行时参数的值

List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

// 省略部分代码

}

 

9)SqlSource 接口定义 Represents the content of a mapped statement read from an XML file or an annotation. It creates the SQL that will be passed to the database out of the input parameter received from the user.

定义的方法只有一个就是:getBoundSql(Object parameterObject)

具体实现类:DynamicSqlSource、StaticSqlSource 等,一般动态SQL就是使用前者

 

10)DynamicSqlSource 如何根据参数解析动态SQL的呢?如下

@Override

public BoundSql getBoundSql(Object parameterObject) {

// 初始化参数,同时自带加上 _parameter,_databaseId

DynamicContext context = new DynamicContext(configuration, parameterObject);

// 动态SQL的元素都会被解析成Java对象,如 <where> 会被解析成 WhereSqlNode,

// 每个对象都会根据传入的参数值解析、拼接SQL

rootSqlNode.apply(context);

// 创建Parser对象

SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);

// 获取传入参数的Class对象

Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();

// 解析SQL中的#{}占位符,根据传入参数,封装需执行SQL的参数

SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());

// 获取封装好参数和执行SQL的BoundSql对象

BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

// 添加新增的参数 _parameter,_databaseId

for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {

boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());

}

return boundSql;

}

 

11)DynamicContext 就是新增了参数 _parameter,_databaseId,其实这也是为什么当我们传入一个参数时,可以通过使用#{_parameter}获取的原因。另外一个是使用一个固定名称的参数 _parameter,在类内部使用需要获取参数时就不需要每次从方法入口传参了,而是直接获取_parameter,这个在 SqlNode 中就可以直接使用了。

 

12)SqlNode.apply(context) 动态SQL解析就是他来处理的,Mapper文件中的动态SQL元素都会被解析为如何下的Java对象,这些对象重写 apply 方法,根据不同的业务实现对 SQL 的拼接,这其实算式策略模式。这里值得注意的是 ${param} 是在如下的 TextSqlNode 解析完成的,从源码可以看出使用了 GenericTokenParser,在解析 #{param} 时同样是用这个类,不过二者的差异是,${param} 中 param的值采用的是OGNL解析,然而 #{param} 中param的值采用的是反射获取。

看看 TextSqlNode 如何解析 ${param}

TextSqlNode——BindingTokenParser

public String handleToken(String content) {

// 初始化 DynamicContext 时,设置的参数,避免从方法传参

Object parameter = context.getBindings().get("_parameter");

if (parameter == null) {

context.getBindings().put("value", null);

} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {

context.getBindings().put("value", parameter);

}

// 使用OGNL赋值value,Ognl.getValue(parseExpression(expression), context, root)

Object value = OgnlCache.getValue(content, context.getBindings());

// issue #274 return "" instead of "null"

String srtValue = (value == null ? "" : String.valueOf(value));

// 这个是空实现,所以使用 ${} 可能会带来SQL注入的问题

checkInjection(srtValue);

return srtValue;

}

 

13)GenericTokenParser 就是 sqlSourceParser 解析 SQL 的核心,这里解析的就是上面说到的#{param},一直以为Mybatis会使用正则表达式,看了代码才知道使用的是字符串索引。

// handler 是当找到参数时如何处理,比如$就用值替换,#就用 ? 替换

ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);

// 初始化 start - end

GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);

// 解析 SQL

String sql = parser.parse(originalSql);

 

14)GenericTokenParser 的 parse 部分源码

// ParameterMappingTokenHandler,每找到一个 #{param} 就调用 handleToken 方法

builder.append(handler.handleToken(expression.toString()));



// 返回 "?",然后添加处理过属性的参数

public String handleToken(String content) {

parameterMappings.add(buildParameterMapping(content));

return "?";

}



// 处理每个参数的属性

// javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName,

// 然后返回 ParameterMapping ,且放在List容器里,保证每个?对应的参数不会错误

ParameterMappingTokenHandler.buildParameterMapping(String content)

 

15)SQL+参数处理好之后,回到第 10 步的

BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

// 看看 BoundSql 的参数

public BoundSql(Configuration configuration,

String sql, List<ParameterMapping> parameterMappings,

Object parameterObject) {

// 动态SQL解析后的SQL,所有的参数用?替代

this.sql = sql;

// 一个List,? 的顺序对应List的索引

this.parameterMappings = parameterMappings;

// 用户输入的参数

this.parameterObject = parameterObject;

this.additionalParameters = new HashMap<String, Object>();

// 调用的是 MetaObject.forObject ,方便使用反射获取对应属性的值

this.metaParameters = configuration.newMetaObject(additionalParameters);

}

 

动态SQL解析+参数处理到这里就结束,接下来的就是如何使用这些参数运行SQL了

 

16)class SimpleExecutor extends BaseExecutor 执行 SQL,类似的还有 BatchExecutor 等

public <E> List<E> doQuery(MappedStatement ms, Object parameter,

RowBounds rowBounds, ResultHandler resultHandler,

BoundSql boundSql) throws SQLException {

Statement stmt = null;

try {

Configuration configuration = ms.getConfiguration();

// 根据配置的属性,使用 JDBC PreparedStatement、CallableStatement Handler

StatementHandler handler = configuration.newStatementHandler(

wrapper, ms, parameter, rowBounds, resultHandler, boundSql);

// 设置 statement 的参数

stmt = prepareStatement(handler, ms.getStatementLog());

// 执行 SQL

return handler.<E>query(stmt, resultHandler);

} finally {

closeStatement(stmt);

}

}



private Statement prepareStatement(StatementHandler handler,

Log statementLog) throws SQLException {

Statement stmt;

// 获取数据库连接

Connection connection = getConnection(statementLog);

// 获取对应的JDBC Statement,同时设置 TimeOut,FetchSize

stmt = handler.prepare(connection, transaction.getTimeout());

// DefaultParameterHandler 设置参数

handler.parameterize(stmt);

return stmt;

}

 

17)DefaultParameterHandler 设置参数

public void setParameters(PreparedStatement ps) {

// 这个参数就是第 14 步所完成的

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();

// issue #448 ask first for additional params

if (boundSql.hasAdditionalParameter(propertyName)) {

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);

}

// 第 14 步所完成的参数封装,包括属性 jdbcType javaType ...

TypeHandler typeHandler = parameterMapping.getTypeHandler();

JdbcType jdbcType = parameterMapping.getJdbcType();

// 如果value和jdbcType同时为null,他也设置了默认的jdbcType,

//所以Mapper文件中可以不写jdbcType

if (value == null && jdbcType == null) {

jdbcType = configuration.getJdbcTypeForNull();

}

try {

// 根据List的index来设置参数的值,jdbcType 就是里面使用策略模式的判断

typeHandler.setParameter(ps, i + 1, value, jdbcType);

} catch (TypeException|SQLException e) {

throw new TypeException("Could not set parameters for mapping: "

+ parameterMapping + ". Cause: " + e, e);

}

}

}

}

}

 

18)接着就是JDBC Statement 执行SQL,处理结果集

statement.execute();

handleResultSets(ps)

 

 

 

3、积累

1)bind 元素的使用,可以减少很多重复的代码

<if test="search != null and search != ''">

<bind name="search01" value="'%'+search+'%'"/>

AND( tt.teacher_name LIKE #{search01}

OR tt.teacher_mobile LIKE #{search01}

)

</if>

 

 

4、OGNL(Object Graph Navigation Library)

ognl 是一种语言规范,mybatis 内部没有引用外部jar而是自写一套实现,就在 org.apache.ibatis.ognl 包下面

可惜并没有提供源码,只能反编译。对于 OGNL也有很多开源的框架比如:camel-ognl、 com.springsource.org.ognl、opensymphony ognl 等

详细:https://commons.apache.org/proper/commons-ognl/language-guide.html

个人觉得 OGNL 在项目的很多地方都可以发挥作用,所以专门学了一下,就使用mybatis的实现

1)语法

符号 表达式 备注
or e1 or e2 逻辑或
and e1 and e2 逻辑与
not not e 逻辑非
eq、== e1 == e2 或 e1 eq e2 是否相等
neq、!= e1 != e2 或 e1 neq e2 是否不等
! !e 逻辑非
lt、< e1 lt e2 、e1 gt e2 小于,大于
lte、<= e1 lte e2 、e1 gte e2 小于等于、大于等于
in e1 in e2 是否属于集合的元素
not in e1 not in e2 是否不属于集合的元素
+、-、*、/、% e1 + e2,e1 * e2,e1/e2,e1 - e2,e1%e2 加减乘除和取余运算
= e1 = e2 把 e2 赋值给 e1
method e.method(args) 调用对象方法
property e.property 对象属性值
[index] e1[ e2 ] 按索引取值,List,数组和Map
@class@method @class@method(args) 调用类的静态方法
@class@field @class@field 调用类的静态字段值

 

2)mybatis 中使用

在xml中配置如下

<if test=" name != null and name != ''"/>

 

mybatis代码解析

A:解析表达式 Ognl.parseExpression(expression);

B:赋值表达式 Ognl.getValue(expression, params);

 

mybatis源码测试

Map<String,Object> params = new HashMap<>();

params.put("name1","123");

params.put("name2",new Integer(123));

params.put("name3",new Date());

params.put("name4","Y");



// 解析字符串为 OGNL 表达式

Object str = Ognl.parseExpression("name1 == '123' and name1 == 123");

System.out.println(str);//(name1 == "123") && (name1 == 123)



// 字符串与数值比较,比较的是字符是否相同,不会区分数据类型

Object obj = Ognl.getValue(str, params );

System.out.println(obj); // true



// 数值与字符串比较,比较的是字符是否相同,不会区分数据类型

obj = Ognl.getValue("name2 == '123' and name2 == 123", params );

System.out.println(obj); // true



// 其他数据类型与字符串比较,出现类型转换错误

obj = Ognl.getValue("name3 != null and name3 != ''", params );

System.out.println(obj); // invalid comparison: java.util.Date and java.lang.String



// 单个字符需要在后面加上 toString(); 否则 Mybatis 会解析为Java的字符

str = Ognl.parseExpression("name4 != null and name4 == 'Y'");

System.out.println(str);//(name4 == null) && (name4 == 'Y')



// 需要改成 name4 != null and name4 == 'Y'.toString()

obj = Ognl.getValue(str, params );

System.out.println(obj); // NumberFormatException: For input string: "Y"

 

 

5、疑问

1)if 元素 test 为什么每次都要判断 name != null and name != '' , 后面这个判断在 GET请求和POST请求都需要判断吗?

答:之所以这样判断是因为 name 可能为空字符的风险,因为如果你使用GET请求,把参数name放在URL上,如 ...?name=&.. 那么在后台解析出来的name就是一个空字符串;

对于POST请求,因为我们都是使用 JSON.stringify(params) 序列化一个对象为字符串,然后把字符串发送给后端,那么如果

A)params.name = null ; 后端收到的是字符串 "null";

B)params.name = undefined ; 后端无法收到这个对象的值,也就是 name 为 null;

C)params.name = "";后端得到的同样是一个空字符串。

其实空字符串一般情况下意义不大,还不如就是一个 null 对象,所以建议前端传递参数的时候尽量不用空字符串,后台也可以过滤处理。

 

2)if 元素 test 如何判断字符、数字,确定是不需要在字符串后面加 toString() 了吗?

P124 以及 P125 说到test的属性的用法,看文中的示例 test = " type = 'Y'" 这个其实是赋值并不是比较,比较应该是这样写 test = " type == 'Y'.toString()" ; P125 页还说新的版本不需要加入 toString(),然而如果没有使用 toString() 会出现如下错误:

NumberFormatException: For input string: "Y"

所以这里的结论是如果是字符串比较确实需要加上 toString()。

public class ExpressionEvaluator {



public boolean evaluateBoolean(String expression, Object parameterObject) {

Object value = OgnlCache.getValue(expression, parameterObject);

if (value instanceof Boolean) {

return (Boolean) value;

}

if (value instanceof Number) {

return !new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);

}

return value != null;

}

 

OgnlCache

Ognl.getValue(parseExpression(expression), context, root);

 

 

 

 

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部