Mybatis动态SQL

2020-09-23 14:16:00 浏览数 (1)

constructor与discriminator鉴别器

通过修改对象属性的方式,可以满足大多数的数据传输对象(Data Transfer Object,DTO)以及绝大部分领域模型的要求。 但有些情况下你想使用不可变类。 通常来说,很少或基本不变的、包含引用或查询数 据的表,很适合使用不可变类。 构造方法注入允许你在初始化时 为类设置属性的值,而不用暴露出公有方法。MyBatis 也支持私有属性和私有 JavaBeans 属 性来达到这个目的,但有一些人更青睐于构造方法注入。constructor 元素就是为此而生的。

我们来看看Student类中的构造器:

代码语言:javascript复制
package org.zero01.pojo;

public class Student {

    private int sid;
    private String sname;
    private int age;
    private String sex;
    private String address;

    public Student(Integer sid, String sname, Integer age, String sex, String address) {
        this.sid = sid;
        this.sname = sname;
        this.age = age;
        this.sex = sex;
        this.address = address;
    }
    ... getter 略 ...
}

注:类中可以写或不写getter setter方法,也可以只写 getter 方法或 setter 方法。

为了将结果注入构造方法,MyBatis需要通过某种方式定位相应的构造方法。 在下面的例子中,MyBatis搜索一个声明了五个形参的的构造方法,以javaType属性中指定的值来进行构造方法参数的排列顺序:

代码语言:javascript复制
<?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="org.zero01.dao.StudentMapper">
    <resultMap id="stuMap" type="Student">
        <constructor>
            <idArg column="sid" javaType="Integer"/>
            <arg  column="sname" javaType="String"/>
            <arg column="age" javaType="Integer"/>
            <arg column="sex" javaType="String"/>
            <arg column="address" javaType="String"/>
        </constructor>
    </resultMap>
    <select id="selectAll" resultMap="stuMap">
      select * from student
    </select>
</mapper>

编写一个简单的测试用例,测试能否正常往构造器中注入数据:

代码语言:javascript复制
package org.zero01.test;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.zero01.dao.StudentMapper;
import org.zero01.pojo.Student;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class TestStudent {

    private SqlSession sqlSession;
    private StudentMapper studentMapper;

    @Before
    public void startTest() throws IOException {
        String confPath = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(confPath);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        sqlSession = sqlSessionFactory.openSession();
        studentMapper = sqlSession.getMapper(StudentMapper.class);
    }

    @After
    public void endTest() {
        if (sqlSession != null) {
            sqlSession.close();
        }
    }

    @Test
    public void testSelectAll() {
        List<Student> studentList = studentMapper.selectAll();
        for (Student student : studentList) {
            System.out.println(new JSONObject(student));
        }
    }
}

控制台输出结果:

代码语言:javascript复制
{"address":"湖北","sname":"One","sex":"男","age":15,"sid":1}
{"address":"杭州","sname":"Jon","sex":"男","age":16,"sid":2}

当你在处理一个带有多个形参的构造方法时,很容易在保证 arg 元素的正确顺序上出错。 从版本 3.4.3 开始,可以在指定参数名称的前提下,以任意顺序编写 arg 元素。 为了通过名称来引用构造方法参数,你可以添加 @Param 注解,或者使用 '-parameters' 编译选项并启用 useActualParamName 选项(默认开启)来编译项目。 下面的例子对于同一个构造方法依然是有效的,尽管第三和第四个形参顺序与构造方法中声明的顺序不匹配:

代码语言:javascript复制
<?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="org.zero01.dao.StudentMapper">
    <resultMap id="stuMap" type="Student">
        <constructor>
            <idArg column="sid" javaType="Integer" name="sid"/>
            <arg column="sname" javaType="String" name="sname"/>
            <arg column="sex" javaType="String" name="sex"/>
            <arg column="age" javaType="Integer" name="age"/>
            <arg column="address" javaType="String" name="address"/>
        </constructor>
    </resultMap>
    <select id="selectAll" resultMap="stuMap">
      select * from student
    </select>
</mapper>

如果类中存在名称和类型相同的属性,那么可以省略 javaType 。

剩余的属性和规则和普通的 id 和 result 元素是一样的。

属性

描述

column

数据库中的列名,或者是列的别名。一般情况下,这和 传递给 resultSet.getString(columnName) 方法的参数一样。

