MyBatis 映射文件详解

MyBatis 映射文件详解

MyBatis 的映射文件指导着 MyBatis 如何对数据库进行增删改查,即在 mapper.xml 文件中定义具体的 SQL 语句,以实现 DAO 层的接口。

mapper.xml 文件的约束头

<?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">

<mappers></mappers> 标签体中定义

增删改

编写 DAO 接口

package cool.yzt.mapper;

import cool.yzt.entity.User;
import java.util.List;

public interface UserMapper {
    // 添加一条记录
    public void save(User user);
    // 删除一条记录
    public void deleteById(int id);
    // 修改一条记录(根据ID 修改密码)
    public void changePassword(User user);
}

编写 XML 映射文件

<!--namespace对应dao接口的全类名-->
<mapper namespace="cool.yzt.mapper.UserMapper">
    <!--插入记录,标签名:insert,id是此sql的标识,对应dao接口的方法名-->
    <!--parameterType是sql的入参类型-->
    <!--标签体内部编写sql,使用#{}作为占位符-->
    <insert id="save" parameterType="user">
        insert into User (username,password,birthday) value(#{username},#{password},#{birthday})
    </insert>

    <!--删除,标签名:delete-->
    <delete id="deleteById" parameterType="int">
        delete from user where id=#{id}
    </delete>

    <!--修改,标签名:update-->
    <update id="changePassword" parameterType="user">
        update user set password=#{password} where username=#{username}
    </update>
</mapper>

使用

// 通过流的方式加载全局配置文件
InputStream is = MyBatisTest.class.getClassLoader().getResourceAsStream("mybatis.xml");
// 获取 SqlSession 的工厂
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
// 获取 SqlSession
SqlSession session = factory.openSession();

// 获取DAO接口的实现类对象,这个对象是 MyBatis 通过动态代理自动生成的
UserMapper mapper = session.getMapper(UserMapper.class);

// 通过实现类对象调用接口的方法
try {
    mapper.deleteById(4);
    // 增删改操作必须手动提交事务
    session.commit();
} finally {
    // 必须正确的关闭 SqlSession
    session.close();
}

注意

  1. 增删改操作的方法可以有 IntgerLongBoolean 类型的返回值,即该操作的影响行数,若返回值大于 0 或为 true,则表示该操作成功
  2. 增删改需要手动提交事务,获取 SqlSession 时可以设置为自动提交:SqlSession session = factory.openSession(true)
  3. 无论操作成功与否都必须正确关闭 SqlSession
  4. 对于 insert 操作,MyBatis 可以获取本次添加记录操作返回的自增主键值,对于 MySQL,支持自动增长主键,可以在映射文件的 标签中进行如下配置,表示获取添加的新记录的自增主键,并赋值给 id 属性
<insert id="save" parameterType="user" useGeneratedKeys="true" keyProperty="id">
    insert into User (username,password,birthday) value(#{username},#{password},#{birthday})
</insert>

则在完成添加操作后,可以通过传入参数的对象,获取该主键的值

mapper.save(user);
session.commit();
System.out.println(user.getId()); // 若不在映射文件中配置,则无法正确获取

参数处理

此处参数是指一个 SQL 语句执行所需要的参数,也是 DAO 接口中方法定义的参数

单个参数

  1. 如果这个参数是基本类型(包括对应的包装类型),MyBatis 不会对其进行特殊处理,直接使用 #{} 取出使用即可,{} 中可以是任何值
  2. 如果这个参数是自定义的对象类型,MyBatis 也不会特殊处理,使用 #{} 取出该对象的对应字段的值

多个参数

  1. 对于多个参数,MyBatis 会将参数封装成一个 map,该 map 的 key 默认是 param1param2...paramNvalue 就是对应传入参数的值
  2. 可以在 DAO 接口定义方法时,传入的参数前加上 @Param 注解,自定义该参数封装为 map 后的 key
// MyBatis 会将传入参数封装为一个map,key为 id 和 name
public User findByIdAndName(@Param("id")Integer id,@Param("name")String name);

在 SQL 语句中,使用 #{key} 取出 key 对应的值

封装 Map

既然对于多个参数 MyBatis 都会封装为 Map,而使用默认 keyparam1param2)或者使用 @Param 注解都非常麻烦,那么建议直接将要传入的参数封装为 Map 后传入,SQL 语句获取值时使用 #{key},例如

DAO 接口中

public User findByIdAndName(Map<String,String> params);

映射文件中

<select id="findByIdAndName" resultType="cool.yzt.entity.User">
    select * from user where id = #{id} and username = #{name}
</select>

使用

Map<String,String> params = new HashMap<String, String>();
params.put("id","4");
params.put("name","李四");
User user = userMapper.findByIdAndName(params);

POJO 和 TO

  1. 如果要传入的参数恰好是业务逻辑中实体类的属性(的一部分),建议直接传入 POJO,使用 #{} 获取 POJO 中的属性值
  2. 如果要传入的参数不是实体类中的属性,要么封装 Map,要么编写一个 TO 类(Transfer Object),专门用于参数的传递,例如分页查询中封装的 pagebean

集合类

对于集合类和数组类型的参数,MyBatis 也会封装成 Map,下表为类型和 key 的对应关系

类型key
Collectioncollection
Listlist
数组array

#{} 与 ${} 的区别

  1. #{}:以预编译的形式将参数设置到 SQL 语句中,使用 ? 作为占位符,类似 JDBC 中的 PreparedStatement
  2. ${}:取出参数后直接以拼字符串的形式拼接在 SQL 语句中,虽然也可以完成与 #{} 一样的功能,但是无法防止 SQL 注入,应用场景:如果数据库进行了分表,需要根据参数查询不同的表,则表名处可以使用 ${} 进行取值,因为表名处不支持使用 ? 作为占位符,而必须使用拼接字符串的方式

查询

结果集自动封装

映射文件中使用 <select></select> 标签定义查询语句,其中 id 属性值与 DAO 接口中定义的查询方法名相同,parameterType 属性定义参数类型(可以省略),resultType 定义返回值类型,返回值可以是基本类型、自定义类型(例如业务对象)、集合,如果是自定义类型,需要写该类的全类名,如果是集合类型,则写集合中存放的元素的类型(而不是集合类型),注意该属性不可以与 resultMap 同时使用

在数据库建一张 emp 表和 dept 表,并插入若干数据,如下,注意数据库中 birthday 字段的类型是 bigint,存储日期的时间戳,并在项目中定义了负责 Java 日期类型和 Long 类型的转换的 TypeHandler,MyBatis 会自动调用,完成转换

emp表.png

dept表.png

定义 Employee 的实体类,注意到数据库 emp 表中部门号的字段名为 dept_id,而实体类中的属性名为 deptId,其他字段名相同

package cool.yzt.entity;

import java.util.Date;

public class Employee {
    private int id;
    private String name;
    private String gender;
    private Date birthday;
    private int deptId;
    /*
    构造器、setter、getter、toString
    */
}
package cool.yzt.entity;

public class Department {
    private int id;
    private String name;
    /*
    构造器、setter、getter、toString
    */
}

编写 DAO 层接口,定义查询方法

package cool.yzt.mapper;

import cool.yzt.entity.Employee;
import java.util.List;

public interface EmployeeMapper {
    // 根据传入的id查询
    public Employee findById(Integer id);
    
    // 查询所有记录,存储在List中
    public List<Employee> findAll();
}

编写 EmployeeMapper.xml 映射文件

<?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">

<!--namespace对应DAO层的接口-->
<mapper namespace="cool.yzt.mapper.EmployeeMapper">
    <!--根据传入的id查询,parameterType可以省略,resultType-->
    <select id="findById" resultType="cool.yzt.entity.Employee">
        select * from emp where id = #{id}
    </select>

    <!--查询所有记录,封装为List,resultType为List中存储的元素的类型-->
    <select id="findAll" resultType="cool.yzt.entity.Employee">
        select * from emp
    </select>
</mapper>

测试

public void test1() {
    InputStream is = MultiTableTest.class.getClassLoader().getResourceAsStream("mybatis.xml");
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
    SqlSession sqlSession = factory.openSession();
    EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
    
    try {
        Employee emp = mapper.findById(6);
        System.out.println(emp);
    } finally {
        sqlSession.close();
    }
}

输出结果,可以看到,除了 deptId 属性,其他字段的值都被正确查询并且封装到 Employee 对象中,这是因为 Java 实体类中的属性名和数据库表中的字段名相同,MyBatis 便可以完成自动映射封装

Employee{id=6, name='李四', gender='男', birthday=Sun Jul 19 14:11:17 CST 2020, deptId=0}

结果集封装为 Map

如果返回值并不是一个完整的业务对象,可以将结果(对应一条记录)封装为一个 Map,Map 的 key 是字段名,值时字段对应的数据库记录值

DAO 层接口方法编写

public Map<String,String> findNameAndBirthdayById(Integer id);

Employee.xml 映射文件配置

<select id="findNameAndBirthdayById" resultType="map">
    select name,birthday from emp where id = #{id}
</select>

测试

Map<String,String> res = mapper.findNameAndBirthdayById(7);
System.out.println(res);
System.out.println(res.get("name"));

输出结果

{birthday=1595139077344, name=唐僧}
唐僧

自定义结果集的封装格式

除了 MyBatis 的结果集自动封装(使用 resultType),更常用的是自定义查询结果集的封装格式映射,使得结果集的封装更加灵活和方便,在映射文件中使用 <resultMap></resultMap> 标签完成这个功能,<resultMap> 标签的 id 属性是此自定义映射的唯一表示,type 属性表示关联至一个 Java 类

<resultMap id="employeeMap" type="cool.yzt.entity.Employee">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="birthday" column="birthday"/>
    <result property="deptId" column="dept_id"/>
</resultMap>

<resultMap> 标签各个子标签的作用
|标签|作用|属性|
|---|---|---|
|id|表示某一个字段的映射,标记为id可以提高性能,一般用于主键|property:对应Java类中的属性,column:对应查询出的某一列的列名|
|result|表示某一个字段的映射,用于普通字段的映射|property和column属性作用与上同|

修改 <select> 标签的 resultTyperesultMap,指向上面自定义的 <resultMap>

<!--根据传入的id查询,parameterType可以省略,resultType-->
<select id="findById" resultMap="employeeMap">
    select * from emp where id = #{id}
</select>

<!--查询所有记录,封装为List,resultType为List中存储的元素的类型-->
<select id="findAll" resultMap="employeeMap">
    select * from emp
</select>

测试

Employee emp = mapper.findById(6);
System.out.println(emp);

发现 MyBatis 可以正确封装数据库的 dept_id 字段到 Java 类中的 deptId 属性

Employee{id=6, name='李四', gender='男', birthday=Sun Jul 19 14:11:17 CST 2020, deptId=2}

特殊需求1:查询 emp 表,并根据 dept_id 查询对应的 dept 表的信息

首先,为测试方便,定义一个 TO,封装 emp 的部分数据和 dept ,可以看到这个类中存在自定义类作为属性,这样一个类并不对应数据库中的任何表,所以封装成一个 TO 用于查询结果的封装和传输

package cool.yzt.to;

public class EmpTO {
    private String employeeName;
    private String employeeGender;
    private Department department;
    /*
    构造器、setter、getter、toString
    */
}

DAO层接口编写,根据 id 查询并附带 department 信息

public EmpTO findWithDeptById(Integer id);

Employee.xml 映射文件编写

方法一:联合查询,嵌套结果集

联合查询两张表,即使用隐式内连接,使用 where 条件消除多余数据,查询有用的字段进行封装,形成嵌套结果集,即结果集对应的 Java 类中还有另外一个的对象作为属性,需要使用 <association> 标签进行封装,<association>property 对应该类的名,javaType 为该类的全类名,其子标签和子标签属性与之前相同,注意 column 属性值要保证唯一

<resultMap id="empTO" type="cool.yzt.to.EmpTO">
    <result property="employeeName" column="ename"/>
    <result property="employeeGender" column="egender"/>
    <association property="department" javaType="cool.yzt.entity.Department">
        <id property="id" column="did"/>
        <result property="name" column="dname"/>
    </association>
</resultMap>

<select id="findWithDeptById" resultMap="empTO">
    select
        emp.name ename,emp.gender egender,dept.id did,dept.name dname
    from
        emp,dept
    where
        emp.id = #{id} and emp.dept_id = dept.id
</select>

测试

InputStream is = MultiTableTest.class.getClassLoader().getResourceAsStream("mybatis.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = factory.openSession();
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);

try {
    EmpTO emp = mapper.findWithDeptById(7);
    System.out.println(emp);
} finally {
    sqlSession.close();
}

打印结果,可以看到结果都已正确封装,包括 TO 类的内部引用

EmpTO{employeeName='唐僧', employeeGender='男', department=Department{id=3, name='宣传部'}}

方法二:分步查询,组合其他方法
考虑到 Department 类作为一个实体类对应数据库中的一张表,所以也为其编写 DAO 层接口和映射文件

DAO 层接口的方法

public Department findById(Integer id);

DepartmentMapper.xml 映射文件

<mapper namespace="cool.yzt.mapper.DepartmentMapper">
    <resultMap id="departmentMap" type="cool.yzt.entity.Department">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
    </resultMap>
    <!--根据传入的id信息查询dept信息-->
    <select id="findById" resultMap="departmentMap">
        select * from dept where id = #{id}
    </select>
</mapper>

修改 EmployeeMapper.xml 映射文件

<resultMap id="empTO" type="cool.yzt.to.EmpTO">
    <result property="employeeName" column="name"/>
    <result property="employeeGender" column="gender"/>
    <!--引用的其他类的对象由别的sql语句查询,指定方法的引用,column是指传入那一列值作为参数-->
    <association property="department" select="cool.yzt.mapper.DepartmentMapper.findById" column="dept_id"/>
</resultMap>
<!--正常查询emp表的信息-->
<select id="findWithDeptById" resultMap="empTO">
    select name,gender,dept_id from emp where id = #{id}
</select>

测试结果,与方法一相同

EmpTO{employeeName='唐僧', employeeGender='男', department=Department{id=3, name='宣传部'}}

使用方法二,建议在主配置文件中开启懒加载,即在必须时才会调用其他查询方法进行查询,否则不会调用

<settings>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

特殊需求2:查询部门,及其每个部门下的员工信息

首先封装一个 TO,注意到该类内部使用 List 集合存储员工

package cool.yzt.to;

import cool.yzt.entity.Employee;

import java.util.List;

public class DeptTO {
    private int id;
    private String name;
    private List<Employee> employees;
    /*
    构造器、setter、getter、toString
    */
}

定义 DAO 层接口方法

public DeptTO findWithEmpById(Integer id);

方法一:联合查询,嵌套结果集

定义映射文件,使用 <collection> 标签封装类内部的集合类属性,ofType 属性为集合内存储的元素的类型

使用外连接查询,根据传入 id 查询对应的 dept,使用左外连接查询其对应的所有 emp 信息

<resultMap id="deptTO" type="cool.yzt.to.DeptTO">
    <id property="id" column="did"/>
    <result property="name" column="dname"/>
    <collection property="employees" ofType="cool.yzt.entity.Employee">
        <id property="id" column="eid"/>
        <result property="name" column="ename"/>
        <result property="gender" column="gender"/>
        <result property="birthday" column="birthday"/>
        <result property="deptId" column="deptId"/>
    </collection>
</resultMap>

<select id="findWithEmpById" resultMap="deptTO">
    select
    dept.id did,dept.name dname,emp.id eid,emp.name ename,emp.gender gender,emp.birthday birthday,emp.dept_id deptId
    from
    dept
    left join emp on emp.dept_id = dept.id
    where dept.id = #{id}
</select>

测试

 InputStream is = MybatisTest.class.getClassLoader().getResourceAsStream("mybatis.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = factory.openSession();
DepartmentMapper mapper = sqlSession.getMapper(DepartmentMapper.class);

try {
    DeptTO dept = mapper.findWithEmpById(1);
    System.out.println(dept.getId());
    System.out.println(dept.getName());
    List<Employee> emps = dept.getEmployees();
    for(Employee emp : emps) {
        System.out.println(emp);
    }
} finally {
    sqlSession.close();
}

打印结果

1
研发部
Employee{id=5, name='张三', gender='女', birthday=Sun Jul 19 14:11:17 CST 2020, deptId=1}
Employee{id=9, name='武松', gender='男', birthday=Sun Jul 19 14:11:17 CST 2020, deptId=1}

方法二:分步查询,组合其他方法

映射文件和查询语句

<resultMap id="deptTO" type="cool.yzt.to.DeptTO">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <collection property="employees" select="cool.yzt.mapper.EmployeeMapper.findByDeptId" column="id"/>
</resultMap>

<select id="findWithEmpById" resultMap="deptTO">
    select id,name from dept where id = #{id}
</select>

测试结果与方法一相同

分步查询多列值的封装传递

在使用 <association> 以及 <collection> 中使用分步查询时,如果调用的别的查询方法需要传递多个参数可以使用如下写法,即将参数封装为一个 Map 传入,注意,Map 的 key 需要和调用的查询方法中使用 #{} 取得的参数名保持一致

<collection property="employees" select="cool.yzt.mapper.EmployeeMapper.findByDeptId" column="{deptId=id,deptName=name}"/>

参考

尚硅谷 MyBatis 教程

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://yzt.cool/archives/mybatis映射文件详解