三.MyBatis篇
1.简介
MyBatis 是一个半自动化的 ORM(对象关系映射)框架,允许开发者手动编写 SQL 语句,从而能够更好地控制 SQL 的执行,提高性能。MyBatis 可以将 SQL 语句与 Java 代码分离,存储在 XML 文件或者注解中,让代码结构更清晰。
2.初步使用
1.导入依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
2.创建Do对象
此处为基本数据对象
@Data
public class EmpDo {
private Long id;
private S tring name;
private Integer age;
private String email;
private String gender;
private String address;
private BigDecimal salary;
}
3.Dao层创建Mapper接口
@Mapper注解告诉mybatis这是一个Mapper接口
@Mapper
public interface EmpMapper {
EmpDo getEmpById(Integer id);
}
4.生成xml配置文件
namespace写Mapper对象的全类名
<?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.zhang.mybatis.dao.EmpMapper">
//此处添加sql方法
</mapper>
还需要在application.properties中指明xml配置文件的位置
mybatis.mapper-locations=classpath:mapper/**.xml
5.编写sql
<select id="getEmpById" resultType="com.zhang.mybatis.bean.EmpDo">
select * from employee where id = #{id}
</select>
<insert id="addEmp">
insert into employee(last_name,email,gender) values (#{lastName},#{email},#{gender})
</insert>
<update id="updateEmp">
update employee set last_name = #{lastName},email = #{email},gender = #{gender} where id = #{id}
</update>
<delete id="deleteEmpById">
delete from employee where id = #{id}
</delete>
- id对应Mapper对象的方法名
- resultType对应返回值
- 括号间为sql语言
接下来就可以直接调用Mapper方法了
6.开启自增id
<insert id="addEmp" useGeneratedKeys="true" keyProperty="id"></insert>
- useGeneratedKeys表示是否开始自增id
- keyProperty指定自动生成的id在Do中对应的属性,把自动生成的id封装回Emp对象的id属性中
7.命名规则转化
java和mysql数据库采用了不同的命名规则:
- java采用驼峰命名(userName)
- 数据库采用蛇形命名(user_name)
在配置文件中开启命名规则自动转换,才能使名称一 一对应上
mybatis.configuration.map-underscore-to-camel-case=true
logging.level.com.atguigu.mybatis.mapper=debug//开SQL日志
8.细节
- 1. 每个Dao 接口 对应一个 XML 实现文件
- 2. Dao 实现类 是一个由 MyBatis 自动创建出来的代理对象
- 3. XML 中 namespace 需要绑定 Dao 接口 的全类名
- 4. XML 中使用 select、update、insert、delete 标签来代表增删改查
- 5. 每个 CRUD 标签 的 id 必须为Dao接口的方法名
- 6. 每个 CRUD标签的 resultType 是Dao接口的返回值类型全类名
- 7. 未来遇到复杂的返回结果封装,需要指定 resultMap 规则
- 8. 以后 xxxDao 我们将按照习惯命名为 xxxMapper,这样更明显的表示出 持久层是用 MyBatis 实现的
3.参数处理
1.占位符#{}和${}的区别
select * from employee where id = #{id}
select * from employee where id = ${id}
#{} :这是MyBatis中预编译的占位符,用于将参数值安全地插入到SQL语句中。在SQL执行前,MyBatis会将#{} 替换为?,然后使用PreparedStatement来设置参数值,有效防止了sql注入问题。
Connection connection = dataSource.getConnection();
String sql = "select * from employee where id=?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setString(1, "admin");
${}:这是字符串替换占位符,MyBatis会直接将${} 替换为实际的参数值,而不进行预编译,会引发sql注入问题。
只有参数位置动态取值才可以用#{},jdbc层面表名等位置不支持预编译,只能用${}。
总结:能用#{}大括号的地方就用,不能用#{}的地方用${}
2.单参数取值
- 对象取值:直接写属性名,不要写对象名
select * from employee where id = #{name}
list取值:使用索引
select * from employee where id = #{ids[5]}
map取值: 使用key值
select * from employee where id = #{myId}
3.多参数取值
新版mybatis支持直接使用#{参数名}
老版mybatis需要在参数前使用@Param指定参数名
Emp getEmployByIdAndName(@Param("id") Long id, @Param("empName") Employee emp)
select * from Employee where id = #{id} and name = #{emp.name}
最佳实践:即使只有一个参数,也用 @Param 指定参数名
4.返回值-结果封装
1.一般返回:
<select id="getEmpById" resultType="com.zhang.mybatis.bean.EmpDo">
select * from employee where id = #{id}
</select>
resultType规定返回值类型
2.返回map:
mapper方法中使用@Mapper(“”)指定key值,key默认从对象属性值中取
@Mapper("id")//将id作为key值
Map<Integer,Emp> getAllMap();
返回值的value实际上不是指定类(Emp),而是一个HashMap,
resultType不写map,写指定类,value值才为指定类
3.自定义结果集- ResultMap
数据库的字段 如果和 Bean的属性 不能一一对应,有两种办法
- 1、如果符合驼峰命名,则开启驼峰命名规则
- 2、编写自定义结果集(ResultMap) 进行封装
<resultMap id="EmpResultMap" type="com.atguigu.mybatis.entity.Employee">
<id column="emp_id" property="empId"/>
<result column="emp_name" property="empName"/>
<result column="emp_salary" property="empSalary"/>
</resultMap>
默认封装规则(resultType):JavaBean中的属性名 去数据库表中 找对应列名的值,一一映射封装。
自定义封装规则(resultMap):明确指定每一列如何封装到JavaBean中
- id 标签:必须指定主键列映射规则
- result 标签:指定普通列映射规则
- collection 标签:指定自定义集合封装规则
- association 标签:指定自定义对象封装规则
<resultMap id="EmpRM" type="com.zhang.mybatis.bean.EmpDo">
<!--声明主键映射规则-->
<id property="id" column="id"></id>
<!--声明普通列映射规则-->
<result property="name" column="name"></result>
<result property="age" column="age"></result>
</resultMap>
在方法中根据ResultMap的id使用对照规则
<select id="getEmpById" resultMap="EmpRM">
select * from employee where id = #{id}
</select>
4.关联查询
一对一关系封装
- association 标签:指定自定义对象封装规则,一般用户联合查询一对一关系的封装。比如一个用户对应一个订单
- javaType:指定关联的Bean的类型
- select:指定分步查询调用的方法
- column:指定分步查询传递的参数列
按照id查询订单以及下单的客户信息
<select id="getOrderById" resultType="com.zhang.mybatis.bean.Order">
select * from orders
left join customer on orders.customer_id = customer.id
where orders.id = #{id}
</select>
使用默认封装(resultType)无法将连接的对象正确装填(customer将返回为null)
Order(id=1, address=chengdu, amount=5000, customerId=1, customer=null)
这时候要在自定义封装(resultMap)中使用association标签
<resultMap id="OrderRM" type="com.zhang.mybatis.bean.Order">
<id property="id" column="id"></id>
<result property="address" column="address"></result>
<result property="amount" column="amount"></result>
<result column="customer_id" property="customerId"></result>
<association property="customer" javaType="com.zhang.mybatis.bean.Customer">
<id property="id" column="id"></id>
<result property="name" column="name"></result>
</association>
</resultMap>
左连接时,association标签中定义了右表的封装关系
一对多关系封装
- collection 标签:指定自定义对象封装规则,一般用户联合查询一对一关系的封装。比如一个用户对应一个订单
- ofType:指定集合中每个元素的类型
- select:指定分步查询调用的方法
- column:指定分步查询传递的参数列
按照id查询客户以及下的所有订单
<resultMap id="CustomerRM" type="com.zhang.myb atis.bean.Customer">
<id property="id" column="id"></id>
<result property="name" column="name"></result>
<collection property="orders" ofType="com.zhang.mybatis.bean.Order">
<id property="id" column="id"></id>
<result property="address" column="address"></result>
<result property="amount" column="amount"></result>
<result column="customer_id" property="customerId"></result>
</collection>
</resultMap>
property对应集合,ofType对应单一类
5.分步查询
- 在 association 和 collection 的封装过程中,可以使用 select + column 指定分步查询逻辑
- select:指定分步查询调用的方法
- column:指定分步查询传递的参数
- 传递单个:直接写列名,表示将这列的值作为参数传递给下一个查询
- 传递多个:column=”{prop1=col1,prop2=col2}”,下一个查询使用prop1、prop2取值
1.手动分步查询
//按照id查询订单
Order order = orderDao.getOrderById(1);
//使用订单中的用户id查询用户
Customer customer = OrderDao.getCustomerById(order.getCustomerId());
//将用户设置到订单中
order.setCustomer(customer);
2.自动分步查询
<resultMap id="OrderRM" type="com.zhang.mybatis.bean.Order">
<id property="id" column="id"></id>
<result property="address" column="address"></result>
<result property="amount" column="amount"></result>
<result property="customerId" column="customer_id"></result>
<collection property="customers"
select="com.zhang.mybatis.dao.OrderDao.getCustomerById"
column="customer_id">
</collection>
</resultMap>
- 在collection或者association中使用select属性告诉mybatis在装填该项时使用另一个查询方法
- column指定传递的参数,当出现多个参数时,使用以下格式
column="{userName=user_name,userAge=user_age}"
3.超级分步问题
问题:当实体类之间存在循环引用时,例如 A 类关联 B 类,B 类又关联 A 类,分步查询可能会陷入无限循环,最终导致栈溢出错误。
解决办法:使用嵌套查询,把分步查询改成嵌套查询,在一个 SQL 语句里完成所有关联查询。不过这样可能会让 SQL 语句变得复杂,降低可读性。
6.延迟加载
- 分步查询 有时候并不需要立即运行,我们希望在用到的时候再去查询,可以开启延迟加载的功能
- 全局配置:
- mybatis.configuration.lazy-loading-enabled=true//开启 MyBatis 的延迟加载特性。
- mybatis.configuration.aggressive-lazy-loading=false//禁用积极延迟加载。
- 全局配置:
7.动态sql
1.简介
- 动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
- 使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。
mybatis中动态sql 默认事务回滚开启
- 真正和事务相关的不是 MyBatis,而是它运行所在的环境。
- 比如在 Spring 中,
@Transactional
注解会开启事务,出现 运行时异常 (RuntimeException) 或 Error 时,默认会自动回滚。 - MyBatis 在 原生使用 SqlSession 时,也会在你没有显式调用
commit()
之前,默认处于一个事务中(不自动提交)。如果执行出错,它会回滚。
- 动态 SQL 只是拼接 SQL,不影响事务;
- MyBatis 的
SqlSession
默认不会自动提交事务,只有在调用commit()
时才提交; - 如果在执行过程中抛出异常(比如 SQL 拼接错误、数据库约束异常),事务就会回滚;
- 在 Spring + MyBatis 中,如果用了
@Transactional
,Spring 会接管事务管理,默认规则同样是运行时异常触发回滚。
2.标签
if、where 标签
- 需求:按照 empName 和 empSalary 查询员工。
- 注意:前端不一定携带所有条件
<select id="selectEmployeeByCondition" resultType="employee">
select emp_id,emp_name,emp_salary from t_emp
<where>
<if test="empName != null">
or emp_name=#{empName}
</if>
<if test="empSalary !=null">
or emp_salary>#{empSalary}
</if>
</where>
</select>
set标签
- 需求:修改 empName 和 empSalary
- 注意:前端不一定携带所有条件
<update id="updateEmployeeDynamic">
update t_emp
<set>
<if test="empName != null">
emp_name=#{empName},
</if>
<if test="empSalary < 3000">
emp_salary=#{empSalary},
</if>
</set>
where emp_id=#{empId}
</update>
trim标签
trim 可以实现 set 去掉多余逗号,where 去掉多余and/or 的功能, 不过写起来比较麻烦
<select id="selectEmployeeByConditionByTrim" resultType="com.atguigu.mybatis.entity.Employee">
select emp_id,emp_name,emp_age,emp_salary,emp_gender from t_emp
<trim prefix="where" suffixOverrides="and|or">
<if test="empName != null">
emp_name=#{empName} and
</if>
<if test="empSalary > 3000">
emp_salary>#{empSalary} and
</if>
<if test="empAge <= 20">
emp_age=#{empAge} or
</if>
<if test="empGender=='male'">
emp_gender=#{empGender}
</if>
</trim>
</select>
当标签中存在内容时,为该内容添加前缀(prefix)或者后缀(suffix)
<trim prefix="前缀" suffix="后缀"></trim>
标签体中最终生成的字符串如果以指定前缀(prefixOverrides)或者后缀(suffixOverrides)开始,就将该前缀覆盖为空串
<trim prefixOverrides="前缀" suffixOverrides="后缀"></trim>
choose/when/otherwise标签
- 在多个分支条件中,仅执行一个。
<select id="selectEmployeeByConditionByChoose"
resultType="com.atguigu.mybatis.entity.Employee">
select emp_id,emp_name,emp_salary from t_emp where
<choose>
<when test="empName != null">emp_name=#{empName}</when>
<when test="empSalary < 3000">emp_salary < 3000</when>
<otherwise>1=1</otherwise>
</choose>
</select>
<choose>:作为根标签,包裹多个 <when> 和一个可选的 <otherwise> 标签,用于表示一组条件选择逻辑。
<choose>
<when test="condition1">
<!-- SQL 片段 1 -->
</when>
<when test="condition2">
<!-- SQL 片段 2 -->
</when>
<!-- 可以有多个 <when> 标签 -->
<otherwise>
<!-- 默认 SQL 片段 -->
</otherwise>
</choose>
foreach标签
- 用来遍历,循环;常用于批量插入场景;批量单个SQL
<!--
collection属性:要遍历的集合
item属性:遍历集合的过程中能得到每一个具体对象,在item属性中设置一个名字,将来通过这个名字引用遍历出来的对象
separator属性:指定当foreach标签的标签体重复拼接字符串时,各个标签体字符串之间的分隔符
open属性:指定整个循环把字符串拼好后,字符串整体的前面要添加的字符串
close属性:指定整个循环把字符串拼好后,字符串整体的后面要添加的字符串
index属性:这里起一个名字,便于后面引用
遍历List集合,这里能够得到List集合的索引值
遍历Map集合,这里能够得到Map集合的key
-->
<foreach collection="empList" item="emp" separator="," open="values" index="myIndex">
(#{emp.empName},#{myIndex},#{emp.empSalary},#{emp.empGender})
</foreach>
- IN子句:查询多ID记录,生成如 (id1, id2)。
<select id="getOrdersByIds" resultMap="OrderRM">
SELECT * FROM orders
WHERE id IN
<foreach item="item" index="index" collection="orderIds"
open="(" separator="," close=")">
#{item}
</foreach>
</select>
- 批量插入:生成多个 VALUES 子句。
<insert id="batchInsertOrders">
INSERT INTO orders (address, amount, customer_id)
VALUES
<foreach item="order" index="index" collection="orderList"
separator=",">
(#{order.address}, #{order.amount}, #{order.customerId})
</foreach>
</insert>
- 批量更新:结合 CASE WHEN 语句。
<update id="batchUpdateOrders">
<foreach collection="emps" item="e" separator=";">
update t_emp set emp_name = #{e.empName} where id = #{e.id}
</foreach>
</update>
sql片段
•抽取可以复用的SQL片段
<sql id="empColumn">
emp_id,emp_name,emp_age,emp_salary,emp_gender
</sql>
<select id="getEmp" resultType="com.atguigu.mybatis.entity.Employee">
select
<include refid="empColumn"/>
from `t_emp` where id = #{id}
</select>
8.特殊字符
•以后在xml中,以下字符需要用转义字符,不能直接写
原始字符 | 转义字符 |
& | & |
< | < |
> | > |
“ | " |
‘ | ' |
9.MyBais 扩展
1.缓存机制
- MyBatis 拥有二级缓存机制:
- 一级缓存默认开启; 事务级别:当前事务共享
- 二级缓存需要手动配置开启:所有事务共享
- 缓存中有就不用查数据库;
- L1~LN:N级缓存
- 数字越小离我越近,查的越快。存储越小,造价越高。
- 数字越大离我越远,查的越慢。存储越大,造价越低。
一级缓存
- 作用域:一级缓存也称为会话(SqlSession)级缓存,它的作用域是单个 SqlSession。在同一个 SqlSession 中,执行相同的 SQL 查询时,MyBatis 会优先从一级缓存中获取数据,而不是再次查询数据库。
- 原理:当一个 SqlSession 执行查询操作时,MyBatis 会将查询结果存储在该 SqlSession 的缓存中。如果后续再次执行相同的查询(相同的 SQL 语句和参数),MyBatis 会直接从缓存中返回结果,而不会再次访问数据库。
- 失效情况:当 SqlSession 执行插入、更新、删除操作,或者调用 SqlSession 的 clearCache() 方法时,一级缓存会被清空。
示例代码:
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询
User user1 = mapper.getUserById(1);
// 第二次查询,使用缓存
User user2 = mapper.getUserById(1);
System.out.println(user1 == user2); // 输出 true
}
二级缓存
- 作用域:二级缓存也称为命名空间(Mapper)级缓存,它的作用域是同一个命名空间(Mapper 接口)。不同的 SqlSession 可以共享二级缓存中的数据,只要它们操作的是同一个 Mapper 接口。
- 原理:MyBatis 的二级缓存是基于 Mapper 命名空间的,每个 Mapper 都有自己独立的二级缓存。当一个 SqlSession 执行查询操作并将结果存储在二级缓存中后,其他 SqlSession 执行相同的查询时,可以直接从二级缓存中获取数据。
- 开启方式:要开启二级缓存,需要在 Mapper XML 文件中添加 标签,或者在 Mapper 接口上添加 @CacheNamespace 注解。
- 失效情况:当 Mapper 对应的 SqlSession 执行插入、更新、删除操作时,该 Mapper 的二级缓存会被清空。
示例代码
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache />
<select id="getUserById" resultType="com.example.entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
try (SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession()) {
UserMapper mapper1 = session1.getMapper(UserMapper.class);
UserMapper mapper2 = session2.getMapper(UserMapper.class);
// 第一次查询,存入二级缓存
User user1 = mapper1.getUserById(1);
// 第二次查询,从二级缓存获取
User user2 = mapper2.getUserById(1);
System.out.println(user1 == user2); // 输出 false,但数据内容相同
}
2.插件机制
- MyBatis 底层使用 拦截器机制提供插件功能,方便用户在SQL执行前后进行拦截增强。
- 拦截器:Interceptor
- 拦截器可以拦截 四大对象 的执行
- ParameterHandler(参数处理器):处理SQL的参数对象
- ResultSetHandler(结果集处理器):处理SQL的返回结果集
- StatementHandler(语句处理器):数据库的处理对象,用于执行SQL语句
- Executor(执行器):MyBatis的执行器,用于执行增删改查操作
10.PageHelper
- PageHelper 是可以用在 MyBatis 中的一个强大的分页插件
- 分页插件就是利用MyBatis 插件机制,在底层编写了 分页Interceptor,每次SQL查询之前会自动拼装分页数据
- select * from emp limit 0,10
- 分页重点:
- 前端 第1页: limit 0,10
- 前端 第2页: limit 10,10
- 前端 第3页: limit 20,10
- 前端 第N页:limit startIndex,pageSize
- 计算规则: pageNum = 1, pageSize = 10
- startIndex = (pageNum – 1)*pageSize
官网:https://pagehelper.github.io
使用
在 pom.xml 中添加如下依赖:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>最新版本</version>
</dependency>
配置拦截器插件
@MapperScan("com.atguigu.mybatis.mapper") //批量只扫描mapper
@Configuration
public class MyBatisConfig {
@Bean
PageInterceptor pageInterceptor(){
//1、创建 分页插件 对象
PageInterceptor interceptor = new PageInterceptor();
//2、设置 参数
//.......
Properties properties = new Properties();
properties.setProperty("reasonable", "true");
interceptor.setProperties(properties);
return interceptor;
}
}
测试类
@SpringBootTest
public class PageTest {
@Autowired
EmpService empService;
@Test
void test02(){
//后端收到前端传来的页码
//响应前端需要的数据:
//1、总页码、总记录数
//2、当前页码
//3、本页数据
PageHelper.startPage(3,5);
// 紧跟着 startPage 之后 的方法就会执行的 SQL 分页查询
List<Emp> all = empService.getAll();
System.out.println("============");
//以后给前端返回它
PageInfo<Emp> info = new PageInfo<>(all);
//当前第几页
System.out.println("当前页码:"+info.getPageNum());
//总页码
System.out.println("总页码:"+info.getPages());
//总记录
System.out.println("总记录数:"+info.getTotal());
//有没有下一页
System.out.println("有没有下一页:"+info.isHasNextPage());
//有没有上一页
System.out.println("有没有上一页:"+info.isHasPreviousPage());
//本页数据
System.out.println("本页数据:"+info.getList());
}
@Test
void test01(){
/**
* 原理:拦截器;
* 原业务底层:select * from emp;
* 拦截做两件事:
* 1)、统计这个表的总数量
* 2)、给原业务底层SQL 动态拼接上 limit 0,5;
*
* ThreadLocal: 同一个线程共享数据
* 1、第一个查询从 ThreadLocal 中获取到共享数据,执行分页
* 2、第一个执行完会把 ThreadLocal 分页数据删除
* 3、以后的查询,从 ThreadLocal 中拿不到分页数据,就不会分页
*
*/
PageHelper.startPage(3,5);
// 紧跟着 startPage 之后 的方法就会执行的 SQL 分页查询
List<Emp> all = empService.getAll();
for (Emp emp : all) {
System.out.println(emp);
}
System.out.println("===============");
List<Emp> all1 = empService.getAll();
System.out.println(all1.size());
}
}
@GetMapping("/emp/page")
public PageInfo getPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum){
PageHelper.startPage(pageNum, 5);
List<Emp> all = empService.getAll();
return new PageInfo<>(all);
}