javaType

一个 Java 类的完全限定名,或一个类型别名(参考上面内建类型别名的列表)。 如果你映射到一个 JavaBean,MyBatis 通常可以断定类型。然而,如 果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证期望的 行为。

jdbcType

JDBC 类型,所支持的 JDBC 类型参见这个表格之前的“支持的 JDBC 类型”。 只需要在可能执行插入、更新和删除的允许空值的列上指定 JDBC 类型。这是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 编程,你需要对可能为 null 的值指定这个类型。

typeHandler

我们在前面讨论过的默认类型处理器。使用这个属性,你可以覆盖默 认的类型处理器。这个属性值是一个类型处理 器实现类的完全限定名,或者是类型别名。

select

用于加载复杂类型属性的映射语句的 ID,它会从 column 属性中指定的列检索数据,作为参数传递给此 select 语句。具体请参考 Association 标签。

resultMap

ResultMap 的 ID,可以将嵌套的结果集映射到一个合适的对象树中,功能和 select 属性相似,它可以实现将多表连接操作的结果映射成一个单一的ResultSet。这样的ResultSet将会将包含重复或部分数据重复的结果集正确的映射到嵌套的对象树中。为了实现它, MyBatis允许你 “串联” ResultMap,以便解决嵌套结果集的问题。想了解更多内容,请参考下面的Association元素。

select

构造方法形参的名字。从3.4.3版本开始,通过指定具体的名字,你可以以任意顺序写入arg元素。参看上面的解释。


discriminator鉴别器

有时一个单独的数据库查询也许返回很多不同 (但是希望有些关联) 数据类型的结果集。 鉴别器元素就是被设计来处理这个情况的, 还有包括类的继承层次结构。 鉴别器非常容易理解,因为它的表现很像 Java 语言中的 switch 语句。

我们先新增两个Student的子类,MaleStudent类:

代码语言:javascript复制
package org.zero01.pojo;

public class MaleStudent extends Student{
}

FemaleStudent类:

代码语言:javascript复制
package org.zero01.pojo;

public class FemaleStudent extends Student {
}

定义鉴别器指定了 column 和 javaType 属性。 column 是 MyBatis 查找比较值的地方。 JavaType 是需要被用来保证等价测试的合适类型(尽管字符串在很多情形下都会有用)。比如:

代码语言:javascript复制
<?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="org.zero01.dao.StudentMapper">
    <resultMap id="stuMap" type="Student">
        <id property="sid" column="sid"/>
        <result property="sname" column="sname"/>
        <result property="age" column="age"/>
        <result property="sex" column="sex"/>
        <result property="address" column="address"/>
        <!-- column:指定判断的列名,javaType:column值对应的java类型 -->
        <discriminator javaType="String" column="sex">
            <!-- sex列的值为 “男” 时,就把结果集包装成MaleStudent对象 -->
            <case value="男" resultType="MaleStudent"/>
            <!-- sex列的值为 “女” 时,就把结果集包装成FemaleStudent对象 -->
            <case value="女" resultType="FemaleStudent"/>
        </discriminator>
    </resultMap>
    <select id="selectAll" resultMap="stuMap">
      select * from student
    </select>
</mapper>

修改测试方法如下:

代码语言:javascript复制
@Test
public void testSelectAll() {
    List<Student> studentList = studentMapper.selectAll();
    for (Student student : studentList) {
        System.out.println(new JSONObject(student));
        System.out.println(student.getClass().getName()   "n");
    }
}

运行成功后,控制台输出结果如下:

代码语言:javascript复制
{"address":"湖北","sname":"One","sex":"男","age":15,"sid":1}
org.zero01.pojo.MaleStudent

{"address":"杭州","sname":"Jon","sex":"男","age":16,"sid":2}
org.zero01.pojo.MaleStudent

{"address":"湖南","sname":"Max","sex":"女","age":18,"sid":3}
org.zero01.pojo.FemaleStudent

{"address":"广州","sname":"Alen","sex":"女","age":19,"sid":4}
org.zero01.pojo.FemaleStudent

从控制台输出结果中,可以看到,我们成功通过鉴别器,将不同的性别的结果集数据封装到了不同的子类中。在case元素中,还可以使用resultMap属性引用某个结果集的映射器,以及可以直接在case元素中使用result等元素进行结果集的封装。例如:

