在一章中,我们讨论了前端调用RESTfule服务的时候传递参数的各种方法,本章我们讨论从 Mapper 接口向数据库 SQL命令映射传递参数,以及使用数据映射定义文件实现多表查询的的方法。

11.1 多个查询参数传递

11.1.1 顺序传参法

顺序传参法按照参数的位置顺序来建立对应关系

  1. public UserEntry selectUser(String name, int deptId);
  1. <select id="selectUser" resultMap="UserResultMap">
  2. select * from user
  3. where user_name = #{0} and dept_id = #{1}
  4. </select>

{}里面的数字代表你传入参数的顺序。

这种方法不建议使用,因为再SQL层的表达不直观,顺序调整的时候特别容易出错,而且还不容易检查。

11.1.2 注解传参法

MyBatis为开发者提供了一个注解@Param,可以通过它去定义映射器的参数名称,使用它可以得到更好的可读性 这个时候需要修改映射文件的代码,此时并不需要给出 parameterType 属性,MyBatis 能够自动探索。这样做使可读性大大提高,使用者也方便了,但是这会带来一个麻烦。如果SQL很复杂,拥有较多的参数,使用起来就很不方便。

  1. public UserEntry selectUser(
  2. @Param("userName") String name,
  3. @Param("deptId")int deptId
  4. );
  1. <select id="selectUser" resultMap="UserResultMap">
  2. select * from user
  3. where user_name = #{userName} and dept_id = #{deptId}
  4. </select>

{}里面的名称对应的是注解@Param括号里面修饰的名称。

这种方法在参数不多的情况还是比较直观的,推荐使用

除了独立的变量也可以传递数组

  1. TestInfo selectAll(
  2. @Param("classId")String classId,
  3. @Param("userIds")String[] userIds
  4. );

在映射文件中使用foreach遍历来处理数组参数

  1. <select id="selectAll" resultMap="BaseResultMap">
  2. select * from
  3. test_info
  4. where
  5. class_id = #{classId,jdbcType=VARCHAR}
  6. and user_id in
  7. <foreach collection="userIds" item="item" index="index" open="(" separator="," close=")">
  8. #{item}
  9. </foreach>
  10. </select>

11.1.3 Map传参法

这种方法把所有的参数都封装到Map对象中,适合传递多个参数,且参数易变能灵活传递的情况。

  1. public UserEntry selectUser(Map<String, Object> params);
  1. <select id="selectUser" parameterType="java.util.Map" resultMap="UserResultMap">
  2. select * from user
  3. where user_name = #{userName} and dept_id = #{deptId}
  4. </select>

{}里面的名称对应的是Map里面的key名称。

下面是调用的方法:

  1. Map paramMap=new hashMap();
  2. paramMap.put(“userName”,”david”);
  3. paramMap.put(“deptId”,1);
  4. UserEntry user=selectUser(paramMap);

严格来说,Map适用几乎所有场景,但轻易不应该使用。原因有两个:首先,Map是一个键值对应的集合,使用者要通过阅读它的键,才能明了其作用;其次,使用Map不能限定其传递的数据类型,因此业务性质不强,可读性差,见到接口方法不能直接的知道要传的参数是什么,使用者要读懂代码才能知道需要传递什么参数给它,所以不推荐使用这种方式。

11.1.4 Bean传参法

我们还可以构造一个Bean对象封装全部的查询参数:

  1. public UserEntry selectUser(UserEntry user);
  1. <select id="selectUser" parameterType="com.test.UserEntry" resultMap="UserResultMap">
  2. select * from user
  3. where user_name = #{userName} and dept_id = #{deptId}
  4. </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 子句的一部分:

  1. <select id="findActiveBlogWithTitleLike" resultType="Blog">
  2. SELECT * FROM BLOG
  3. WHERE state = ‘ACTIVE’
  4. <if test="title != null">
  5. AND title like #{title}
  6. </if>
  7. </select>

这条语句提供了可选的查找文本功能。如果不传入 title,那么所有处于 ACTIVE 状态的 BLOG 都会返回;如果传入了 title 参数,那么就会对 title 一列进行模糊查找并返回对应的 BLOG 结果(title 的参数值可以包含查找掩码或通配符字符)。

如果希望通过 titleauthor 两个参数进行可选搜索,只需要加入另一个条件即可。

  1. <select id="findActiveBlogLike"
  2. resultType="Blog">
  3. SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  4. <if test="title != null">
  5. AND title like #{title}
  6. </if>
  7. <if test="author != null and author.name != null">
  8. AND author_name like #{author.name}
  9. </if>
  10. </select

11.2.2 分支判断

有时候我们不想使用所有的条件,而只是想从多个条件中选择一个使用(即进行分支判断)。针对这种情况,MyBatis 提供了 choosewhenotherwise 这样的分支判断子句 ,它有点像 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子句。而且,若子句的开头为 ANDORwhere 元素也会将它们去除。

如果 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 分别以注解的方式实现一对一和一对多关联查询。由于我们不推荐使用注解的方式执行关联查询,故此处不做深入讨论。

版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。