继上篇分析了getById,我们继续分析其他sql语句(多参数,且参数中指定javaType,jdbcType):
<select id="getById" resultMap="cityResultMap">
SELECT * FROM city where id = #{id}
</select>
<select id="getByNameAndCountryCode" resultType="City">
SELECT * FROM city where Name = #{name} and CountryCode = #{cCode,javaType=string,jdbcType=VARCHAR}
</select>
getByNameAndCountryCode和getById相比,SqlSource的构建几乎一样,我们看看解析传参,先看下这个接口:
City getByNameAndCountryCode(@Param(value = "name") String name, @Param(value = "cCode") String countryCode);
参数用@Param注解标识并指定了value,此时在ParamNameResolve中生成的SortedMap为{0,"name";1,"cCode"}(若不用注解,生成的map(jdk>=1.8且setting中userActuaParamName为true):{0,"arg0";1,"arg1"}或者{0,"0";1,"1"},在接口中写的名字被遗忘)生成的param为:
是个Map对象,还加了两个键值对:param1、param2,再看设置参数:
getById走了typeHandlerRegister.hasTypeHandler分支,而现在parameterObject已经是Map对象,就走到else分支,构建MetaObject,并取出propertName(name、cCode,若没有在接口方法加注解,sql语句就只能用#{arg0}或#{0}了)的值。#{}里面加上javaType和jdbcType的作用就是生成ParameterMapping的typeHandler,若没有添加则生成UnKnowTypeHandler单参数和多参数就是这几处不同。那sqlSession.selectList怎么传参数呢?请自己思考下。
MetaObject分析。。。
继续下一个sql(用$标识参数):
<select id="getByName" resultType="City">
SELECT * FROM city where Name like '%${name}%'
</select>
dao方法:
List<City> getByName(@Param(value = "name") String name);
和之前一样的流程就不说了,这个sql在parseDynamicTags时(构建MixedSqlNode),解析TextSqlNode会判断是否为动态sql,(TextSqlNode判断是否是动态就是判断有无"${}"标识),这里就把TextSqlNode加到MixedSqlNode中了。在使用SqlSource时:
处理sql语句中${name},直接用参数(zhou)替换了name:
假如方法没有@Param注解,OgnlCache.getValue就会报错:
看来对于单个参数而言,如果没有使用@Param注解,参数内容会被直接传递,若有@Param,则参数会被构建为ParamMap对象,key为@Param的value值,value为实际参数值。getBoundSql方法继续往下走,最终还是生成了StaticSqlSource。
动态sql就是在执行某个方法时根据实际参数把DynamicSqlSource转换为StaticSqlSource
静态sql就直接生成了RawSqlSource,内部持有StaticSqlSource
看下生成的boundSql:
后续的执行流程就和getById一样了。
继续下一个sql(返回Map对象):
<select id="getCityAsMapById" resultType="java.util.Map">
SELECT * FROM city where id = #{id}
</select>
这个sql的解析和之前的getById没有区别,本想分析下result的处理,一看还是把我抬走吧,先留着。
继续:
<select id="getListCDATALtId" resultType="City" parameterType="java.lang.Long">
select * from city where id <![CDATA[<=]]> #{maxId}
</select>
<select id="getListBetweenIds" resultType="City">
select * from city where id between #{param1} and #{arg1} order by ${arg2}
</select>
简要说明下,CDATA是xml的语法,期间的内容会被原样输出,下面三个参数的方法并没有使用@Param注解,那参数就是arg0到argN-1了,mybatis会给你加上param1到paramN
下一个sql(返回Map):
<select id="getCityMappedById" resultType="City">
select * from city where id <= #{maxId}
</select>
解析SqlSource和之前没区别,区别是这个方法返回map对象
@MapKey(value = "id") Map<Long, City> getCityMappedById(Long maxId);
当然要在方法上注解下map对象的key是什么,value就不用了,value就是sql中的resultType,如何实现的?MapperMethod的MapperSignature对象会解析接口方法:
调用时调用Session.selectMap:
和selectOne一样,还是调用selectList,并对返回的结果处理成map对象。
下一个sql,insert:
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into city(Name,CountryCode,District,Population) values (#{name}, #{countryCode}, #{district}, #{population})
</insert>
对应的接口方法:
Boolean insert(City city);
返回值为Boolean代表成功与否,也可以为int代表插入数量,参数是City,当然也可以分开写,多参数需要@Param注解哦,insert中使用useGeneratedKey和keyProperty属性用来把自动生成的id赋值给City对象的id字段,同上和之前相同的流程就不分析了(如SqlSource的构建),我们看看不一样的地方:
在构建MappedStatement时会生成Jbdc3KeyGenerator对象而不是NoKeyGenerator。
调用SqlSession的insert方法,内部调用了update方法,CachingExecutor会刷掉待缓存的缓存,然后调用BaseExecutor方法清掉Session缓存
继续看PrepareStatementHandler的update方法:
@Override
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
int rows = ps.getUpdateCount();
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}
直接把自动生成的id赋值给parameterObject(city)的keyProperty属性(id)了
再提一下设置参数处:
和之前的多参数一样,通过MetaObject获取propertyName的值,注意上图else之前的else if语句,如果parameterObject的类型存在于typeHandlerRegistry,value就是parameterObject,由于typeHandlerRegistry包含了基本类型,那么参数类型如果是非基本类型,或者是自定义的java类,就通过MetaObject来获取对应的propertypName的值。
最后看下返回值,看下MapperMethod这个方法就很清楚了:
private Object rowCountResult(int rowCount) {
final Object result;
if (method.returnsVoid()) {
result = null;
} else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
result = rowCount;
} else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
result = (long)rowCount;
} else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
result = rowCount > 0;
} else {
throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
}
return result;
}
update和delete语句是insert的子集,就不再多说了,下面看下动态sql,什么是动态sql?先看下配置:
<select id="getCityIf" resultType="City">
select * from city where CountryCode=#{countryCode}
<if test="district != null">
and District like #{district}
</if>
</select>
动态sql就是说这个sql语句要根据你的参数生成
比如上面的getCityIf,如果district为空:select * from ... #{countryCode}
若不为空:select ... #{countryCode} and District like #{district}
之前已经有过动态sql了,就是当sql语句包含${}时也算是一种动态sql
最终动态sql转换为静态sql,即DynamicSqlSource -> StaticSqlSource
看下代码:
如果是ELEMENT_NODE,就通过handler处理并将生成的SqlNode放入contents:
上面是IfHandler,又递归调用了parseDynamicTags,最终生成的List<SqlNode>:
DynamicSqlSource的MixedSqlNode就像公司的部门一样,是一种组合关系:
Content
StaticTextSqlNode
IfSqlNode
-- StaticTextSqlNode
StaticTextSqlNode
继续看如何生成BoundSql,先调用rootSqlNode的apply方法生成sql语句,直接看IfSqlNode的apply方法:
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
满足条件则会调用contents.apply,contents是IfSqlNode内部的SqlNode,看下最终生成的BoundSql:
BoundSql中有sql、sql?的参数信息,实际参数,动态sql就这些,再看几个熟悉下,下一个bind:
<select id="getCityIfLike" resultType="City">
<bind name="likeDistrict" value="'%' + district + '%'" />
select * from city where CountryCode=#{countryCode}
<if test="district != null">
and District like #{likeDistrict}
</if>
</select>
bind的作用就是对参数做一些额外的格式化,看下bindHandler在hanlerNode时生成的VarDeclSqlNode,apply方法直接把name和value放到context的bindings中:
还记得MappedStatement.getBoundSql最后一步吗?把context的binds设置到BoundSql的additionalParameter中,而在DefaultParameterHandler的setParameters方法中,取值会先去boundSql的additionalParameter查一下,这里发现有likeDistrict,就直接用它的value了。
下一个choose:
<select id="getCityChoose" resultType="City" parameterType="City">
select * from city where CountryCode=#{countryCode}
<choose>
<when test="name != null">
and Name = #{name}
</when>
<when test="district != null">
and District = #{district}
</when>
<otherwise>
and Name = 'Peking'
</otherwise>
</choose>
</select>
看下类定义就全明白了:
public class ChooseSqlNode implements SqlNode {
private final SqlNode defaultSqlNode;
private final List<SqlNode> ifSqlNodes;
public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
this.ifSqlNodes = ifSqlNodes;
this.defaultSqlNode = defaultSqlNode;
}
@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : ifSqlNodes) {
if (sqlNode.apply(context)) {
return true;
}
}
if (defaultSqlNode != null) {
defaultSqlNode.apply(context);
return true;
}
return false;
}
}
下三个<where><trim><set>:
<select id="getCityWhere" resultType="City" parameterType="City">
select * from city
<where>
<if test="countryCode != null">CountryCode=#{countryCode}</if>
<if test="name != null">and Name=#{name}</if>
<if test="district != null">and District=#{district}</if>
</where>
</select>
<select id="getCityTrim" resultType="City" parameterType="City">
select * from city
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="countryCode != null">CountryCode=#{countryCode}</if>
<if test="name != null">and Name=#{name}</if>
<if test="district != null">and District=#{district}</if>
</trim>
</select>
<update id="updateSet" parameterType="City">
update city
<set>
<if test="name != null">Name=#{name},</if>
<if test="countryCode != null">CountryCode=#{countryCode},</if>
<if test="district != null">District=#{district},</if>
<if test="population != null">Population=#{population}</if>
</set>
where ID = #{id}
</update>
<update id="updateTrim" parameterType="City">
update city
<trim prefix="SET" suffixOverrides=",">
<if test="name != null">Name=#{name},</if>
<if test="countryCode != null">CountryCode=#{countryCode},</if>
<if test="district != null">District=#{district},</if>
<if test="population != null">Population=#{population}</if>
</trim>
where ID = #{id}
</update>
WhereSqlNode和SetSqlNode基础TrimSqlNode,使用where、set标签需要给sql加上where、set,where需要去掉紧随where的and、or,set需要去掉最后那个逗号
WhereSqlNode:
public class WhereSqlNode extends TrimSqlNode {
private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
public WhereSqlNode(Configuration configuration, SqlNode contents) {
super(configuration, contents, "WHERE", prefixList, null, null);
}
}
告诉TrimSqlNode:把WHERE加到sql语句最前面,且紧随where的prefixList都给我去掉
SetSqlNode:
public class SetSqlNode extends TrimSqlNode {
private static List<String> suffixList = Arrays.asList(",");
public SetSqlNode(Configuration configuration,SqlNode contents) {
super(configuration, contents, "SET", null, null, suffixList);
}
}
告诉TrimSqlNode,把SET加到sql语句最前面,sql最后如果是suffixList,去掉他们
TrimSqlNode老老实实干活喽:
最后一个foreach:
<!-- index是集合索引,item为该索引的元素 -->
<select id="getCityForEachList" resultType="City" parameterType="java.util.List">
select * from city where ID in
<foreach item="item" index="index" collection="list" open="(" separator="," close=")">
#{item},#{index}
</foreach>
</select>
<!-- index是集合索引,item为该索引的元素 -->
<select id="getCityForEachArray" resultType="City">
select * from city where ID in
<foreach item="item" index="index" collection="array" open="(" separator="," close=")">
#{item},#{index}
</foreach>
</select>
<!-- index是集合索引,item为该索引的元素 -->
<select id="getCityForEachSet" resultType="City" parameterType="java.util.Set">
select * from city where ID in
<foreach item="item" index="index" collection="collection" open="(" separator="," close=")">
#{item},#{index}
</foreach>
</select>
<select id="getCityForEachMap" resultType="City">
select * from city where ID in
<foreach item="item" index="index" collection="ids" open="(" separator="," close=")">
#{item},#{index}
</foreach>
</select>
foreach常用语批量insert,或者select ... in (arg1,arg2,arg3....),参数可以是Array、List、Set、Map,若参数为map,需要@Params注解喽,foreach标签中item代表集合中的对象,index代表集合的索引号,collection代表你传入的是collection或者array(这一点我不太明白),open和close会在foreach之前、之后加上字符(一般为左右括号),seperate为每个迭代间插入字符(一般为逗号了),传参调用后,比如传入的是ArrayList,DefaultSqlSession会wrapCollection:
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;
}
若是collection(array,set)则key为collection,若是list对象,同样添加key为list的Entry,若是数组则key为array,若是map,好吧,直接return,我们先看下mixedSqlNode.apply:
继续到ForEachSqlNode.apply方法:
@Override
public boolean apply(DynamicContext context) {
Map<String, Object> bindings = context.getBindings();
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
applyOpen(context);
int i = 0;
for (Object o : iterable) {
DynamicContext oldContext = context;
if (first || separator == null) {
context = new PrefixedContext(context, "");
} else {
context = new PrefixedContext(context, separator);
}
int uniqueNumber = context.getUniqueNumber();
// Issue #709
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
applyIndex(context, i, uniqueNumber);
applyItem(context, o, uniqueNumber);
}
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
applyClose(context);
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}
根据collectionExpression(list or array or 自定义的ids)来获取iterable对象,collection属性的作用应该就是这个吧
后续方法:
1、先拿到迭代对象
2、应用open符号,如左括号
3、然后就开始遍历,
3.1、先保存一个oldContext
3.2、若第一次遍历,或分隔符为空,new 一个PrefixContext,且构造参数为空字符串,否则构造函数为分隔符
3.3、bind索引及集合内容,key为item、index,且构造了另外一个key:__frch_index_0,__frch_item_0,value为下标索引及该索引对应的值
3.4、new FilteredDynamicContext,参数为context(这里是PrefixContext了),调用sqlNode.apply方法,我们的foreachSqlNode的MixedSqlNode是这样:
FilteredDynamicContext的appendSql就是把#{item}及#{index}替换为#{__frch__item_0} #{__frch_index_0},然后调用了delegate.appendSql,就是PrefixContext的appendSql
@Override
public void appendSql(String sql) {
if (!prefixApplied && sql != null && sql.trim().length() > 0) {
delegate.appendSql(prefix);
prefixApplied = true;
}
delegate.appendSql(sql);
}
分隔符就是这样加进去的
3.5、刚才的是3.4的东东哦,有点多,下一步就是把first设置为false,把oldContext赋值回context(原来的context被装修了两下,想通过装饰后的context拿到装饰前的context也是可以的)
4、应用close符号,去掉bing的item和index,最后看下ForEachSqlNode的apply方法生成的context:
然后及通过SqlSourceParse用?替掉#{}里的内容,生成StaticSqlSource。foreach就是这些了,不过刚才用的是集合是基本类型,我们看下批量插入:
<insert id="insertBatch" useGeneratedKeys="true" keyProperty="id">
insert into city (Name,CountryCode,District,Population) values
<foreach item="city" index="index" collection="collection" separator=",">
(#{city.name}, #{city.countryCode}, #{city.district}, #{index})
</foreach>
</insert>
使用了city.name,生成的bind还是一样key:__frch_city_0 ... n, value: city对象,sql的#{city.name}变为#{__frch_city_0.name},
如何给参数赋值呢?下图你debug到的话应该很清楚了:
调用之前是这里:
public Object getValue(String name) {
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
return null;
} else {
return metaValue.getValue(prop.getChildren());
}
} else {
return objectWrapper.get(prop);
}
}
PropertyTokenizer很有意思,它会把点后面的当做child,之前的当name,还有强大的MetaObject,所有对象通吃!