MyBatis 教程完全指南:从入门配置到企业级动态 SQL 实战攻略
作者:西凉的悲伤
前言
本篇文章目标:
- 从零理解 MyBatis 的设计思想
- 掌握 Spring Boot 集成 MyBatis 的完整流程
- 理解 Mapper、XML、参数映射机制
- 掌握动态 SQL 标签使用
- 结合真实企业开发场景完成 CRUD、批量操作、多参数查询等操作
示例环境:
- JDK 8+
- Spring Boot 2.x
- MyBatis 3.x
- MySQL 8.x
一、什么是 MyBatis?
1.1 MyBatis 简介
MyBatis 是一个优秀的 持久层框架(Persistence Framework)。
它通过 XML 或注解的方式,将 Java 方法与 SQL 语句进行映射,使开发人员可以更加灵活地操作数据库。
简单来说:
MyBatis 负责帮助我们管理 JDBC 的连接、参数设置、结果集处理,但是 SQL 语句仍然由开发人员自己编写。
传统 JDBC:
Connection conn = DriverManager.getConnection(url,user,password);
PreparedStatement ps =
conn.prepareStatement(
"select * from student where id=?"
);
ps.setLong(1,id);
ResultSet rs = ps.executeQuery();
while(rs.next()){
}存在的问题:
- 大量重复代码
例如:
- 获取连接
- 创建 Statement
- 设置参数
- 关闭资源
- ResultSet 转对象
- SQL 和 Java 代码耦合严重
- 查询结果需要手动封装
MyBatis 对 JDBC 进行了封装。
使用 MyBatis 后:
Mapper:
Student selectById(Long id);
XML:
<select id="selectById">
select *
from student
where id=#{id}
</select>调用:
Student student = studentMapper.selectById(1L);
MyBatis 自动完成:
Java对象 | | Mapper接口 | | MyBatis | | SQL执行 | | ResultSet | | Java对象
1.2 MyBatis 解决了什么问题?
(1)减少 JDBC 重复代码
JDBC:
try(Connection conn = dataSource.getConnection()){
}
catch(Exception e){
}MyBatis:
mapper.selectList();
(2)SQL 与 Java 解耦
以前:
String sql = "select * from student where name=?";
修改 SQL 需要修改 Java。
MyBatis:
Mapper.java
↓
StudentMapper.xmlSQL 可以独立维护。
(3)方便动态 SQL
例如:
查询学生:
如果传入姓名:
where name='Tom'
如果没有姓名:
查询全部
MyBatis:
<if test="name != null">
and name=#{name}
</if>(4)强大的对象映射能力
数据库:
create_time
Java:
private Date createTime;
MyBatis 可以自动转换:
create_time
↓
createTime二、MyBatis 核心组件
MyBatis 主要包含以下几个核心组件:
| 组件 | 作用 |
|---|---|
| SqlSessionFactory | 创建 SqlSession |
| SqlSession | 执行 SQL |
| Mapper 接口 | 定义数据库操作方法 |
| Mapper XML | 保存 SQL |
| Executor | SQL 执行器 |
| ResultMap | 结果映射 |
整体执行流程:
Controller
↓
Service
↓
Mapper接口
↓
Mapper.xml
↓
SqlSession
↓
Executor
↓
JDBC
↓
Database三、Spring Boot 集成 MyBatis
3.1 创建 Spring Boot 项目
创建 Maven 项目。
目录结构:
mybatis-demo ├── controller ├── service ├── mapper │ └── StudentMapper.java ├── entity │ └── Student.java ├── resources │ ├── mapper │ └── StudentMapper.xml └── application.yml
3.2 Maven 引入依赖
MyBatis Starter
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>
mybatis-spring-boot-starter
</artifactId>
<version>2.3.1</version>
</dependency>作用:
Spring Boot 自动完成:
- SqlSessionFactory 创建
- Mapper 扫描
- MyBatis 配置加载
MySQL 驱动
<dependency>
<groupId>com.mysql</groupId>
<artifactId>
mysql-connector-j
</artifactId>
</dependency>Lombok(可选)
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
3.3 application.yml 配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations:
classpath:mapper/*.xml
type-aliases-package:
com.demo.entity
configuration:
map-underscore-to-camel-case: true配置解释
mapper-locations
指定 XML 文件位置。
例如:
resources
└── mapper
└── StudentMapper.xml配置:
mapper-locations: classpath:mapper/*.xml
表示加载:
classpath:/mapper/*.xml
type-aliases-package
指定实体包。
例如:
package com.demo.entity;
public class Student{
}配置:
type-aliases-package: com.demo.entity
之后 XML 可以:
resultType="Student"
不用:
resultType="com.demo.entity.Student"
map-underscore-to-camel-case
开启驼峰转换。
数据库:
create_time
Java:
private Date createTime;
开启:
map-underscore-to-camel-case:true
MyBatis 自动转换。
3.4 Mapper 扫描
方式一:
启动类添加:
@SpringBootApplication
@MapperScan("com.demo.mapper")
public class Application {
public static void main(String[] args){
SpringApplication.run(
Application.class,args
);
}
}作用:
扫描:
@Mapper
public interface StudentMapper{
}方式二:
每个 Mapper 添加:
@Mapper
public interface StudentMapper{
}企业项目更推荐:
@MapperScan
统一管理。
四、创建示例表
后续所有 MyBatis 示例都基于 student 表。
CREATE TABLE student
(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
age INT,
gender VARCHAR(10),
birthday DATE,
create_time DATETIME
);插入测试数据:
INSERT INTO student
(name,age,gender,birthday,create_time)
VALUES
('Tom',18,'M','2005-01-01',now()),
('Jack',20,'M','2003-01-01',now()),
('Lucy',19,'F','2004-01-01',now());五、MyBatis 第一个查询
5.1 创建实体
@Data
public class Student {
private Long id;
private String name;
private Integer age;
private String gender;
private Date birthday;
private Date createTime;
}5.2 创建 Mapper
@Mapper
public interface StudentMapper {
Student selectById(Long id);
}5.3 XML
resources/mapper/StudentMapper.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">
<mapper namespace="com.demo.mapper.StudentMapper">
<select id="selectById"
parameterType="java.lang.Long"
resultType="com.demo.entity.Student">
select
id,
name,
age,
gender,
birthday,
create_time
from student
where id=#{id}
</select>
</mapper>六、MyBatis Mapper 参数详解
这是企业开发中非常容易出现问题的地方。
6.1 单个普通参数
Mapper:
Student selectById(Long id);
XML:
where id=#{id}为什么可以直接使用?
因为 MyBatis 对单参数进行了特殊处理。
实际上:
#{id}会绑定:
方法参数
6.2 多个普通参数
例如:
Student query(
String name,
Integer age
);XML:
select *
from student
where name=#{name}
and age=#{age}运行:
报错:
Parameter 'name' not found
原因:
MyBatis 默认不知道:
第一个参数叫什么 第二个参数叫什么
它默认名称:
arg0 arg1 param1 param2
6.3 使用 @Param
修改:
Student query(
@Param("name")
String name,
@Param("age")
Integer age
);XML:
where name=#{name}
and age=#{age}正常执行。
6.4 什么情况下可以省略 @Param?
| 参数类型 | 是否需要 |
|---|---|
| 单个普通参数 | 不需要 |
| Java Bean对象 | 不需要 |
| Map | 不需要 |
| 多个普通参数 | 需要 |
| 多个集合参数 | 建议使用 |
例如对象:
Student query(Student student);
XML:
#{name}
#{age}可以直接访问。
原因:
MyBatis 会读取对象属性。
七、MyBatis parameterType 与 resultType 详解
在 MyBatis XML 中,最常见的两个属性:
parameterType resultType
很多初学者容易混淆。
简单理解:
| 属性 | 作用 | 方向 |
|---|---|---|
| parameterType | 指定传入 SQL 的 Java 参数类型 | Java → SQL |
| resultType | 指定 SQL 返回结果转换成什么 Java 类型 | SQL → Java |
如下:
Controller
↓
Service
↓
Mapper 方法参数
↓
parameterType
↓
SQL执行
↓
数据库结果
↓
resultType
↓
Java对象7.1 parameterType 使用
1. 基础类型
Mapper:
Student selectById(Long id);
XML:
<select id="selectById"
parameterType="java.lang.Long">
select *
from student
where id=#{id}
</select>这里:
parameterType="java.lang.Long"
表示:
SQL 接收一个 Long 类型参数。
2. String 类型
Mapper:
List<Student> selectByName(String name);
XML:
<select id="selectByName"
parameterType="java.lang.String">
select *
from student
where name=#{name}
</select>调用:
studentMapper.selectByName("Tom");
最终 SQL:
select * from student where name='Tom'
3. Java 对象作为参数
实际开发中最常见。
定义查询对象:
@Data
public class StudentQuery {
private String name;
private Integer minAge;
private Integer maxAge;
private Date startBirthday;
private Date endBirthday;
}Mapper:
List<Student> query(StudentQuery query);
XML:
<select id="query"
parameterType="com.demo.entity.StudentQuery">
select *
from student
where name=#{name}
</select>MyBatis 会自动读取:
query.getName()
7.2 resultType 使用
resultType 表示:
SQL 返回的数据转换为什么对象。
例如:
SQL:
select * from student where id=1
数据库返回:
| id | name | age |
|---|---|---|
| 1 | Tom | 18 |
resultType:
resultType="com.demo.entity.Student"
MyBatis 自动生成:
Student student=new Student();
student.setId(1);
student.setName("Tom");
student.setAge(18);7.3 resultType 自动映射规则
MyBatis 默认按照:
数据库字段
|
|
Java属性
匹配。
例如:
数据库:
create_time
Java:
private Date createTime;
开启:
mybatis: configuration: map-underscore-to-camel-case:true
自动转换:
create_time ↓ createTime
7.4 resultMap 手动映射
什么时候需要 resultMap?
场景:
1. 字段名称完全不同
数据库:
student_name
Java:
private String name;
无法自动匹配。
使用:
<resultMap id="studentMap"
type="com.demo.entity.Student">
<id column="id"
property="id"/>
<result column="student_name"
property="name"/>
</resultMap>查询:
<select id="select" resultMap="studentMap"> select id, student_name from student </select>
7.5 resultMap 与 resultType 区别
| resultType | resultMap | |
|---|---|---|
| 简单对象 | 推荐 | 不需要 |
| 字段名称一致 | 推荐 | 不需要 |
| 字段需要转换 | 不支持 | 支持 |
| 复杂关联查询 | 不适合 | 推荐 |
| 一对多 | 不支持 | 支持 |
企业开发:
80% 查询使用 resultType,复杂查询使用 resultMap。
八、MyBatis 动态 SQL
动态 SQL 是 MyBatis 最核心的功能之一。
主要标签:
| 标签 | 作用 |
|---|---|
| if | 条件判断 |
| where | 动态生成 where |
| trim | 自定义拼接 |
| set | 动态更新 |
| foreach | 循环 |
| choose | 类似 switch |
8.1 if 标签
场景
查询学生:
如果传姓名:
where name='Tom'
如果没有:
查询全部
Mapper:
List<Student> query(Student student);
XML:
<select id="query"
resultType="Student">
select *
from student
where 1=1
<if test="name != null">
and name=#{name}
</if>
</select>判断不为空字符串
很多开发人员会犯错误:
错误:
<if test="name != null"> </if>
因为:
name=""
也会进入。
正确:
<if test="name != null
and name != ''">
and name=#{name}
</if>含义:
必须满足:
name存在 并且 name不是空字符串
8.2 where 标签
为什么需要 where?
例如:
<select>
select *
from student
<if test="name!=null">
and name=#{name}
</if>
</select>如果 name 有值:
生成:
select * from student and name='Tom'
错误。
使用 where:
<select id="query"
resultType="Student">
select *
from student
<where>
<if test="name!=null and name!=''">
and name=#{name}
</if>
<if test="age!=null">
and age=#{age}
</if>
</where>
</select>如果:
name="Tom" age=null
生成:
select * from student where name='Tom'
如果:
name=null age=18
生成:
select * from student where age=18
where 会自动:
- 删除第一个 and
- 判断是否需要 where
8.3 trim 标签
where 本质上就是 trim。
例如:
<where> ... </where>
等价:
<trim prefix="where" prefixOverrides="and|or"> </trim>
trim 常见用途一:动态 where
<trim prefix="where"
prefixOverrides="and|or">
<if test="name!=null">
and name=#{name}
</if>
</trim>trim 常见用途二:动态 insert
例如:
实体:
@Data
public class Student {
private String name;
private Integer age;
}插入:
<insert id="insert">
insert into student
<trim prefix="("
suffix=")"
suffixOverrides=",">
<if test="name!=null">
name,
</if>
<if test="age!=null">
age,
</if>
</trim>
values
<trim prefix="("
suffix=")"
suffixOverrides=",">
<if test="name!=null">
#{name},
</if>
<if test="age!=null">
#{age},
</if>
</trim>
</insert>如果:
name="Tom" age=null
生成:
insert into student ( name ) values ( 'Tom' )
8.4 set 标签
用于动态 update。
例如:
错误:
update student set name=?, age=?
如果 age 不传:
update student set name=?, where id=?
SQL错误。
使用 set:
<update id="update">
update student
<set>
<if test="name!=null">
name=#{name},
</if>
<if test="age!=null">
age=#{age},
</if>
</set>
where id=#{id}
</update>set 会自动删除最后一个逗号。
8.5 foreach 标签
用于:
- in 查询
- 批量插入
- 批量删除
- 批量更新
List 查询
Mapper:
List<Student> selectByIds(
@Param("ids")
List<Long> ids
);
XML:
<select id="selectByIds">
select *
from student
where id in
<foreach collection="ids"
item="id"
open="("
close=")"
separator=",">
#{id}
</foreach>
</select>传入:
Arrays.asList(1L,2L,3L)
生成:
where id in (1,2,3)
九、MyBatis CRUD 实战
下面开始企业开发中最常见的 CRUD。
9.1 新增数据
普通 insert
Mapper:
int insert(Student student);
XML:
<insert id="insert">
insert into student
(
name,
age,
gender,
birthday
)
values
(
#{name},
#{age},
#{gender},
#{birthday}
)
</insert>9.2 动态 insertSelective
实际项目中经常使用。
例如:
对象:
Student student=new Student();
student.setName("Tom");
student.setAge(null);希望:
insert into student
(name)
values
('Tom')XML:
<insert id="insertSelective">
insert into student
<trim prefix="("
suffix=")"
suffixOverrides=",">
<if test="name!=null">
name,
</if>
<if test="age!=null">
age,
</if>
<if test="gender!=null">
gender,
</if>
<if test="birthday!=null">
birthday,
</if>
</trim>
values
<trim prefix="("
suffix=")"
suffixOverrides=",">
<if test="name!=null">
#{name},
</if>
<if test="age!=null">
#{age},
</if>
<if test="gender!=null">
#{gender},
</if>
<if test="birthday!=null">
#{birthday},
</if>
</trim>
</insert>9.3 insertSelective 与普通 insert 区别
| 方式 | 特点 |
|---|---|
| 普通 insert | 所有字段必须赋值 |
| insertSelective | 只插入非 null 字段 |
| 适合场景 | 字段很多、部分字段可选 |
例如:
用户表:
id username password nickname avatar email phone address create_time update_time
不可能每次所有字段都有值。
所以企业项目大量使用:
insertSelective
十、MyBatis 批量插入详解
在企业项目中,批量插入是非常常见的场景。
例如:
- 批量导入用户
- 批量同步订单
- 批量保存明细
- 批量保存日志
MyBatis 中常见的批量插入方式主要有:
- foreach 拼接 values
- 循环调用 insert
- 批量 Executor
实际开发中最常用:
foreach + 一条 insert SQL
10.1 foreach 批量插入
假设:
student 表:
CREATE TABLE student
(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
age INT,
gender VARCHAR(10),
birthday DATE
);实体:
@Data
public class Student {
private Long id;
private String name;
private Integer age;
private String gender;
private Date birthday;
}Mapper
int batchInsert(
@Param("list")
List<Student> list
);
XML
<insert id="batchInsert">
insert into student
(
name,
age,
gender,
birthday
)
values
<foreach collection="list"
item="item"
separator=",">
(
#{item.name},
#{item.age},
#{item.gender},
#{item.birthday}
)
</foreach>
</insert>传入:
List<Student> list = new ArrayList<>();
Student s1=new Student();
s1.setName("Tom");
s1.setAge(18);
Student s2=new Student();
s2.setName("Jack");
s2.setAge(20);
studentMapper.batchInsert(list);MyBatis 最终生成:
insert into student
(
name,
age
)
values
('Tom',18),
('Jack',20)一次 SQL 完成插入。
10.2 foreach 批量插入优势
优点
1. 性能高
例如:
插入 1000 条数据。
方式一:
for(Student s:list){
insert(s);
}执行:
1000次SQL
方式二:
foreach:
insert into student values (), (), ()
执行:
1次SQL
2. 减少数据库连接
数据库交互:
应用 | | 数据库
次数减少。
10.3 批量插入数量限制
但是:
foreach 不是无限制。
例如:
100000条数据
生成:
insert into student values (...), (...), (...)
SQL 可能超过:
max_allowed_packet
或者:
数据库解析压力过大。
实际企业:
一般:
500~1000条一次
例如:
List<List<Student>> 分批: 500条 500条 500条
十一、insertSelective 与批量插入区别
很多项目同时存在:
insertSelective batchInsert
二者解决的问题不同。
11.1 insertSelective
作用:
单条数据动态选择字段。
例如:
实体:
Student student=new Student();
student.setName("Tom");
student.setAge(null);SQL:
insert into student ( name ) values ( 'Tom' )
实现:
<insert id="insertSelective">
insert into student
<trim prefix="("
suffix=")"
suffixOverrides=",">
<if test="name!=null">
name,
</if>
<if test="age!=null">
age,
</if>
</trim>
values
<trim prefix="("
suffix=")"
suffixOverrides=",">
<if test="name!=null">
#{name},
</if>
<if test="age!=null">
#{age},
</if>
</trim>
</insert>11.2 批量插入
作用:
多条数据一次保存。
例如:
List<Student>
对应:
insert into student values (), (), ()
11.3 两者区别
| insertSelective | batchInsert | |
|---|---|---|
| 数据量 | 单条 | 多条 |
| 目的 | 动态字段 | 提高插入效率 |
| 核心标签 | if + trim | foreach |
| 典型场景 | 新增业务对象 | 批量导入 |
十二、ON DUPLICATE KEY UPDATE 详解
MySQL 提供:
ON DUPLICATE KEY UPDATE
作用:
如果插入数据违反唯一索引,则执行更新。
例如:
表:
CREATE TABLE user ( id bigint primary key, username varchar(50) unique, age int );
第一次:
insert into user
(username,age)
values
('Tom',18);数据库:
| id | username | age |
|---|---|---|
| 1 | Tom | 18 |
第二次:
insert into user
(username,age)
values
('Tom',20)
ON DUPLICATE KEY UPDATE
age=20;因为:
username=Tom 违反唯一索引
所以:
执行:
update user set age=20 where username='Tom'
12.1 MyBatis 示例
Mapper:
int insertOrUpdate(Student student);
XML:
<insert id="insertOrUpdate">
insert into student
(
id,
name,
age,
gender
)
values
(
#{id},
#{name},
#{age},
#{gender}
)
ON DUPLICATE KEY UPDATE
name=#{name},
age=#{age},
gender=#{gender}
</insert>12.2 企业使用场景
非常常见:
同步第三方数据
例如:
每天同步供应商:
供应商订单
第一次:
insert
第二次:
update
不需要:
先查询 再判断 再新增/修改
减少一次数据库交互。
十三、MyBatis 批量更新
批量更新常见两种方式:
- foreach 多条 update
- CASE WHEN 一条 update
13.1 foreach 批量更新
Mapper:
int batchUpdate(
@Param("list")
List<Student> list
);XML:
<update id="batchUpdate">
<foreach collection="list"
item="item"
separator=";">
update student
set
name=#{item.name},
age=#{item.age}
where id=#{item.id}
</foreach>
</update>最终:
update student set name='Tom' where id=1; update student set name='Jack' where id=2;
13.2 CASE WHEN 批量更新
例如:
需要:
id=1 修改 Tom id=2 修改 Jack id=3 修改 Lucy
SQL:
update student set name = case id when 1 then 'Tom' when 2 then 'Jack' when 3 then 'Lucy' end where id in(1,2,3);
MyBatis:
<update id="batchUpdate">
update student
set name =
case id
<foreach collection="list"
item="item">
when #{item.id}
then #{item.name}
</foreach>
end
where id in
<foreach collection="list"
item="item"
open="("
close=")"
separator=",">
#{item.id}
</foreach>
</update>13.3 两种批量更新比较
| foreach update | CASE WHEN | |
|---|---|---|
| SQL数量 | 多条 | 一条 |
| 性能 | 一般 | 高 |
| 实现难度 | 简单 | 复杂 |
| 适合数据量 | 少量 | 大量 |
企业:
几十条:
foreach
几千条:
CASE WHEN
十四、MyBatis 多参数查询实战
实际项目中,一个查询方法经常包含多个类型参数。
例如:
查询学生:
条件:
- 一个查询对象
- 一个 String
- 一个 List
- 一个 Integer
- 一个日期范围
14.1 创建查询对象
@Data
public class StudentQuery {
/**
* 开始日期
*/
private Date startBirthday;
/**
* 结束日期
*/
private Date endBirthday;
private String gender;
}14.2 Mapper
List<Student> queryStudent(
@Param("query")
StudentQuery query,
@Param("name")
String name,
@Param("ids")
List<Long> ids,
@Param("age")
Integer age,
@Param("limit")
Integer limit
);这里为什么全部加 @Param?
因为:
参数超过一个。
MyBatis 不知道:
query name ids age limit
分别是什么。
14.3 XML
<select id="queryStudent"
resultType="Student">
select
id,
name,
age,
gender,
birthday,
create_time
from student
<where>
<!-- 对象参数 -->
<if test="query.startBirthday != null">
and birthday >= #{query.startBirthday}
</if>
<if test="query.endBirthday != null">
and birthday <= #{query.endBirthday}
</if>
<!-- String参数 -->
<if test="name != null
and name != ''">
and name=#{name}
</if>
<!-- Integer参数 -->
<if test="age != null">
and age=#{age}
</if>
<!-- List参数 -->
<if test="ids != null
and ids.size()>0">
and id in
<foreach collection="ids"
item="id"
open="("
close=")"
separator=",">
#{id}
</foreach>
</if>
</where>
limit #{limit}
</select>调用:
StudentQuery query=new StudentQuery();
query.setStartBirthday(
new Date()
);
mapper.queryStudent(
query,
"Tom",
Arrays.asList(1L,2L),
18,
10
);最终 SQL:
select * from student where birthday >= ? and name='Tom' and age=18 and id in(1,2) limit 10
十五、查询单个 Student 对象
如果查询结果只有一条:
Mapper:
Student queryOne(
@Param("query")
StudentQuery query,
@Param("name")
String name
);XML:
<select id="queryOne"
resultType="Student">
select *
from student
where name=#{name}
limit 1
</select>返回:
Student student;
十六、include 标签复用 SQL
企业项目中:
一个表可能几十个字段。
例如:
select id, name, age, gender, birthday, create_time from student
很多 XML 重复。
MyBatis 提供:
<sql>
16.1 定义公共字段
<sql id="studentColumns"> id, name, age, gender, birthday, create_time </sql>
16.2 使用
<select id="selectList" resultType="Student"> select <include refid="studentColumns"/> from student </select>
最终:
select id, name, age, gender, birthday, create_time from student
16.3 include 的企业意义
优点:
- 避免重复字段
- 修改字段只改一处
- 保证多个 SQL 字段一致
常见:
Base_Column_List
例如 MyBatis Generator 生成:
<sql id="Base_Column_List"> id, order_no, status, create_time </sql>
大量项目都会使用。
十七、MyBatis 分表实战
在企业项目中,分表是非常常见的数据库优化方案。
例如订单表:
order_info
数据量达到:
- 几千万
- 几亿
单表查询和维护压力增大。
因此按照月份进行拆分:
order_info_202601 order_info_202602 order_info_202603 ...
这种方式叫:
水平分表(Horizontal Sharding)
17.1 为什么需要分表?
假设:
订单表:
order_info
数据:
2024年 5000万 2025年 8000万 2026年 1亿
问题:
1. 单表索引巨大
例如:
select * from order_info where order_no='10001'
数据库需要维护巨大 B+Tree。
2. 历史数据影响当前业务
例如:
查询今天订单:
where create_time >= today
但是:
数据库还包含:
5年前订单
3. 表维护困难
例如:
- 加字段
- 建索引
- 数据迁移
17.2 分表设计
例如:
订单表:
order_info_202601
结构:
CREATE TABLE order_info_202601
(
id BIGINT PRIMARY KEY,
order_no VARCHAR(64),
user_id BIGINT,
amount DECIMAL(10,2),
status VARCHAR(20),
create_time DATETIME
);其他月份:
order_info_202602 order_info_202603
结构完全一致。
17.3 MyBatis 动态表名
MyBatis 默认:
select * from order_info
表名固定。
但是分表:
需要:
select * from order_info_202601
表名动态变化。
注意
不能:
from #{tableName}原因:
#{} 是预编译参数。
最终:
from ?
数据库不支持。
必须使用:
${}
例如:
<select id="selectOrder">
select *
from ${tableName}
where order_no=#{orderNo}
</select>17.4 ${} 与 #{} 区别
这是 MyBatis 非常重要的知识。
#{}
预编译:
where id=#{id}
生成:
where id=?
参数:
PreparedStatement
优点:
- 防 SQL 注入
- 性能更好
${}
字符串替换:
from ${tableName}
生成:
from order_info_202601
缺点:
存在 SQL 注入风险。
17.5 分表 Mapper 示例
Mapper
public interface OrderMapper {
List<OrderInfo> selectByOrderNo(
@Param("tableName")
String tableName,
@Param("orderNo")
String orderNo
);
}XML
<select id="selectByOrderNo"
resultType="OrderInfo">
select
id,
order_no,
user_id,
amount,
status,
create_time
from ${tableName}
where order_no=#{orderNo}
</select>调用:
String tableName =
"order_info_202601";
orderMapper.selectByOrderNo(
tableName,
"ORDER001"
);最终:
select * from order_info_202601 where order_no='ORDER001'
17.6 动态表名安全处理
由于:
${}
存在风险。
不要:
String tableName=request.getParameter();
例如:
恶意输入:
order_info where 1=1
可能生成:
from order_info where 1=1
正确方式:
代码控制。
例如:
public String getOrderTableName(LocalDate date){
String month =
date.format(
DateTimeFormatter.ofPattern("yyyyMM")
);
return "order_info_"+month;
}只允许:
order_info_202601 order_info_202602
十八、分表 CRUD 完整示例
18.1 新增
Mapper:
int insertOrder(
@Param("tableName")
String tableName,
@Param("order")
OrderInfo order
);XML:
<insert id="insertOrder">
insert into ${tableName}
(
id,
order_no,
user_id,
amount,
status,
create_time
)
values
(
#{order.id},
#{order.orderNo},
#{order.userId},
#{order.amount},
#{order.status},
#{order.createTime}
)
</insert>18.2 查询
<select id="selectOrder">
select *
from ${tableName}
where order_no=#{orderNo}
</select>18.3 删除
Mapper:
int deleteOrder(
@Param("tableName")
String tableName,
@Param("id")
Long id
);XML:
<delete id="deleteOrder">
delete from ${tableName}
where id=#{id}
</delete>18.4 修改
<update id="updateOrder">
update ${tableName}
<set>
<if test="order.status!=null">
status=#{order.status},
</if>
<if test="order.amount!=null">
amount=#{order.amount},
</if>
</set>
where id=#{order.id}
</update>十九、批量插入企业案例
假设:
订单明细表:
order_detail
字段:
id order_id product_id quantity price
实体:
@Data
public class OrderDetail {
private Long id;
private Long orderId;
private Long productId;
private Integer quantity;
private BigDecimal price;
}Mapper:
int batchInsert(
@Param("list")
List<OrderDetail> list
);XML:
<insert id="batchInsert">
insert into order_detail
(
id,
order_id,
product_id,
quantity,
price
)
values
<foreach collection="list"
item="item"
separator=",">
(
#{item.id},
#{item.orderId},
#{item.productId},
#{item.quantity},
#{item.price}
)
</foreach>
</insert>生成:
insert into order_detail values (1,100,1,2,20), (2,100,2,1,50), (3,100,3,5,10)
二十、MyBatis 一级缓存
MyBatis 默认开启一级缓存。
一级缓存:
SqlSession 级别缓存。
例如:
SqlSession session; StudentMapper mapper = session.getMapper(StudentMapper.class); mapper.selectById(1); mapper.selectById(1);
第一次:
执行 SQL:
select * from student where id=1
第二次:
直接从缓存获取。
20.1 一级缓存生命周期
范围:
SqlSession
例如:
SqlSession1 缓存存在 关闭 缓存消失
20.2 什么情况下一级缓存失效?
1. 不同 SqlSession
session1 session2
互相不能共享。
2. 执行增删改
例如:
mapper.update();
MyBatis 会清空缓存。
原因:
数据可能变化。
3. 手动清理
sqlSession.clearCache();
二十一、MyBatis 二级缓存
二级缓存:
Mapper 级别缓存。
多个 SqlSession 可以共享。
结构:
SqlSession1
|
|
Mapper二级缓存
|
|
SqlSession2开启:
XML:
<cache/>
例如:
<mapper namespace="StudentMapper"> <cache/> </mapper>
实体需要:
实现 Serializable:
public class Student
implements Serializable{
}21.1 二级缓存适合场景
适合:
- 配置数据
- 基础数据
- 不经常变化的数据
例如:
城市列表:
city
不适合:
订单:
order
因为:
变化频繁。
二十二、MyBatis 日志配置
开发中排查 SQL 非常重要。
Spring Boot:
application.yml
mybatis:
configuration:
log-impl:
org.apache.ibatis.logging.stdout.StdOutImpl输出:
Preparing: select * from student where id=? Parameters: 1(Long)
生产环境:
推荐:
slf4j
例如:
mybatis: configuration: log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
二十三、MyBatis 常见问题总结
23.1 foreach 集合为空
错误:
<foreach collection="ids">
但是:
ids=null
生成:
where id in ()
SQL错误。
正确:
<if test="ids!=null and ids.size()>0"> </if>
23.2 SQL 注入问题
危险:
where name='${name}'
例如:
输入:
Tom' or '1'='1
生成:
where name='Tom' or '1'='1'
正确:
where name=#{name}23.3 大分页问题
例如:
select * from order limit 1000000,20
MySQL:
需要扫描:
1000020条
然后丢弃。
优化:
使用:
覆盖索引:
select id from order limit 1000000,20
或者:
基于 ID:
where id > lastId limit 20
23.4 不建议 select *
错误:
select * from student
问题:
- 多查询字段
- 网络传输增加
- 表结构变化影响程序
推荐:
select id, name, age from student
23.5 XML 特殊字符
例如:
小于:
<
需要:
<
大于:
>
需要:
>
例如日期:
错误:
birthday <= #{date}
正确:
birthday <= #{date}
二十四、MyBatis 与 MyBatis-Plus 对比
| MyBatis | MyBatis-Plus | |
|---|---|---|
| SQL控制 | 完全控制 | 部分自动 |
| 学习成本 | 较高 | 较低 |
| 复杂SQL | 优秀 | 一般 |
| CRUD | 需要写 | 自动生成 |
| 企业大型系统 | 常用 | 常用 |
二十五、企业项目 MyBatis 最佳实践
25.1 Mapper 方法命名规范
推荐:
查询:
select query find
新增:
insert
修改:
update
删除:
delete
25.2 XML 按业务拆分
不要:
StudentMapper.xml 10000行
推荐:
student order invoice
25.3 大批量操作必须分批
例如:
错误:
batchInsert(1000000)
推荐:
500条 1000条 提交一次
25.4 动态 SQL 注意可读性
不要:
几十层 if
复杂业务:
放 Service 层处理。
总结
本文完整介绍了 MyBatis 从基础到企业实战:
基础
- MyBatis 原理
- Spring Boot 集成
- Mapper
- XML
参数
- parameterType
- resultType
- resultMap
- @Param
动态 SQL
- if
- where
- trim
- set
- foreach
CRUD
- 新增
- 动态新增
- 批量插入
- 批量更新
- ON DUPLICATE KEY UPDATE
企业实践
- 分表
- 动态表名
- 缓存
- SQL 日志
- 性能优化
- 常见问题
到此这篇关于MyBatis 教程完全指南:从入门配置到企业级动态 SQL 实战攻略的文章就介绍到这了,更多相关mybatis动态sql实战指南内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
