学 MyBatis?看这一篇就够了!

726 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、Mapper组件

Mapper 组件 = Mapper 接口 + Mapper XML 文件

1. 注意事项

  • 接口命名:实体名 + Mapper,编译之后和XML文件放在一起
  • XML 命名空间使用接口全限定名
  • Mapper 接口方法名和Mapper XML文件元素id值相同
  • 方法返回类型对应SQL元素中定义的resultType / resultMap
  • 方法参数类型对应SQL元素中定义的paramterType类型(一般不写)

2. Mapper接口定义

public interface UserMapper {
    // 抽象方法
    User queryByName(String name);
}

3. Mapper接口使用

@Test
public void queryByName() {
    SqlSession session = MyBatisUtil.openSession();
    UserMapper userMapper = session.getMapper(UserMapper.class);
    System.out.println(userMapper.queryByName("刘备"));
    session.commit();
    session.close();
}

4. Mapper接口原理

底层使用的是动态代理,生成Mapper接口的实现类。

接口只是规范,本质上的实现由实现类对象完成,而实现类由MyBatis使用动态代理创建。而我们只需要提供Mapper接口对应的Mapper XML文件,获取实现类对象时传入接口的字节码对象即可。

实现类底层的操作方式其实与原先一样,因为Mapper XML命名空间是使用Mapper接口的全限定名,方法名又与对应XML元素id一致,因此可以通过获取调用方法所在Mapper接口的全限定名和方法名进行拼接,再加上调用方法的实参实现。

二、@Param注解

// 本质相当于构建一个Map,key为注解@Param的值,value为参数值
User queryByNameAndPassword(@Param("name") String name, @Param("password") String password);

可以使用Alt + Enter快速生成

集合/数组参数:当传递一个List对象/数组对象给MyBatis时,MyBatis会自动封装到Map中,List对象以list为key,数组对象以array为key,也可以使用@Param注解自定义名称。

三、MyBatis中#和$的区别

  • 相同点:都可以获取对象中的信息

  • 不同点

    #$
    传递的任何类型参数都会加上一对单引号直接作为SQL语句的一部分
    支持简单类型参数作为值 (基数类及包装类、String、BigDecimal等)不支持简单类型参数作为值
    没有SQL注入问题,相对安全存在SQL注入问题,相对不安全

总结:ORDER BY / GROUP BY子句中获取参数使用$,其它使用#