代码语言:javascript复制
<resultMap id="vehicleResult" type="Vehicle">
  <id property="id" column="id" />
  <result property="vin" column="vin"/>
  <result property="year" column="year"/>
  <result property="make" column="make"/>
  <result property="model" column="model"/>
  <result property="color" column="color"/>
  <discriminator javaType="int" column="vehicle_type">
    <case value="1" resultType="carResult">
      <result property="doorCount" column="door_count" />
    </case>
    <case value="2" resultType="truckResult">
      <result property="boxSize" column="box_size" />
      <result property="extendedCab" column="extended_cab" />
    </case>
    <case value="3" resultType="vanResult">
      <result property="powerSlidingDoor" column="power_sliding_door" />
    </case>
    <case value="4" resultType="suvResult">
      <result property="allWheelDrive" column="all_wheel_drive" />
    </case>
  </discriminator>
</resultMap>

Mybatis动态SQL

MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其它类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。虽然在以前使用动态 SQL 并非一件易事,但正是 MyBatis 提供了可以被用在任意 SQL 映射语句中的强大的动态 SQL 语言得以改进这种情形。

动态 SQL 元素和 JSTL 或基于类似 XML 的文本处理器相似。在 MyBatis 之前的版本中,有很多元素需要花时间了解。MyBatis 3 大大精简了元素种类,现在只需学习原来一半的元素便可。MyBatis 采用功能强大的基于 OGNL 的表达式来淘汰其它大部分元素。MyBatis 3 只需要学习以下元素即可:

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

(1) if 元素:

if 元素通常要做的事情是根据条件动态生成 where 子句的一部分。比如:

代码语言:javascript复制
<select id="findStudentById" resultType="Student" parameterType="Student">
    select * from student
    where sid = #{sid}
    <if test="sname != null">
      and sname = #{sname}
    </if>
</select>

这条语句提供了一种可选的查找文本功能。如果没有传入“sname”,那么只会查询sid相匹配的记录;反之若传入了“sname”,那么就会增多一个“sname”字段的匹配条件(细心的读者可能会发现,“title”参数值是可以包含一些掩码或通配符的)。

和if语句一样,除了可以使用!=、<=、>=等运算符外,if元素也可以使用 and、or、not之类的运算符,如下:

代码语言:javascript复制
<select id="findStudentById" resultType="Student" parameterType="Student">
    select * from student
    where sid = #{sid}
    <if test="sname != null and sex !=null ">
      and sname = #{sname}
    </if>
</select>

(2)choose, when, otherwise元素:

有时候我们需要使用分支条件判断,针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。例如:

代码语言:javascript复制
<select id="findStudentById" resultType="Student" parameterType="Student">
    select * from student
    where sid = #{sid}
    <choose>
        <when test="sname != null">
            and sname = #{sname}
        </when>
        <when test="sex != null">
            and sex = #{sex}
        </when>
        <when test="address != null">
            and address = #{address}
        </when>
        <otherwise>
            and age > 0
        </otherwise>
    </choose>
</select>

(2)trim, where, set元素:

前面几个例子已经合宜地解决了一个臭名昭著的动态 SQL 问题。现在回到“if”示例,这次我们将sid = #{sid}也设置成动态的条件,看看会发生什么:

代码语言:javascript复制
<select id="findStudentById" resultType="Student" parameterType="Student">
    select * from student
    where
    <if test="sid != null">
        sid = #{sid}
    </if>
    <if test="sname != null and sex !=null ">
        and sname = #{sname}
    </if>
</select>

如果这些条件没有一个能匹配上会发生什么?最终这条 SQL 会变成这样:

代码语言:javascript复制
SELECT * FROM student
WHERE

这会导致查询失败。如果仅仅第二个条件匹配又会怎样?这条 SQL 最终会是这样:

代码语言:javascript复制
SELECT * FROM student
WHERE and sname = #{sname}

显而易见,这个查询也会失败。这个问题不能简单地用条件句式来解决,如果你也曾经被迫这样写过,那么你很可能从此以后都不会再写出这种语句了。

好在 MyBatis 中有一个简单的处理,这在 90% 的情况下都会有用。而在不能使用的地方,你可以自定义处理方式来令其正常工作。我们只需要把sql语句中 where 替换成 MyBatis 中的 where元素即可,如下:

代码语言:javascript复制
<select id="findStudentById" resultType="Student" parameterType="Student">
    select * from student
    <where>
        <if test="sid != null">
            sid = #{sid}
        </if>
        <if test="sname != null and sex !=null ">
            and sname = #{sname}
        </if>
    </where>
</select>

where 元素只会在至少有一个子元素的条件返回 SQL 子句的情况下才去插入“WHERE”子句,如果没有 SQL 子句的返回则不会插入“WHERE”子句。而且,若语句的开头为“AND”或“OR”,where 元素也会将它们去除。

如果 where 元素没有按正常套路出牌,我们可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:

代码语言:javascript复制
<select id="findStudentById" resultType="Student" parameterType="Student">
    select * from student
    <trim prefix="where" prefixOverrides="AND | OR ">
        <if test="sid != null">
            sid = #{sid}
        </if>
        <if test="sname != null and sex !=null ">
            and sname = #{sname}
        </if>
    </trim>
</select>

prefixOverrides 属性可以忽略通过管道分隔的文本序列(注意此例中的空格也是必要的)。它的作用是移除所有指定在 prefixOverrides 属性中的内容,并且插入 prefix 属性中指定的内容。但是 prefixOverrides 属性移除的是文本中前面的内容,例如有一段文本内容如下:

代码语言:javascript复制
AND sname = #{sname} AND 

然后指定 prefixOverrides 属性的值为 “AND” ,由于 prefixOverrides 属性只会移除前面的AND,所以移除后的文件内容如下:

代码语言:javascript复制
sname = #{sname} AND 

与之相对应的属性是 suffixOverrides ,它的作用是移除所有指定在 suffixOverrides 属性中的内容,而它移除的是文本后面的内容。例如,在执行update更新语句的时候,我们也希望至少有一个子元素的条件返回 SQL 子句的情况下才去插入 “SET” 子句,而且,若语句的结尾为 “ , ” 时需要将它们去除。使用trim元素实现如下:

代码语言:javascript复制
<update id="updateStudent">
    update student
    <trim prefix="set" suffixOverrides=",">
        <if test="sname != null ">sname = #{sname},</if>
        <if test="age != 0 ">age = #{age},</if>
        <if test="sex != null ">sex = #{sex},</if>
        <if test="address != null ">address = #{address}</if>
    </trim>
    where sid = #{sid}
</update>

suffixOverrides属性会把语句末尾中的逗号删除,同样的,prefix属性会将指定的内容插入到语句的开头。与prefix属性相对应的是suffix属性,该属性会将指定的内容插入到语句的末尾。

以上我们使用trim元素实现了动态的更新语句,这种方式还有些麻烦,其实还可以更简单,使用set元素即可,如下:

代码语言:javascript复制
<update id="updateStudent">
    update student
    <set>
        <if test="sname != null ">sname = #{sname},</if>
        <if test="age != 0 ">age = #{age},</if>
        <if test="sex != null ">sex = #{sex},</if>
        <if test="address != null ">address = #{address}</if>
    <set>
    where sid = #{sid}
</update>

set 元素会动态前置 SET 关键字,同时也会删掉无关的逗号,因为用了条件语句之后很可能就会在生成的 SQL 语句的后面留下这些逗号。(因为用的是“if”元素,若最后一个“if”没有匹配上而前面的匹配上,SQL 语句的最后就会有一个逗号遗留)


(4)foreach元素:

动态 SQL 的另外一个常用的操作需求是对一个集合进行遍历,通常是在构建 IN 条件语句的时候。比如:

代码语言:javascript复制
<select id="selectStudentIn" resultMap="stuMap">
  select * from student where sid in
  <foreach collection="list" index="index" item="item" open="(" separator="," close=")">
      #{item}
  </foreach>
</select>

foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及在迭代结果之间放置分隔符。这个元素是很智能的,因此它不会偶然地附加多余的分隔符:

  • collection属性指定接收的是什么集合
  • open属性指定开头的符号
  • close属性指定结尾的符号
  • separator属性指定迭代结果之间的分隔符
  • item属性存储每次迭代的集合元素(map集合时为值)
  • index属性存储每次迭代的索引(map集合时为键)

测试代码如下:

