MybatisDynamicSQL原理——更优雅的使⽤Mybatis
Mybatis Dynamic SQL原理
相信使⽤过mybatis的⼈都有⼀些类似的困惑,xml格式的sql模板开发枯燥⽆味,⽽且容易出错,我们该如何减少不必要的模板代码的开发?或许下⽂的Mybatis Dynamic SQL是Mybatis官⽅给出的另⼀个答案。鉴于⽬前⽹上还没有太多介绍Mybatis Dynamic SQL的⽂章,这⾥进⾏⼀些简单的原理阐释。在理解其⼯作原理之后,读者如果对如何更好使⽤Mybatis Dynamic Sql有兴趣可以跳转到另⼀篇⽂章,在这篇⽂章中会给出⼀个最佳实践的⽰例:
⼀、Mybatis Dynamic Sql是什么
⾸先要澄清的是,这⾥的Mybatis Dynamic Sql并不是指Mybatis的动态sql能⼒,⽽是Mybatis官⽅的另⼀个项⽬,这个项⽬并不是为了取代Mybatis,⽽是为了让开发者更⽅便的使⽤mybatis, 也就是说它只是mybatis的⼀个补充。官⽹地址是:
官⽅介绍Mybatis Dynamic Sql原话是:
This library is a framework for generating dynamic SQL statements.
翻译过来:Mybatis Dynamic Sql是⼀个⽤于⽣成动态SQL语句的框架。
重点在于"⽣成动态sql语句",我们知道Mybatis是⼀个半⾃动化的ORM(对象关系映射)框架, 这个半⾃动化体现在需要开发者提供sql模板,mybatis会根据sql模板以及参数来⽣成最终要执⾏的sql。mybatis给开发者提供了 if, choo, when, otherwi, trim, where, t, foreach等标签⽤于更加灵活的组合sql,这就是mybatis的动态sql能⼒。
<!--mybatis动态sql能⼒的体现⽰例:-->
<lect id="findUrById" resultType="ur">
lect*from ur
<where>
<if test="id != null">
id=#{id}
</if>
and deleteFlag=0;
</where>
</lect>
上⾯例⼦:如果传⼊的id 不为空,那么才会SQL才拼接id = #{id}。
mybatis的动态sql能⼒使它区别其它ORM框架,能够更加灵活的⽣成更加复杂的sql。回到Mybatis Dynamic SQL,官⽅提到它是⼀个⽣成动态sql的框架,也就是说它是为了将mybatis的动态sql能⼒发挥到极致才诞⽣的。在Mybatis Dynamic Sql诞⽣之前,我们使⽤mybatis有两种⽅式,⼀是使⽤xml格式的⽂件提供动态sql的模板;另⼀种是使⽤注解来提供动态sql的模板。接下来我们看Mybatsi Dynamic SQL的使⽤⽅式,并和传统⽅式进⾏直观的对⽐。
如果有这样⼀条sql:
lect id, animal_name, body_weight, brain_weight from animal_data where id in(1,5,7)and body_weight is between1.0and3.0order by id desc, body_w eight;
如果使⽤xml的⽅式,需要在xml格式的⽂件中定义出sql模板:
<lect id="getAnimalData"resultMap="AnimalDataResult">
SELECT
id,
animal_name,
body_weight,
brain_weight
FROM
animal_data
WHERE id IN
<foreach item="item"index="index"collection="idList"open="("parator=","clo=")">
#{item}
</foreach>
AND body_weight BETWEEN
#{minWeight} AND #{maxWeight}
ORDER BY
id DESC, body_weight
</lect>
...
// 对应mapper接⼝中的⽅法签名:
List<AnimalData> getAnimalData(@Param("idList") List<Long> idList,
@Param("minWeight") double minWeight,
@Param("maxWeight") double maxWeight);
如果要在注解中使⽤动态sql要更加⿇烦⼀点(⼀种⽅式是使⽤script标签,如下;另⼀种⽅式是Provider类,此处为演⽰):
@Select({
"<script>"+
"SELECT id, animal_name, body_weight, brain_weight from animal_data "+
" WHERE id IN"+
" <foreach item=\"item\" index=\"index\" collection=\"idList\" open=\"(\" parator=\",\" clo=\")\">"+
" #{item} "+
" </foreach>"+
" AND body_weight BETWEEN"+
" #{minWeight} AND #{maxWeight}"+
" ORDER BY id DESC, body_weight"+
"</script>"
})
@ResultMap(value="AnimalDataResult")
List<AnimalData>getAnimalData(@Param("idList") List<Long> idList,
@Param("minWeight")double minWeight,
@Param("maxWeight")double maxWeight);
如果使⽤Mybatis Dynamic SQL:
public List<AnimalData>getAnimalData(List<Long> idList,double minWeight,double maxWeight){
SelectStatementProvider lectStatement =lect(id, animalName, bodyWeight, brainWeight)
.from(animalData)
.where(id,isIn(idList))
.and(bodyWeight,isBetween(minWeight).and(maxWeight))
.
orderBy(id.descending(), bodyWeight)
.build()
.render(RenderingStrategies.MYBATIS3);
List<AnimalData> animals = mapper.lectMany(lectStatement);
}
...
// mapper接⼝中的⽅法签名
@SelectProvider(type=SqlProviderAdapter.class, method="lect")
@ResultMap("AnimalDataResult")
List<AnimalData>lectMany(SelectStatementProvider lectStatement);
归根揭底,Mybatis Dynamic SQL只是mybatis动态sql能⼒的另⼀种使⽤⽅式,它和xml以及注解是平⾏的。
也就是说xml和注解能完成的事情mybatis dynamic sql也应该能够完成,那么我们为什么还要使⽤它呢?
个⼈认为⾄少有以下⼏个优点:
1、所见即所得,代码即是业务
2、天然⽀持limit、offt物理分页
3、天然⽀持批量操作
4、减少模板代码的⼿动开发
5、更符合java程序员的习惯
6、lambda表达式的⽅式,⾮常简洁
⼆、Mybatis Dynamic Sql原理
其实要明⽩Mybatis Dynamic SQL的原理,关键在于明⽩mybatis的原理。先来思考两个问题
1. mybatis的本质是什么?
2. 不管我们使⽤xml、注解、mybatis dynamic sql这三种的那种⽅式,我们都只定义了⼀个接⼝,并没有实现类,mybatis是怎么知道
要去执⾏什么逻辑?
2.1 mybatis的本质是什么
先从第⼀个问题开始,我们都知道mybatis是⼀个ORM框架,ORM即Object Relational Mapping,也就是java的pojo对象与关系表的映射框架。但是在我个⼈看来,mybatis是⼀个屏蔽了底层细节的sql执⾏器,mybatis做的事情就是连接数据库,在数据库上执⾏sql。⾄于查询结果与pojo对象映射,数据类型的对应,ssion的管理、事务的管理等等功能都是执⾏sql⾃然⽽然的附带功能。
既然要执⾏sql,⾸先要⾯对的就是sql从哪⼉来。最为直接的思路就是完整的把sql写出来,但是这样不具有通⽤性,因为⼀点点改动就会造成sql的不适⽤。为了增加复⽤性,进⼀步的想法是采⽤类似于模板引擎的实现思路:模板和参数共同⽣成具体的结果(模板+参数=输出),模板是静态的,参数是动态的,参数改变会使⽣成的结果不同,但是它们都是相似的。
回过头来看,mybatis的三种使⽤⽅式,xml、注解以及mybatis dynamic sql实际上就是模板+参数。这三种使⽤⽅式中,参数都来⾃于⽅法的调⽤,但是模板的定义⽅式不同,xml是在xml格式的mapper
⽂件中定义sql模板,注解是在注解中⽤字符串定义模板,⽽mybatis dynamic sql是使⽤代码的调⽤关系来定义模板。
不管使⽤那种⽅式,mybatis最终都是获取模板,然后⽤参数填充来⽣成最终要去执⾏的sql。
xml和注解都是直接可见的sql模板,那mybatis dynamic sql是怎么⽣成动态sql模板的呢?Mybatis⼜是在什么时候获取到动态sql模板的呢?
其实⽣成模板的具体逻辑都在各⾃的Render类中:
以下是InrtRender类中计算sql模板的源码:
// InrtRenderer类:
public InrtStatementProvider<T>render(){
ValuePhraVisitor visitor =new ValuePhraVisitor(renderingStrategy);
List<Optional<FieldAndValue>> fieldsAndValues = model.mapColumnMappings(m -> m.accept(visitor)) .List());
return DefaultInrtStatementProvider.d())
.withInrtStatement(calculateInrtStatement(fieldsAndValues))
.build();
}
// 计算动态sql模板
private String calculateInrtStatement(List<Optional<FieldAndValue>> fieldsAndValues){ return"inrt into"//$NON-NLS-1$
+spaceBefore(model.table().tableNameAtRuntime())
+spaceBefore(calculateColumnsPhra(fieldsAndValues))
+spaceBefore(calculateValuesPhra(fieldsAndValues));
}
从源码看,在调⽤render()⽅法的时候模板就已经计算出来了,那什么时候调⽤了render()⽅法呢?可以从这个⽅法往上溯源,最终的调⽤轨迹如图所⽰:
上图中,mapper接⼝中default inrt()⽅法是开发者调⽤的⼊⼝,⽽inrt(InrtStatementProvider< ?> provider)这个抽象接⼝是最终交给mybatis去执⾏的⼊⼝,注意蓝⾊线框,这⾥是⼀个lambda表达式的执⾏,是在执⾏完括号中的逻辑之后才调⽤的,也就是说在第六步真正交给mybatis去执⾏的之前动态sql的模板已经⽣成出来了。
Mybatis则是在ProviderSqlSource这个类中使⽤反射调⽤SqlProviderAdapter类中的⽅法去获取之前已经⽣成好的动态SQL模板的: