一、Mybatis 介绍

基本介绍

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

Mybatis官方文档: https://mybatis.org/mybatis-3/zh_CN/index.html

Quick Start

// todo

查看官网案例: https://mybatis.org/mybatis-3/zh_CN/getting-started.html

二、🌈Mybatis 基本使用

基本使用

  1. 导入依赖

  2. 进行数据库配置

  3. 定义实体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Data
    public class Emp implements Serializable {

    private Integer id;

    private String empName;

    private Integer age;

    private Double empSalary;
    }
  4. 定义接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public interface EmpMapper {

    //查询单行记录
    Emp getEmpById(Integer id);

    //查询多行记录
    List<Emp> getAll();

    //更新
    int updateEmp(Emp emp);

    // 添加
    int addEmp(Emp em);

    // 删除
    int deleteEmp(Integer id);
    }
  5. 在mapper.xml 中编写CRUD

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    <?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.xiaoyu.mapper.EmpMapper">

    <!-- 查询单行 -->
    <select id="getEmpById" resultMap="BaseResultMap">
    select * from t_emp where id = #{id}
    </select>

    <!-- 查询所有 -->
    <select id="getAll" resultMap="BaseResultMap">
    select * from t_emp
    </select>

    <!-- 更新语句 -->
    <update id="updateEmp">
    update
    t_emp set emp_name = #{empName}, age = #{age},
    emp_salary = #{empSalary}
    where
    id = #{id}
    </update>

    <!-- 插入语句 -->
    <insert id="addEmp" useGeneratedKeys="true" keyProperty="id">
    insert into
    t_emp(emp_name, age, emp_salary)
    values
    (#{empName}, #{age}, #{empSalary})
    </insert>

    <!-- 删除语句 -->
    <delete id="deleteEmp" parameterType="integer">
    delete from t_emp where id = #{id}
    </delete>
    </mapper>
  6. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @SpringBootTest
    class MybatisApplicationTests {

    @Resource
    EmpMapper empMapper;

    @Test
    void testGetEmp() {
    System.out.println(empMapper.getEmpById(1));
    }

    @Test
    void testAll() {
    List<Emp> all = empMapper.getAll();
    all.forEach(System.out::println);
    }
    }

三、参数传递

两种取值方式

  • #{}:底层使用 PreparedStatement 方式,SQL预编译后设置参数,无SQL注入攻击风险

  • ${}:底层使用 Statement 方式,SQL无预编译,直接拼接参数,有SQL注入攻击风险

使用场景

  • 所有参数位置,都应该用 #{}

  • 需要动态表名等,才用 ${}

注意: 凡是使用了 ${} 的业务,一定要自己编写防SQL注入攻击代码

参数取值

传参形式 示例 取值方式
单个参数 - 普通类型 getEmploy(Long id) #{变量名}
单个参数 - List类型 getEmploy(List id) #{变量名[0]}
单个参数 - 对象类型 addEmploy(Employ e) #{对象中属性名}
单个参数 - Map类型 addEmploy(Map<String,Object> m) #{map中属性名}

案例

  • 形参使用@Param指定参数名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Mapper 
    public interface EmpParamMapper {

    //多个参数,用@Param指定参数名, #{参数名} 就可以取值。
    Emp getEmployHaha(@Param("id") Long id,
    @Param("m") Map<String,Object> m,
    @Param("ids") List<Long> ids,
    @Param("e") Emp e);
    }
  • 取值使用#{参数名} 来取值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <select id="getEmployHaha" resultType="com.atguigu.mybatis.bean.Emp">
    select
    *
    from
    t_emp
    where
    id = #{id}
    and
    emp_name = #{m.name}
    and
    age = #{ids[2]}
    and
    emp_salary = #{e.empSalary}
    </select>
  • 实际参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Test
    void testParam03(){
    Map<String, Object> params = new HashMap<>();
    params.put("name", "bbbb");

    Emp emp = new Emp();
    emp.setEmpSalary(1000.0D);
    empParamMapper.getEmployHaha(1L, params,
    Arrays.asList(19L,20L,21L,22L,34L), emp);
    }

四、⭐ResultMap

ResultMap是Mybatis最强大的元素,它可以将查询到的复杂数据(比如查询到几个表中数据)映射到一个结果集当中。

简单结果映射

问题

若实体类中的属性和数据库表的字段不一致会出现 不对应的字段查询结果为空的情况

解决办法:

  1. JavaBean 和 数据库一样 【不推荐】
  2. 使用 列别名 【不推荐】
  3. 使用 驼峰命名自动映射
  4. 使用 ResultMap(自定义结果集)

使用resultMpa进行简单的结果集映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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.xiaoyu.mapper.EmpMapper">

<resultMap id="BaseResultMap" type="com.xiaoyu.pojo.Emp">
<id property="id" column="id"/>
<result property="empName" column="emp_name" />
<result property="age" column="age" />
<result property="empSalary" column="emp_salary"/>
</resultMap>

<!-- resultMap:指定自定义映射规则 -->
<select id="getEmpById" resultMap="BaseResultMap">
select * from t_emp where id = #{id}
</select>
</mapper>
  • resultMap 元素是 MyBatis 中最重要最强大的元素

  • ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。

  • ResultMap 的优秀之处在于 你完全可以不用显式地配置它们。

  • 如果这个世界总是这么简单就好了。(复杂映射!)

复杂映射

MyBatis 创建时的一个思想是:数据库不可能永远是你所想或所需的那个样子。 我们希望每个数据库都具备良好的第三范式或 BCNF 范式,可惜它们并不都是那样。 如果能有一种数据库映射模式,完美适配所有的应用程序,那就太好了,但可惜也没有。 而 ResultMap 就是 MyBatis 对这个问题的答案

一对一查询案例
按照id查询订单 以及 下单的客户

  • 实体类处理

    • Order类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @Data
      public class Order implements Serializable {
      // 订单id
      private Long id;
      // 派送地址
      private String address;
      // 订单金额
      private BigDecimal amount;
      // 客户id
      private Long customerId;
      // 该订单的消费者
      Customer customer;
      }
    • Customer 实体类

      1
      2
      3
      4
      5
      6
      7
      8
      @Data
      public class Customer implements Serializable {
      private Long id;
      // 客户姓名
      private String customerName;
      // 手机号
      private String phone;
      }
  • 使用ResultMap 进行结果嵌套处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    <?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.xiaoyu.mapper.OrderMapper">

    <resultMap id="OrderAndCustomerResultMap" type="com.xiaoyu.pojo.Order">
    <id property="id" column="id" jdbcType="BIGINT"/>
    <result property="address" column="address" jdbcType="VARCHAR"/>
    <result property="amount" column="amount" jdbcType="DECIMAL"/>

    <!-- 一对一关联封装 -->
    <association property="customer" javaType="com.xiaoyu.pojo.Customer">
    <id property="id" column="id" jdbcType="BIGINT"/>
    <result property="customerName" column="customer_name"
    jdbcType="VARCHAR"/>
    <result property="phone" column="phone" jdbcType="VARCHAR"/>
    </association>
    </resultMap>

    <select id="queryOrderAndCustomer" resultMap="OrderAndCustomerResultMap">
    select
    c.id,c.customer_name, c.phone, o.id, o.address, o.amount
    from
    t_order o
    join
    t_customer c
    on
    o.customer_id = c.id
    where
    o.id = #{id}
    </select>
    </mapper>

一对多案例

按照id查询订单 以及 下单的客户

  • 实体类处理

    • Order实体类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Data
    public class Order implements Serializable {
    // 订单id
    private Long id;
    // 派送地址
    private String address;
    // 订单金额
    private BigDecimal amount;
    // 客户id
    private Long customerId;
    }
    • Customer 实体类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @Data
      public class Customer implements Serializable {
      private Long id;
      // 客户姓名
      private String customerName;
      // 手机号
      private String phone;
      // 消费者的多个订单
      private List<Order> list;
      }
  • 使用ResultMap 进行复杂结果映射

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    <?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.xiaoyu.mapper.CustomerMapper">

    <resultMap id="CustomerAndOrderResultMap" type="com.xiaoyu.pojo.Customer">
    <id property="id" column="c_id" jdbcType="BIGINT"/>
    <result property="customerName"
    column="customer_name" jdbcType="VARCHAR"/>
    <result property="phone" column="phone" jdbcType="VARCHAR"/>

    <!-- collection:说明 一对N 的封装规则 ofType: 集合中元素的类型 -->
    <collection property="list" ofType="com.xiaoyu.pojo.Order" >
    <id property="id" column="o_id" jdbcType="BIGINT"/>
    <result property="address" column="address" jdbcType="VARCHAR"/>
    <result property="amount" column="amount" jdbcType="DECIMAL"/>
    <result property="customerId"
    column="customer_id" jdbcType="BIGINT"/>
    </collection>
    </resultMap>

    <select id="queryCustomerAndOrderById" resultMap="CustomerAndOrderResultMap">
    select
    c.id c_id, customer_name, phone, o.id, address, amount, customer_id
    from
    t_customer c
    left join
    t_order o
    on
    c.id = o.customer_id
    where
    c.id = #{id};
    </select>
    </mapper>

Result的属性

属性 描述
property 映射到JavaBean的属性。
column 数据库中的列名,或者是列的别名。
javaType 一个 Java 类的全限定名,或一个类型别名。 如果你映射到一个 JavaBean,MyBatis 通常可以推断类型。如果你映射到的是 HashMap需明确地指定 javaType
jdbcType JDBC 类型, 只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型。
typeHandler 使用这个属性,你可以覆盖默认的类型处理器。 这个属性值是一个类型处理器实现类的全限定名,或者是类型别名。

分布查询

五、⭐动态SQL

动态SQL就是根据不同的条件生成不同的SQL语句

动态SQL常用标签

IF标签

1
2
3
4
5
6
7
8
9
<select id="queryBlog" parameterType="map" resultType="blog" >
select * from blog where 1 = 1
<if test="title != null">
and title = #{title}
</if>
<if test="author != null">
and author = #{author}
</if>
</select>

where标签

作用: 可以智能的添加或去掉 SQL语句中的 and | or

1
2
3
4
5
6
7
8
9
10
11
12
<!-- where 标签的使用-->
<select id="queryBlog2" parameterType="map" resultType="blog" >
select * from blog
<where>
<if test="title != null">
and title = #{title}
</if>
<if test="author != null">
and author = #{author}
</if>
</where>
</select>

choose、when、otherwise标签

类似样式java的switch case语句

  • choose —> switch
  • when —> case
  • otherwise —> default

set标签

set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号

1
2
3
4
5
6
7
8
9
10
<update id="updateBlog" parameterType="map" >
update blog
<set>
<if test="title != null"> title = #{title},</if>
<if test="author != null"> author = #{author},</if>
<if test="createTime != null"> createTime = #{createTime},</if>
<if test="views != null"> views = #{views},</if>
</set>
where id = #{id}
</update>

ForEach 标签

foreach 标签说明

  • collection 需要遍历的集合
  • item 集合中的每个元素
  • open 开始拼接的字符
  • close 结束时拼接的字符
  • separator 分隔符
1
2
3
4
5
6
7
8
<select id="queryForEach" parameterType="map" resultType="blog" >
select * from blog
<where>
<foreach collection="ids" item="id" open=" and (" close=")" separator="or" >
id = #{id}
</foreach>
</where>
</select>

拼接完成的sql语句

select * from blog where 1 = 1 and ( id = 1 or id = 2 or id = 3 or id = 4 )

小结: 所谓的动态SQL, 本质还是SQL语句, 只是我们可以在SQL层面, 去执行一个逻辑代码

SQL片段

有的时候, 我们可能会将一些功能的部分抽取出来, 方便复用

  1. 使用sql标签抽取出公共的部分

    1
    2
    3
    4
    5
    6
    7
    8
    <sql id="if-title-author" >
    <if test="title != null">
    and title = #{title}
    </if>
    <if test="author != null">
    and author = #{author}
    </if>
    </sql>
  2. 在需要使用的地方使用 include 标签即可

    1
    2
    3
    4
    5
    6
    <select id="queryBlog2" parameterType="map" resultType="blog" >
    select * from blog
    <where>
    <include refid="if-title-author" />
    </where>
    </select>

注意事项:

  • 最好基于单表来定义SQL片段
  • SQL片段中不要存在where 标签

六、Cache

简介

什么是缓存 [Cache]

  • 存在内存中的临时数据
  • 将用户经常查询的数据放在缓存(内存)中, 用户查询数据就不用从磁盘上(关系型数据库数据文件) 查询, 从缓存中查询, 从而提高查询效率, 解决了高并发系统的性能问题

为什么使用缓存

  • 减少和数据库的交互次数, 减少系统开销, 提高系统效率

什么样的数据能使用缓存

  • 经常查询并且不经常改变的数据

Mybatis中的缓存

Mybatis 包含一个非常强大的查询缓存特性, 他可以方便地定制和配置缓存. 缓存可以极大的提升查询效率

Mybatis系统中默认定义了两级缓存: 一级缓存二级缓存

  • 默认情况下, 只有一级缓存开启 ( SqlSession级别的缓存(事务级别的缓存), 也成为了本地缓存)
  • 二级缓存需要手动开启和配置, 他是基于namespace 级别的缓存
  • 为了提高拓展性, Mybatis定义了缓存接口Cache. 我们可以通过实现Cache 接口来自定义二级缓存

一级缓存

  • 一级缓存也叫本地缓存: SqlSession 在此次十五中存在的缓存
    • 与数据库同义词回话期间查询到的数据会放在本地缓存中
    • 以后如果需要获取相同的数据, 直接从缓存中拿, 没有必要再去查询数据库

测试

  1. 开始日志
  2. 测试在一个Session中查询两次相同的记录
  3. 查看日志输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void queryStuById(){

SqlSession sqlSession = MybatisUtils.getSqlSession();
StuMapper mapper = sqlSession.getMapper(StuMapper.class);

Stu stu = mapper.queryStu(2);
System.out.println(stu);
System.out.println("=========================================================");

//更新数据时, 缓存会刷新
// mapper.update(new Stu(6, "小红", 33, "女"));
// sqlSession.clearCache(); //手动刷新缓存

Stu stu1 = mapper.queryStu(2);
System.out.println(stu == stu1);

sqlSession.commit();
sqlSession.close();
}

缓存失效的情况

  1. 查询不同的记录

  2. 增删改操作, 可能会改变原来的数据, 所以必定会刷新缓存!

  3. 查询不同的Mapper.xml

  4. 手动清理缓存

    1
    sqlSession.clearCache();  //手动清理缓存

小结

  • 一级缓存默认开启, 只在一次SqlSession中有效, 也就是拿到连接到关闭连接这个区间段

  • 一级缓存相当于一个Map

二级缓存

  • 二级缓存也叫全局缓存, 一级缓存作用域太低, 所以诞生了二级缓存
  • 基于namespace 级别的缓存, 一个名称空间, 对应一个二级缓存
  • 工作机制
    • 一个回话查询一条数据, 这个数据就会被放在当前回话的一级缓存中
    • 如果当前会话关闭了, 这个回话对应的一级缓存也就没有了; 当回话关闭了, 一级缓存中的数据就会被保存到二级缓存中
    • 新的会话查询信息, 就可以从二级缓存中获取内容
    • 不同的mapper查出的数据会放在自己对应的缓存(map)中

二级缓存的使用

在要使用二级缓存的mapper.xml文件中开启

1
<cache/>

也可以自定义一些参数

1
2
3
4
5
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>

测试

问题: 如果在cache标签中没有定义参数, 则会有如下异常

Cause: java.io.NotSerializableException: com.xiaoyu.pojo.Stu

解决方法: 将实体类序列化, 实现Serializable 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testQuery(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
StuMapper mapper = sqlSession.getMapper(StuMapper.class);

Stu stu = mapper.queryStu(1);
System.out.println(stu);
sqlSession.close();

SqlSession sqlSession2 = MybatisUtils.getSqlSession();
StuMapper mapper2 = sqlSession2.getMapper(StuMapper.class);
Stu stu2 = mapper2.queryStu(1);
System.out.println(stu2);

System.out.println(stu == stu2);
}

小结

  • 只要开启了二级缓存, 在同一个mapper.xml就下有效
  • 所有的数据都会先放在一级缓存中
  • 只有当回话提交, 或者关闭时, 才会提交到二级缓存中!

缓存原理

七、分页插件 PageHelper