代码语言:javascript复制
@Test
public void testSelectPostIn() {
    List<Integer> idList = new ArrayList<Integer>();
    for (int i = 1; i <= 4; i  ) {
        idList.add(i);
    }

    List<Student> studentList = studentMapper.selectStudentIn(idList);
    for (Student student : studentList) {
        System.out.println(new JSONObject(student));
        System.out.println(student.getClass().getName()   "n");
    }
}

运行该方法后,最终生成出来的sql语句为:

SELECT * FROM student WHERE sid IN (1,2,3,4)

注意:

你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象传递给 foreach 作为集合参数。当使用可迭代对象或者数组时,index 是当前迭代的次数,item 的值是本次迭代获取的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。


(5)bind元素:

bind 元素可以从 OGNL 表达式中创建一个变量并将其绑定到上下文。比如:

代码语言:javascript复制
<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%'   _parameter.getTitle()   '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

(6)sql、include元素:

sql元素用来定义可重用的 SQL 代码段,这些代码段可以被包含在其他语句中,它可以被静态地(在加载参数) 参数化。而include元素就是用来引入sql元素所定义的可重用 SQL 代码段的,如下示例:

代码语言:javascript复制
<sql id="base_column_list">
    sid,sname,age,sex,address
</sql>
<select id="selectAll" resultMap="stuMap">
  select
  <include refid="base_column_list"/>
  from student
</select>

最终生成出来的sql语句为:

select sid,sname,age,sex,address from student

sql元素中也可以使用include元素,例如:

代码语言:javascript复制
<sql id="someinclude">
  from
    <include refid="${include_target}"/>
</sql>

关于返回null值:

有时候我们的表格中的某个列可能会存储了一些null值,如下表格:

当某个列存在null值的话,我们使用数据库的内置函数进行求和、统计之类的操作时,可能会刚好操作的记录的同一个字段都是null,那么返回的结果集就会是null。例如,我们对上面那张表格进行sum求和操作:

代码语言:javascript复制
<select id="selectBySumAge" resultType="int">
    SELECT SUM(age) FROM student
</select>

如果我们在dao层接口方法中声明的返回值是基本数据类型的话,就会报错,如下:

代码语言:javascript复制
...
public interface StudentMapper {
    ...
    int selectBySumAge();
}

测试用例代码:

代码语言:javascript复制
@Test
public void testSelectBySumAge() {
    Integer sumAge = studentMapper.selectBySumAge();
    System.out.println(sumAge);
}

这时运行测试用例的话,就会报如下错误:

代码语言:javascript复制
org.apache.ibatis.binding.BindingException: Mapper method 'org.zero01.dao.StudentMapper.selectBySumAge attempted to return null from a method with a primitive return type (int).

    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:93)
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
    at com.sun.proxy.$Proxy5.selectBySumAge(Unknown Source)
    at org.zero01.test.TestStudent.testSelectBySumAge(TestStudent.java:87)
    ... 略

会报这个错误是因为int这种基本数据类型是无法接收null的,只能使用包装类型进行接收。

为了解决这个问题,我们需要把dao层接口方法的返回值修改为Integer类型,如下:

代码语言:javascript复制
...
public interface StudentMapper {
    ...
    Integer selectBySumAge();
}

xml中的resultType可以使用int,但是为了防止意外问题,建议还是使用Integer包装类型:

代码语言:javascript复制
<select id="selectBySumAge" resultType="java.lang.Integer">
    SELECT SUM(age) FROM student
</select>

测试代码不变,运行后,控制台输出内容如下:

代码语言:javascript复制
null

除了在代码层解决这个问题外,还可以在sql中解决这个问题,以sum求和示例,使用以下几种sql语句,可以避免返回null值:

代码语言:javascript复制
/* 第一种: 采用 IFNULL(expr1,expr2)函数,当expr1为NULL时,则数据返回默认值expre2 */
SELECT IFNULL(SUM(age),0) FROM student  /* 若 SUM() 函数结果返回为 NULL 则返回 0 */

/* 第二种: 采用从 COALESCE(value,...) 函数, COALESCE 函数作用是返回传入参数中第一个非空的值 */
SELECT COALESCE(SUM(age),0) FROM student

/* 第三种: 采用 case WHEN THEN WHEN THEN .. ELSE END 函数,注意 CASE WHEN 函数最后是以 END 结尾 */
SELECT CASE WHEN ISNULL(SUM(age)) THEN 0 ELSE SUM(age) END AS ageSum FROM student

0 人点赞