本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一、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 >= #{minAge} </if> <if test="maxAge != null"> AND age <= #{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>
五、关系概述
-
关联关系:A依赖B,并将B作为成员变量,则A和B存在关联关系。
-
关联关系分类
-
判断对象的关系
- 从对象的实例上看
- 根据对象的属性
- 根据具体需求
六、单向多对一
- 保存
@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>
注意事项
- 关联属性需要通过 "." 进行取值,即#{brand.id}
- 需要先保存关联属性对应的对象再保存其它,即先保存brand对象再保存product对象,否则添加的product将没有对应的关联id
- 额外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>
七、单向多对多(存在中间表)
- 保存
@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插入中间表
- 查询
<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>
- 删除
步骤:先删除中间表数据,再删除其它表数据(避免存在外键约束导致抛异常)
拓展
- 删除可分为硬删除和软删除,硬删除即将数据从磁盘中删除掉,而软删除又叫逻辑删除,仅仅只是将数据标记为已删除状态
- 我们为什么要使用缓存?有什么好处?第一,可以提升查询速度,提高用户体验性;减轻数据库查询压力。
- 什么对象适合放在缓存中?经常查询的数据,以及很少被修改的数据,即读远大于写的数据。