java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > mybatis动态sql实战指南

MyBatis 教程完全指南:从入门配置到企业级动态 SQL 实战攻略

作者:西凉的悲伤

MyBatis 是一个优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射,MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的工作,本文介绍MyBatis 教程完全指南:从入门配置到企业级动态SQL实战攻略,感兴趣的朋友一起看看吧

前言

本篇文章目标:

示例环境:

一、什么是 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()){
}

存在的问题:

  1. 大量重复代码

例如:

  1. SQL 和 Java 代码耦合严重
  2. 查询结果需要手动封装

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.xml

SQL 可以独立维护。

(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
ExecutorSQL 执行器
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 自动完成:

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

数据库返回:

idnameage
1Tom18

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 区别

resultTyperesultMap
简单对象推荐不需要
字段名称一致推荐不需要
字段需要转换不支持支持
复杂关联查询不适合推荐
一对多不支持支持

企业开发:

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 会自动:

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 标签

用于:

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 中常见的批量插入方式主要有:

  1. foreach 拼接 values
  2. 循环调用 insert
  3. 批量 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 两者区别

insertSelectivebatchInsert
数据量单条多条
目的动态字段提高插入效率
核心标签if + trimforeach
典型场景新增业务对象批量导入

十二、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);

数据库:

idusernameage
1Tom18

第二次:

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 批量更新

批量更新常见两种方式:

  1. foreach 多条 update
  2. 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 updateCASE WHEN
SQL数量多条一条
性能一般
实现难度简单复杂
适合数据量少量大量

企业:

几十条:

foreach

几千条:

CASE WHEN

十四、MyBatis 多参数查询实战

实际项目中,一个查询方法经常包含多个类型参数。

例如:

查询学生:

条件:

  1. 一个查询对象
  2. 一个 String
  3. 一个 List
  4. 一个 Integer
  5. 一个日期范围

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 的企业意义

优点:

  1. 避免重复字段
  2. 修改字段只改一处
  3. 保证多个 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

优点:

${}

字符串替换:

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

问题:

  1. 多查询字段
  2. 网络传输增加
  3. 表结构变化影响程序

推荐:

select
id,
name,
age
from student

23.5 XML 特殊字符

例如:

小于:

<

需要:

&lt;

大于:

>

需要:

&gt;

例如日期:

错误:

birthday <= #{date}

正确:

birthday &lt;= #{date}

二十四、MyBatis 与 MyBatis-Plus 对比

MyBatisMyBatis-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 从基础到企业实战:

基础

参数

动态 SQL

CRUD

企业实践

到此这篇关于MyBatis 教程完全指南:从入门配置到企业级动态 SQL 实战攻略的文章就介绍到这了,更多相关mybatis动态sql实战指南内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文