四、动态SQL

  • if、where:常用于过滤查询中

    <!--根据用户名称和年龄范围查询用户的功能-->
    <select id="queryByUqo" resultType="cn.regex.ims.domain.User">
        SELECT id, name, password, age
        FROM user
        <where>
            <if test="name != null and name != ''">
                AND name LIKE CONCAT('%', #{name}, '%')
            </if>
            <if test="minAge != null">
                AND age &gt;= #{minAge}
            </if>
            <if test="maxAge != null">
                AND age &lt;= #{maxAge}
            </if>
        </where>
    </select>
    
  • if、set:常用于更新语句中

    <!--修改用户名称、密码和年龄的功能-->
    <update id="updateById">
        UPDATE user
        <set>
            <if test="name != null and name != ''">
                name = #{name}
            </if>
            <if test="password != null and password != ''">
                password = #{password}
            </if>
            <if test="age != null">
                age = #{age}
            </if>
        </set>
        WHERE id = #{id}
    </update>
    
  • foreach:常用于批量删除、批量添加、查询语句中

    • collection 遍历数组或集合的 key 或者属性名
    • open 遍历开始拼接的字符串
    • index 遍历索引
    • item 遍历元素
    • separator 每遍历元素拼接字符串
    • close 遍历结束拼接字符串
    <!--批量保存用户的功能-->
    <insert id="batchInsert" useGeneratedKeys="true" keyProperty="id">
          <!-- INSERT INTO `user` VALUES ('1', '刘备', '001', '46'),('3', '关羽', '003', '44'); -->
          INSERT INTO user (name, password, age) VALUES
          <foreach collection="users" item="user" separator=",">
              (name = #{name}, password = #{password}, age = #{age})
          </foreach>
    </insert>
    
    <!--根据 id 批量删除用户的功能-->
    <delete id="batchDelete">
        DELETE FROM user WHERE id IN
        <foreach collection="ids" open="(" item="id" separator="," close=")">
            #{id}
        </foreach>
    </delete>
    

五、关系概述

  1. 关联关系:A依赖B,并将B作为成员变量,则A和B存在关联关系。

  2. 关联关系分类

    关联关系分类.png

  3. 判断对象的关系

    • 从对象的实例上看
    • 根据对象的属性
    • 根据具体需求

六、单向多对一

  1. 保存
@Data
public class Brand {
    private Long id;
    private String name;
}
@Data
public class Product {
    private Long id;
    private String name;
    private Brand brand;    // 关联属性
}
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
    <!--
        #{name} ==> product.getName
        #{brand.id} ==> product.getBrand().getId()
    -->
    INSERT INTO product (name, brand_id) VALUES (#{name}, #{brand.id})
</insert>

注意事项

  1. 关联属性需要通过 "." 进行取值,即#{brand.id}
  2. 需要先保存关联属性对应的对象再保存其它,即先保存brand对象再保存product对象,否则添加的product将没有对应的关联id
  1. 额外SQL查询
<select id="selectById" resultType="product">
    SELECT id, name, brand_id FROM product WHERE id = #{id}
</select>

如果采用以上方式查询,会发现查询到的品牌为null,原因是结果集的列名与对象的属性名不一致,通过MyBatis提供的resultMap元素解决。

<!--
    id   唯一标识resultMap,可以有多个resultMap
    type 数据要封装的对象类型
-->
<resultMap id="baseResultMap" type="Product">
    <!--
        column   结果集对应的列名
        property 封装对象上的属性名
    -->
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <result column="brand_id" property="brand.id"/>
    <!--
        association 针对关联属性配置,非集合类型
        column      传给额外SQL语句的值对应的列名
        property    额外SQL语句查询结果要封装的对象类型
        select      额外SQL语句的位置(全限定名 + id)
    -->
    <association property="brand" column="brand_id"
                 select="cn.regex.ims.mapper.BrandMapper.selectById"/>
</resultMap>
<select id="selectById" resultMap="baseResultMap">
    SELECT id, name, brand_id FROM product WHERE id = #{id}
</select>

N + 1 问题:A表中有N条数据,每一条数据都关联着B表中不同的数据,当查询A表所有数据(包含B表关联的数据)时,会执行 1 + N 条SQL语句,即查询A表所有数据(1条SQL) + 查询A表所有数据关联的数据(N条SQL)

比如有两张表,一张员工表,一张部门表,要查所有员工及其对应的部门,则需要执行一条查询所有员工的SQL,查询出的每一条记录都需要顺便执行一条SQL查询部门信息。

解决问题:通过多表查询方式

<resultMap id="selectAllResultMap" type="Product">
 <id column="id" property="id"/>
 <result column="name" property="name"/>
 <result column="b_id" property="brand.id"/>
 <result column="b_name" property="brand.name"/>
</resultMap>
<select id="selectAll" resultMap="selectAllResultMap">
 SELECT p.id, p.name, p.brand_id b_id, b.name b_name
 FROM product p
 JOIN brand b
     ON p.brand_id = b.id
</select>

当属性和关联表的列过多时,可以设置前缀,如下:

<resultMap id="selectAllResultMap" type="Product">
 <id column="id" property="id"/>
 <result column="name" property="name"/>
 <!--
        columnPrefix    列名前缀
        property        属性前缀
        javaType        需要封装到的对象的类型
 -->
 <association columnPrefix="b_" property="brand" javaType="Brand">
     <result column="id" property="id"/>
     <result column="name" property="name"/>
 </association>
</resultMap>

七、单向多对多(存在中间表)

  1. 保存
@Data
public class Teacher {
    private Long id;
    private String name;
}
@Data
public class Student {
    private Long id;
    private String name;
    List<Teacher> teachers = new ArrayList<>();
}

步骤:插入学生和老师,获取它们的id,将id插入中间表

  1. 查询
<resultMap id="baseResultMap" type="Student">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <!--
        collection 针对关联属性配置,集合类型
    -->
    <collection property="teachers" column="id"
                select="cn.regex.ims.mapper.TeacherMapper.selectById"/>
</resultMap>
<select id="selectById" resultMap="baseResultMap">
    SELECT id, name FROM student WHERE id = #{id}
</select>
<select id="selectById" resultType="Teacher">
    select t.id, t.name
    from teacher_student ts
             join teacher t on ts.teacher_id = t.id
    where ts.student_id = #{id};
</select>
  1. 删除

步骤:先删除中间表数据,再删除其它表数据(避免存在外键约束导致抛异常)

拓展

  1. 删除可分为硬删除和软删除,硬删除即将数据从磁盘中删除掉,而软删除又叫逻辑删除,仅仅只是将数据标记为已删除状态
  2. 我们为什么要使用缓存?有什么好处?第一,可以提升查询速度,提高用户体验性;减轻数据库查询压力。
  3. 什么对象适合放在缓存中?经常查询的数据,以及很少被修改的数据,即读远大于写的数据。