在一章中,我们讨论了前端调用RESTfule服务的时候传递参数的各种方法,本章我们讨论从 Mapper 接口向数据库 SQL命令映射传递参数,以及使用数据映射定义文件实现多表查询的的方法。
11.1 多个查询参数传递
11.1.1 顺序传参法
顺序传参法按照参数的位置顺序来建立对应关系
public UserEntry selectUser(String name, int deptId);
<select id="selectUser" resultMap="UserResultMap">
select * from user
where user_name = #{0} and dept_id = #{1}
</select>
{}里面的数字代表你传入参数的顺序。
这种方法不建议使用,因为再SQL层的表达不直观,顺序调整的时候特别容易出错,而且还不容易检查。
11.1.2 注解传参法
MyBatis为开发者提供了一个注解@Param
,可以通过它去定义映射器的参数名称,使用它可以得到更好的可读性 这个时候需要修改映射文件的代码,此时并不需要给出 parameterType
属性,MyBatis 能够自动探索。这样做使可读性大大提高,使用者也方便了,但是这会带来一个麻烦。如果SQL很复杂,拥有较多的参数,使用起来就很不方便。
public UserEntry selectUser(
@Param("userName") String name,
@Param("deptId")int deptId
);
<select id="selectUser" resultMap="UserResultMap">
select * from user
where user_name = #{userName} and dept_id = #{deptId}
</select>
{}里面的名称对应的是注解@Param括号里面修饰的名称。
这种方法在参数不多的情况还是比较直观的,推荐使用。
除了独立的变量也可以传递数组
TestInfo selectAll(
@Param("classId")String classId,
@Param("userIds")String[] userIds
);
在映射文件中使用foreach遍历来处理数组参数
<select id="selectAll" resultMap="BaseResultMap">
select * from
test_info
where
class_id = #{classId,jdbcType=VARCHAR}
and user_id in
<foreach collection="userIds" item="item" index="index" open="(" separator="," close=")">
#{item}
</foreach>
</select>
11.1.3 Map传参法
这种方法把所有的参数都封装到Map对象中,适合传递多个参数,且参数易变能灵活传递的情况。
public UserEntry selectUser(Map<String, Object> params);
<select id="selectUser" parameterType="java.util.Map" resultMap="UserResultMap">
select * from user
where user_name = #{userName} and dept_id = #{deptId}
</select>
{}里面的名称对应的是Map里面的key名称。
下面是调用的方法:
Map paramMap=new hashMap();
paramMap.put(“userName”,”david”);
paramMap.put(“deptId”,1);
UserEntry user=selectUser(paramMap);
严格来说,Map适用几乎所有场景,但轻易不应该使用。原因有两个:首先,Map是一个键值对应的集合,使用者要通过阅读它的键,才能明了其作用;其次,使用Map不能限定其传递的数据类型,因此业务性质不强,可读性差,见到接口方法不能直接的知道要传的参数是什么,使用者要读懂代码才能知道需要传递什么参数给它,所以不推荐使用这种方式。
11.1.4 Bean传参法
我们还可以构造一个Bean对象封装全部的查询参数:
public UserEntry selectUser(UserEntry user);
<select id="selectUser" parameterType="com.test.UserEntry" resultMap="UserResultMap">
select * from user
where user_name = #{userName} and dept_id = #{deptId}
</select>
{}里面的名称对应的是User类里面的成员属性。
这种方法很直观,但需要建一个实体类,扩展不容易,需要加属性,要看情况谨慎使用。
11.1.5 总结与讨论
- 使用 map 传递参数导致了业务可读性的丧失,导致后续扩展和维护的困难,在实际的应用中要果断废弃这种方式。
- 使用 @Param 注解传递多个参数,受到参数个数的影响。当参数较少时这是最佳的传参方式,它比用 Java Bean 更好,因为它更加直观;当参数较多的时候,多个参数将给调用带来困难,此时不推荐使用它。
- 当参数个数较多时,建议使用 Java Bean 方式。
- 对于使用混合参数的,要明确参数的合理性。
11.2 参数个数不定的查询
很多时候应用系统会给用户提供较多的查询条件进行选择,用户可以选择不同的参数组合。这样在做编程开发时并不能确定查询参数的个数。对于这种情况,我们通常使用“动态SQL“技术来满足要求。
动态 SQL是MyBatis的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。
在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。
11.2.1 条件判断
使用动态 SQL 最常见情景是使用条件判断字句 if 决定是否包含 where
子句的一部分:
<select id="findActiveBlogWithTitleLike" resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
这条语句提供了可选的查找文本功能。如果不传入 title
,那么所有处于 ACTIVE
状态的 BLOG 都会返回;如果传入了 title
参数,那么就会对 title
一列进行模糊查找并返回对应的 BLOG 结果(title
的参数值可以包含查找掩码或通配符字符)。
如果希望通过 title
和 author
两个参数进行可选搜索,只需要加入另一个条件即可。
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select
11.2.2 分支判断
有时候我们不想使用所有的条件,而只是想从多个条件中选择一个使用(即进行分支判断)。针对这种情况,MyBatis 提供了 choose、when、otherwise 这样的分支判断子句 ,它有点像 Java 中的 switch
语句。
还是上面的例子,但是策略变为:传入了 title
就按 title
查找,传入了 author
就按 author
查找的情形。若两者都没有传入,就返回标记为 featured
的 BLOG(这可能是管理员认为,与其返回大量的无意义随机 Blog,还不如返回一些由管理员精选的 Blog)。
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>
11.2.3 trim where set
前面几个例子已经方便地解决了多参数查询的动态 SQL 问题。现在回到之前的 if
示例,这次我们将 state = 'ACTIVE'
设置成动态条件,看看会发生什么。
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
如果没有匹配的条件会怎么样?最终这条 SQL 会变成这样:
SELECT * FROM BLOG WHERE
这会导致查询失败。如果匹配的只是第二个条件又会怎样?这条 SQL 会是这样:
SELECT * FROM BLOG WHERE AND title like ‘someTitle’
这个查询也会失败。这个问题不能简单地用条件元素来解决。这个问题是如此的难以解决,以至于解决过的人不会再想碰到这种问题。
有些人会用如下的方法来解决问题:
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE 1=1
<if test="state != null">
AND state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
这种方法可以解决上面的各种问题,但MyBatis 有一个更加简单且适合大多数场景的解决办法——使用<where>
元素:
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>
where
元素只会在子元素返回任何内容的情况下才插入 WHERE
子句。而且,若子句的开头为 AND
或 OR
,where
元素也会将它们去除。
如果 where
元素与你期望的不太一样,你也可以通过自定义 trim
元素来定制 where
元素的功能。比如,和 where
元素等价的自定义 trim
元素为:
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
prefixOverrides
属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides
属性中指定的内容,并且插入 prefix 属性中指定的内容。
用于动态更新语句的类似解决方案叫做 set。set 元素可以用于动态包含需要更新的列,忽略其它不更新的列。比如:
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>
这个例子中,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。
来看看与 set 元素等价的自定义 trim 元素:
<trim prefix="SET" suffixOverrides=",">
...
</trim>
11.2.4 遍历集合的元素(foreach)
动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候):
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>
foreach
元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符。
提示 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。
11.3 其它操作的讨论
前文讨论的参数传递都以查询(SELECT
)做范例。这些方法同样适用于向其他常见的更新(UPDATE
)、删除(DELETE
)方法传递参数以便确定WHERE
字句的内容。
12.4 多数据表查询映射
数据库中多表之间存在着三种常见关系,分别为多对多、一对多和一对一关系。本节简单讨论使用数据映射定义文件实现多表查询的的方法。
12.4.1 一对一查询
以图书和作者的关系为例,每本书都有一个主要作者,作者都有自己的属性,下面是我们在前一章中定义的类:
package com.longser.union.cloud.data.model;
import lombok.Data;
import java.time.LocalDate;
@Data
public class Book {
private String name;
private Double price;
private Boolean isPublic;
private Author author;
private LocalDate publishDate;
}
package com.longser.union.cloud.data.model;
import lombok.Data;
@Data
public class Author {
private String name;
private Integer age;
}
现在新定义如下的查询接口映射
public interface BookMapper {
Book getBookById(Integer id);
}
BookMapper 中定义了一个查询 Book 的方法,但是我希望查出来 Book 的同时,也能查出来它的 Author,因此定义如下的映射文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.longser.union.cloud.data.mapper.BookMapper">
<resultMap id="BookWithAuthor"
type="com.longser.union.cloud.data.model.Book">
<id column="id" property="id"/>
<result column="name" property="name"/>
<association property="author"
javaType="com.longser.union.cloud.data.model.Author">
<id column="aid" property="id"/>
<result column="aname" property="name"/>
<result column="aage" property="age"/>
</association>
</resultMap>
<select id="getBookById" resultMap="BookWithAuthor">
SELECT b.*,a.`age` AS aage,a.`id` AS aid,a.`name` AS aname FROM book b,author a WHERE b.`aid`=a.`id` AND b.`id`=#{id}
</select>
</mapper>
在这个查询 SQL 中,首先应该做好一对一查询,然后,返回值一定要定义成 resultMap,注意,这里千万不能写错。然后,在 resultMap 中,来定义查询结果的映射关系。
其中,association 节点用来描述一对一的关系。这个节点中的内容,和 resultMap 一样,也是 id,result 等,在这个节点中,我们还可以继续描述一对一。
由于在实际项目中,每次返回的数据类型可能都会有差异,这就需要定义多个 resultMap,而这多个 resultMap 中,又有一部份属性是相同的,所以,我们可以将相同的部分抽出来,做成一个公共的模板,然后被其他 resultMap 继承,优化之后的 mapper 如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.longser.union.cloud.data.mapper.BookMapper">
<resultMap id="BaseResultMap"
type="com.longser.union.cloud.data.model.Book">
<id column="id" property="id"/>
<result column="name" property="name"/>
</resultMap>
<resultMap id="BookWithAuthor"
type="com.longser.union.cloud.data.model.Book"
extends="BaseResultMap">
<association property="author"
javaType="com.longser.union.cloud.data.model.Author">
<id column="aid" property="id"/>
<result column="aname" property="name"/>
<result column="aage" property="age"/>
</association>
</resultMap>
<select id="getBookById" resultMap="BookWithAuthor">
SELECT b.*,a.`age` AS aage,a.`id` AS aid,a.`name` AS aname FROM book b,author a WHERE b.`aid`=a.`id` AND b.`id`=#{id}
</select>
</mapper>
11.4.2 一对多查询
一对多查询也是一个非常典型的使用场景。比如用户和角色的关系,一个用户可以具备多个角色。
首先我们准备三个表:
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`nameZh` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
INSERT INTO `role` VALUES ('1', 'dba', '数据库管理员');
INSERT INTO `role` VALUES ('2', 'admin', '系统管理员');
INSERT INTO `role` VALUES ('3', 'user', '用户');
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`enabled` tinyint(1) DEFAULT NULL,
`locked` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
INSERT INTO `user` VALUES ('1', 'root', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('2', 'admin', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('3', 'sang', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
INSERT INTO `user_role` VALUES ('1', '1', '1');
INSERT INTO `user_role` VALUES ('2', '1', '2');
INSERT INTO `user_role` VALUES ('3', '2', '2');
INSERT INTO `user_role` VALUES ('4', '3', '3');
这三个表中有用户表,角色表以及用户角色关联表,其中用户角色关联表用来描述用户和角色之间的关系,他们是一对多的关系。现在,根据这三个表,创建两个实体类:
@Data
public class User {
private Integer id;
private String username;
private String password;
private List<Role> roles;
}
public class Role {
private Integer id;
private String name;
private String nameZh;
}
接下来,定义一个根据 id 查询用户的方法:
User getUserById(Integer id);
然后,定义该方法的实现:
<resultMap id="UserWithRole" type="com.longser.union.cloud.data.model.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<collection property="roles" ofType="com.longser.union.cloud.data.model.Role">
<id property="id" column="rid"/>
<result property="name" column="rname"/>
<result property="nameZh" column="rnameZH"/>
</collection>
</resultMap>
<select id="getUserById" resultMap="UserWithRole">
SELECT u.*,r.`id` AS rid,r.`name` AS rname,r.`nameZh` AS rnameZh FROM USER u,role r,user_role ur WHERE u.`id`=ur.`uid` AND ur.`rid`=r.`id` AND u.`id`=#{id}
</select>
在 resultMap 中,通过 collection 节点来描述集合的映射关系。在映射时,会自动将一的一方数据集合并,然后将多的一方放到集合中,能实现这一点,靠的就是 id 属性。
11.4.3 补充说明
除了使用映射文件定义以外,也可以使用 @One
注解和 @Many
分别以注解的方式实现一对一和一对多关联查询。由于我们不推荐使用注解的方式执行关联查询,故此处不做深入讨论。
版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。