MyBatis笔记

#Part1 Mybatis

一、MaBatis简介

1. 简介

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

MyBatis最初是Apache的一个开源项目iBatis, 2010年6月这个项目由Apache Software Foundation迁移到了Google Code。随着开发团队转投Google Code旗下, iBatis3.x正式更名为MyBatis。代码于2013年11月迁移到Github。

MyBatis官网介绍:https://mybatis.org/mybatis-3/zh_CN/index.html

2. 持久层框架对比

  • JDBC
    • SQL 夹杂在Java代码中耦合度高,导致硬编码内伤
    • 维护不易且实际开发需求中 SQL 有变化,频繁修改的情况多见
    • 代码冗长,开发效率低
  • HibernateJPA
    • 操作简便,开发效率高
    • 程序中的长难复杂 SQL 需要绕过框架
    • 内部自动生成的 SQL,不容易做特殊优化
    • 基于全映射的全自动框架,大量字段的 POJO 进行部分映射时比较困难。
    • 反射操作太多,导致数据库性能下降
  • MyBatis
    • 轻量级,性能出色
    • SQL 和 Java 编码分开,功能边界清晰。Java代码专注业务、SQL语句专注数据
    • 开发效率稍逊于 Hibernate,但是完全能够接收

开发效率:Hibernate>Mybatis>JDBC

运行效率:JDBC>Mybatis>Hibernate

3. 🌕MaBatis3快速入门

§3.0 准备工作

(1) 创建数据库
CREATE DATABASE `mybatis-example`;

USE `mybatis-example`;

CREATE TABLE `t_emp`(
    emp_id INT AUTO_INCREMENT,
    emp_name CHAR(100),
    emp_salary DOUBLE(10,5),
    PRIMARY KEY(emp_id)
);

INSERT INTO `t_emp`(emp_name,emp_salary) VALUES("tom",200.33);
INSERT INTO `t_emp`(emp_name,emp_salary) VALUES("jerry",666.66);
INSERT INTO `t_emp`(emp_name,emp_salary) VALUES("andy",777.77);
(2) 导入依赖

pom.xml 文件中导入mybatis依赖。

<project ...>

    <dependencies>
        
        <!-- mybatis依赖 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.11</version>
        </dependency>

        <!-- MySQL驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.25</version>
        </dependency>

        <!--junit5测试-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.3.1</version>
        </dependency>
        
    </dependencies>

</project>
(3) 准备实体类

创建 pojo包 下的 Employee实体类

public class Employee {

    private Integer empId;

    private String empName;

    private Double empSalary;
    
    // getter、setter、toString、...
}

§3.1 准备Mapper接口和MapperXML文件

MyBatis 框架下,SQL语句编写位置发生改变,从原来的Java类,改成XML或者注解定义。推荐在XML文件中编写SQL语句,让用户能更专注于 SQL 代码,不用关注其他的JDBC代码。

一般编写SQL语句的文件命名:XxxMapper.xml(Xxx一般取表名)

Mybatis 中的 Mapper 接口相当于以前的 Dao。但是区别在于,Mapper 仅仅只是建接口即可,我们不需要提供实现类,具体的SQL写到对应的Mapper文件。

MybatisChange

MyBatis用法的思路如下图所示:

image_pikdpXSaNV
(1) 定义mapper接口

创建 mapper包 下的 EmployeeMapper接口

package com.example.mapper;

import com.example.pojo.Employee;

/**
 * t_emp表对应数据库SQL语句映射接口
 * 接口只规定方法、参数和返回值
 */
public interface EmployeeMapper {

    /**
     * 根据员工id查询员工数据方法
     * @param empId  员工id
     * @return 员工实体对象
     */
    Employee selectEmployee(Integer empId);
    
}
(2) 定义MapperXML文件

resources目录 下,创建 mappers目录 中的 EmployeeMapper.xml文件。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace等于mapper接口类的全限定名,这样实现对应 -->
<mapper namespace="com.example.mapper.EmployeeMapper">

    <!-- 
        声明标签编写sql语句:<select>、<insert>、<update>、<delete>
        每个标签对应接口的一个方法的实现,注意:Mapper接口不能重载!!!
            - id = 方法名
            - resultType = 方法的返回值类型
            - 在标签内编写SQL语句
	 -->
    <select id="selectEmployee" resultType="com.example.pojo.Employee">
        <!-- #{empId}代表动态传入的参数,并且进行赋值,后面详细讲解 -->
        select 
        emp_id empId, emp_name empName, emp_salary empSalary 
        from 
        t_emp 
        where 
        emp_id = #{empId}
    </select>
</mapper>

§3.2 准备MyBatis配置文件

mybatis框架配置文件:数据库连接信息、性能配置、mapper.xml配置等。

习惯上命名为 mybatis-config.xml,这个文件名仅仅只是建议,并非强制要求。将来整合 Spring 之后,这个配置文件可以省略。

resource目录 下创建 mybatis-config.xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!-- 
    environments表示配置Mybatis的开发环境,可以配置多个环境,在众多具体环境中,使用default属性指定实际运行时使用的环境。
    default属性的取值是environment标签的id属性的值。
	-->
    <environments default="development">
        <!-- environment表示配置Mybatis的一个具体的环境 -->
        <environment id="development">
            <!-- Mybatis的内置的事务管理器 -->
            <transactionManager type="JDBC"/>
            <!-- 配置数据源 -->
            <dataSource type="POOLED">
                <!-- s1. 配置建立数据库连接的具体信息 -->
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis-example"/>
                <property name="username" value="root"/>
                <property name="password" value="12346"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <!-- 
		s2. Mapper注册:指定Mybatis映射文件的具体位置
            - mapper标签:配置一个具体的Mapper映射文件
            - resource属性:指定Mapper映射文件的实际存储位置,这里需要使用一个以类路径根目录为基准的相对路径 
        对Maven工程的目录结构来说,resources目录下的内容会直接放入类路径,所以这里我们可以以resources目录为基准
		-->
        <mapper resource="mappers/EmployeeMapper.xml"/>
    </mappers>

</configuration>

§3.3 运行和测试

public class MyBatisTest {

    @Test
    public void testSelectEmployee() throws IOException {

        // 1. 创建SqlSessionFactory对象
        // 1.1 声明Mybatis全局配置文件的路径
        String mybatisConfigFilePath = "mybatis-config.xml";
        // 1.2 以输入流的形式加载Mybatis配置文件
        InputStream inputStream = Resources.getResourceAsStream(mybatisConfigFilePath);
        // 1.3 基于读取Mybatis配置文件的输入流创建SqlSessionFactory对象
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        // 2. 使用SqlSessionFactory对象开启一个会话
        SqlSession session = sessionFactory.openSession();

        // 3. 根据EmployeeMapper接口的Class对象获取Mapper接口类型的对象(动态代理技术)
        EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);

        // 4. 调用代理类方法,即可触发对应的SQL语句
        Employee employee = employeeMapper.selectEmployee(1);
        System.out.println("employee = " + employee);

        // 5. 关闭SqlSession
        session.commit(); 	//提交事务(DQL不需要,其他需要),也可以定义openSession(true)表示自动提交
        session.close(); 	//关闭会话

    }
}

说明:

  • SqlSession:代表Java程序和数据库之间的会话。(HttpSession是Java程序和浏览器之间的会话)
  • SqlSessionFactory:是“生产”SqlSession的“工厂”。
  • 工厂模式:如果创建某一个对象,使用的过程基本固定,那么我们就可以把创建这个对象的相关代码封装到一个“工厂类”中,以后都使用这个工厂类来“生产”我们需要的对象。

SqlSession和HttpSession区别:

  • HttpSession:工作在Web服务器上,属于表述层。
    • 代表浏览器和Web服务器之间的会话。
  • SqlSession:不依赖Web服务器,属于持久化层。
    • 代表Java程序和数据库之间的会话。

4. iBatis和MyBatis原理

4.1 iBatis原理

iBatisPrinciple

4.2 MyBatis原理

MyBaitsPrinciple

二、☀️MyBatis基本使用

1. 日志输出配置

MyBatis的XML配置文件设计标签和顶层结构如下:

我们可以在mybatis的配置文件使用settings标签设置,输出运过程SQL日志。

settings设置项:

设置名 描述 有效值 默认值
logImpl 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 SLF4J | LOG4J(3.5.9 起废弃) | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING 未设置

日志配置:

<configuration>
    <settings>
        <!-- 选择控制台输出 -->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
</configuration>

2. 向SQL语句传参

在 Mapper.xml 文件中写sql语句,有两种方式传参:

  1. #{key}:等价于 占位符+复制,如 emp_id=?,再对?赋值。【推荐,可以防止sql注入攻击】
  2. ${key}:等价于 字符串拼接,如 "emp_id=" + id

区别在于:

  • #{key}:只能代替值的位置,不能代替容器名、标签、列名、sql关键字等。
  • ${key}:可以动态替代所有字符串,如 ${columnName} = #{columnValue}

3. 数据输入

3.0 概念说明

这里数据输入具体是指上层方法(例如Service方法)调用Mapper接口时,数据传入的形式。

  • 简单类型(单值类型):只包含一个值的数据类型
    • 基本数据类型:int、byte、short、double、...
    • 基本数据类型的包装类型:Integer、Character、Double、...
    • 字符串类型:String
  • 复杂类型(多值类型):包含多个值的数据类型
    • 实体类类型:Employee、Department、...
    • 集合类型:List、Set、Map、...
    • 数组类型:int[]、String[]、...
    • 复合类型:List<Employee>、实体类中包含集合...

3.1 输入单个简单类型参数

单个简单类型参数,在 #{} 中可以随意命名,通常还是使用和接口方法参数同名。

例:

Mapper接口中抽象方法的声明:

Employee selectEmployee(Integer empId);

SQL语句:

<select id="selectEmployee" resultType="com.example.pojo.Employee">
 select emp_id empId,emp_name empName,emp_salary empSalary 
 from t_emp 
 where emp_id=#{empId}
</select>
  • 其中 #{empId} 内的内容可以随便填写,建议同名。

3.2 输入多个简单类型参数|@Param

单个简单类型参数,在 #{} 中有两种填写方式:

  1. 注解指定:在接口中的形参前使用 @Param{"名称"} 标注传入sql的参数。【推荐】
  2. MyBatis默认机制
    • 方案1:从左到右依次填写 arg0, arg1, ...
    • 方案2:从左到右依次填写 param1, param2, ...

例:

方式一:注解指定

Mapper接口中抽象方法的声明:

int updateEmployee(@Param("empId") Integer empId, @Param("empSalary") Double empSalary);

SQL语句:

<update id="updateEmployee">
 update t_emp 
 set emp_salary=#{empSalary} 
 where emp_id=#{empId}
</update>
  • 其中 #{empSalary} #{empId} 中的内容是 @Param() 注解中设定的值。

方式二:MyBatis默认机制

arg0参数:

Mapper接口中抽象方法的声明:

int updateEmployee(Integer empId, Double empSalary);

SQL语句:

<update id="updateEmployee">
    update t_emp 
    set emp_salary=#{arg0} 
    where emp_id=#{arg1}
</update>

param1参数:

Mapper接口中抽象方法的声明:

int updateEmployee(Integer empId, Double empSalary);

SQL语句:

<update id="updateEmployee">
    update t_emp 
    set emp_salary=#{param1} 
    where emp_id=#{param2}
</update>

3.3 输入实体类类型参数

实体类类型参数,在 #{} 中填写对应实体类的属性名

Mybatis会根据#{}中传入的数据,加工成getXxx()方法,通过反射在实体类对象中调用这个方法,从而获取到对应的数据。填充到#{}解析后的问号占位符这个位置。

例:

Mapper接口中抽象方法的声明:

int insertEmployee(Employee employee);

SQL语句:

<insert id="insertEmployee">
 insert into t_emp(emp_name,emp_salary) 
 values(#{empName}, #{empSalary})
</insert>

3.4 输入Map类型参数

Map类型参数,在 #{} 中填写对应的key

使用场景:有很多零散的参数需要传递,但是没有对应的实体类类型可以使用。使用@Param注解一个一个传入又太麻烦了,所以都封装到Map中。

Mapper接口中抽象方法的声明:

int updateEmployeeByMap(Map<String, Object> paramMap);

SQL语句:

<update id="updateEmployeeByMap">
 update t_emp 
 set emp_salary=#{empSalaryKey} 
 where emp_id=#{empIdKey}
</update>

4. 数据输出

4.0 输出概述

数据输出总体上有两种形式:

  1. 增删改操作返回的受影响行数:直接使用 int 或 long 类型接收即可。
  2. 查询操作的查询结果。

我们需要做的是,指定查询的输出数据类型即可,并且插入场景下,实现主键数据回显示。

4.1 返回单个简单类型

<select> 标签中,通过resultType属性指定查询返回值类型。

resultType的取值可为:

  1. 类的全限定路径
  2. 别名简称

类型别名问题在官网中有详细的说明:https://mybatis.org/mybatis-3/zh_CN/configuration.html#typeAliases

(1) Java类型别名

类型别名可为 Java 类型设置一个缩写名字,它仅用于 XML 配置,意在降低冗余的全限定类名书写。

下面是Mybatis为常见的 Java 类型内建的类型别名:

别名 映射的类型
_byte byte
_char (since 3.5.10) char
_character (since 3.5.10) char
_long long
_short short
_int int
_integer int
_double double
_float float
_boolean boolean
string String
byte Byte
char (since 3.5.10) Character
character (since 3.5.10) Character
long Long
short Short
int Integer
integer Integer
double Double
float Float
boolean Boolean
date Date
decimal BigDecimal
bigdecimal BigDecimal
biginteger BigInteger
object Object
object[] Object[]
map Map
hashmap HashMap
list List
arraylist ArrayList
collection Collection
iterator Iterator

例:

Mapper接口中抽象方法的声明:

int selectEmpCount();

SQL语句:

<select id="selectEmpCount" resultType="_int">
 select count(*) from t_emp
</select>
(2) 自定义类型别名
① 单独定义|typeAlias

类型别名可为 Java 类型设置一个缩写名字。它仅用于 XML 配置,意在降低冗余的全限定类名书写。

例如在 mybatis-config.xml 中定义:

<configuration>
    <typeAliases>
        <typeAlias alias="Author" type="com.example.pojo.Author"/>
        <typeAlias alias="Blog" type="com.example.pojo.Blog"/>
    </typeAliases>
</configuration>

当这样配置时,Blog别名可以用在任何使用com.example.pojo.Blog类型的地方。

② 批量定义|typeAliases

也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean,则别名就是包中类的首字母小写。

例如在 mybatis-config.xml 中定义:

<configuration>
    <typeAliases>
        <package name="com.example.pojo"/>
    </typeAliases>
</configuration>
③ 注解定义|@Alias

在批量定义的前提下,可以在类上添加 @Alias("别名") 注解,为其添加别名。

@Alias("author")
public class Author {
    ...
}

4.2 返回实体类类型

(1) 类型指定

resultType的取值可为:

  1. 类的全限定路径
  2. 别名简称(见[上节](#(2) 自定义类型别名))

例:

Mapper接口的抽象方法

Employee selectEmployee(Integer empId);

SQL语句:

<!-- resultType属性:指定封装查询结果的Java实体类的全类名 -->
<select id="selectEmployee" resultType="com.example.mybatis.entity.Employee">
 <!-- 给每一个字段设置一个别名,让别名和Java实体类中属性名一致 -->
 select emp_id empId, emp_name empName, emp_salary empSalary 
 from t_emp 
 where emp_id=#{maomi}
</select>
(2) ☀️驼峰式自动映射|mapUnderscoreToCamelCase

mybatis-config.xml 中的 <settings> 标签中定义如下内容:

<configuration>
    <settings>
        <!-- 规则要求数据库表字段命名方式:单词_单词 -->
        <!-- 规则要求Java实体类属性名命名方式:首字母小写的驼峰式命名 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
</configuration>

4.3 返回Map类型

适用于SQL查询返回的各个字段综合起来并不和任何一个现有的实体类对应,没法封装到实体类对象中。能够封装成实体类类型的,就不使用Map类型。

例:

Mapper接口的抽象方法:

Map<String,Object> selectEmpNameAndMaxSalary();

SQL语句:

<!-- 指定resultType为Map类型的别名 -->
<select id="selectEmpNameAndMaxSalary" resultType="map">
 SELECT
     emp_name 员工姓名,
     emp_salary 员工工资,
 	(SELECT AVG(emp_salary) FROM t_emp) 部门平均工资
 FROM 
 	t_emp 
 WHERE 
     emp_salary=(
         SELECT MAX(emp_salary) 
 		FROM t_emp
     )
</select>

4.4 返回List类型

查询结果返回多个实体类对象,希望把多个实体类对象放在List集合中返回。此时不需要任何特殊处理,在resultType属性中还是设置实体类类型即可。

例:

Mapper接口中抽象方法:

List<String> queryNamesBySalary(Double salary);
List<Employee> selectAll();

SQL语句:

<!-- 返回值是集合,resultType不需要指定为List类型,只需要指定其范型类型即可 -->
<select id="queryNamesBySalary" resultType="string">
 select emp_name
 from t_emp
 where emp_salary > #{salary}
</select>

<select id="selectAll" resultType="com.example.mybatis.entity.Employee">
 select emp_id empId,emp_name empName,emp_salary empSalary
 from t_emp
</select>

4.5 ☀️返回主键值(主键回显)

(1) 自增长类型主键

在MapperXML文件的sql标签中添加以下属性:

  1. useGeneratedKeys="true":开启表示需要数据库的自增主键。
  2. keyColumn="xxx":主键列的值。
  3. keyProperty="xxx":接收主键列值的实体属性。

例:

Mapper接口中的抽象方法

int insertEmployee(Employee employee);

SQL语句

<insert id="insertEmployee" useGeneratedKeys="true" keyColumn="emp_id" keyProperty="empId">
 insert into t_emp(emp_name,emp_salary)
 values(#{empName},#{empSalary})
</insert>
(2) 非自增长类型主键

在MapperXML文件的sql标签中添加<selectKey>标签,在其中指定一段sql语句,并设置以下属性:

  1. order="before | after":表示指定的sql语句是在原sql语句之前执行还是之后执行。
  2. resultType="xxx":返回值的类型。
  3. keyProperty="xxx":接收主键列值的实体属性。

例:

Mapper接口中的抽象方法

int insertUser(User user);

SQL语句

<insert id="insertUser" parameterType="User">
 <selectKey order="BEFORE" resultType="string" keyProperty="id">
     SELECT UUID() as id
 </selectKey>
 INSERT INTO user (id, username, password) 
 VALUES (
     #{id},
     #{username},
     #{password}
 )
</insert>

4.6 实体类属性和数据库字段对应关系

  1. 别名对应

    将字段的别名设置成和实体类属性一致。如:select emp_id empId, emp_name empName

  2. 全局配置驼峰式命名规则自动映射

    <setting name="mapUnderscoreToCamelCase" value="true"/>

  3. 使用resultMap

    使用<resultMap>标签定义对应关系,再在后面的SQL语句中引用这个对应关系。

    • resultType属性只能映射一层结构,多层次的对象结构无法映射,即多表查询的时候结果无法映射。
    • resultMap可以自定义映射关系,可以单层次,也可以多层次。
    <!-- 专门声明一个resultMap设定column到property之间的对应关系 -->
    <resultMap id="selectEmployeeByRMResultMap" type="com.example.mybatis.entity.Employee">
        <!-- 
    		id标签:设置主键列和主键属性之间的对应关系
     			- column属性用于指定字段名
    			- property属性用于指定Java实体类属性名
    	-->
        <id column="emp_id" property="empId"/>
    
        <!-- result标签:设置普通字段和Java实体类属性之间的关系 -->
        <result column="emp_name" property="empName"/>
        <result column="emp_salary" property="empSalary"/>
    </resultMap>
    
    
    <!-- resultMap属性与<resultMap>标签的id对应 -->
    <select id="selectEmployeeByRM" resultMap="selectEmployeeByRMResultMap">
        select emp_id, emp_name, emp_salary 
        from t_emp 
        where emp_id=#{empId}
    </select>
    

5. 提取公共代码|@BeforeEach/@AfterEach

在执行sql代码时,往往需要相同的操作,如:

public class MyBatisTest {
    @Test
    public void testSelectEmployee() throws IOException {
        // 前置代码
        String mybatisConfigFilePath = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(mybatisConfigFilePath);
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession session = sessionFactory.openSession();

        // 执行sql
        EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
        Employee employee = employeeMapper.selectEmployee(1);
        System.out.println("employee = " + employee);

        // 后置代码
        session.commit();
        session.close();
    }
}

可以使用 @BeforeEach@AfterEach注解,提取相同部分的代码:

public class MyBatisTest {
    
    private SqlSession session;
    
    // 前置代码
    @Before
    public void before() throw IOException {
        String mybatisConfigFilePath = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(mybatisConfigFilePath);
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        session = sessionFactory.openSession();
    }    
    
    // 后置代码
    @After
    public void clean() throw IOException {
        session.commit();
        session.close();
    }    
    
    // 执行sql
    @Test
    public void testSelectEmployee() throws IOException {
        EmployeeMapper employeeMapper = session.getMapper(EmployeeMapper.class);
        Employee employee = employeeMapper.selectEmployee(1);
        System.out.println("employee = " + employee);
    }
}

三、MyBatis多表映射

1. 多表查询实体类设计方案

  • 对一情况:属性中包含对方对象
  • 对多情况:属性中包含对方对象集合

注意:

  1. 只有真实发生多表查询时,才需要设计和修改实体类,否则不提前设计和修改实体类。
  2. 无论多少张表联查,实体类设计都是两两考虑。
  3. 在查询映射的时候,只需要关注本次查询相关的属性。例如:查询订单和对应的客户,就不要关注客户中的订单集合。

例:客户和订单的实体类设计。

entitydesign
/** 客户实体类 */
public class Customer {
 private Integer customerId;
 private String customerName;
 private List<Order> orderList;// 体现的是对多的关系
}
// 查询客户和客户对应的订单集合 --> Order中的customer对象不要管!



/** 订单实体类 */
public class Order {
 private Integer orderId;
 private String orderName;
 private Customer customer;// 体现的是对一的关系
}
// 查询订单和订单对应的客户实体 --> Customer中的orderList不要管!

2. 🌕对一映射

§2.0 需求说明

根据ID查询订单,以及订单关联的用户的信息。

§2.1 实体类设计

创建 pojo包 中的 Customer类Order类

/** 客户实体类 */
public class Customer {
    private Integer customerId;
    private String customerName;
}


/** 订单实体类 */
public class Order {
    private Integer orderId;
    private String orderName;
    private Customer customer;	// 对一:装对方类型的对象
}

§2.2 OrderMapper接口

public interface OrderMapper {
    Order selectOrderWithCustomer(Integer orderId);
}

§2.3 OrderMapper.xml配置文件

<association> 标签中配置关联对象属性的字段。

创建 mappers目录 中的 OrderMapper.xml文件

<mapper namespace="com.example.mapper.CustomerMapper">

    <!-- 创建resultMap实现“对一”关联关系映射
			- id属性:这个<resultMap>所服务的那条SQL语句中对应的resultMap属性
  			- type属性:这个<resultMap>所服务的那条SQL语句最终要返回的类型(这里是用起别名的形式) -->
    <resultMap id="selectOrderWithCustomerResultMap" type="order">

        <!-- 先设置Order自身属性和字段的对应关系 -->
        <id column="order_id" property="orderId"/>
        <result column="order_name" property="orderName"/>


        <!-- 使用association标签配置“对一”关联关系
      		- property属性:关联对象的属性名
      		- javaType属性:关联对象的类型(这里是用起别名的形式) -->
        <association property="customer" javaType="customer">
            <!-- 配置Customer类的属性和字段名之间的对应关系 -->
            <id column="customer_id" property="customerId"/>
            <result column="customer_name" property="customerName"/>
        </association>

    </resultMap>


    <!-- 查询的sql语句 -->
    <select id="selectOrderWithCustomer" resultMap="selectOrderWithCustomerResultMap">
        SELECT order_id, order_name, c.customer_id, customer_name
        FROM t_order o
        LEFT JOIN t_customer c
        ON o.customer_id=c.customer_id
        WHERE o.order_id=#{orderId}
    </select>

</mapper>

对应关系:

one2one

§2.4 mybatis-config全局注册配置文件

<configuration>
	<!-- 给实体类起别名 -->
    <typeAlias>
    	<package name="com.example.pojo"/>
    </typeAlias>
    
    
    <!-- 注册Mapper配置文件 -->
    <mappers>
        <!-- 在mapper标签的resource属性中指定Mapper配置文件以“类路径根目录”为基准的相对路径 -->
        <mapper resource="mappers/OrderMapper.xml"/>
    </mappers>

</configuration>

§2.5 运行测试

public class MyBatisTest {

    private SqlSession session;

    @BeforeEach
    public void init() throws IOException {
        session = new SqlSessionFactoryBuilder()
            .build(Resources.getResourceAsStream("mybatis-config.xml"))
            .openSession();
    }

    @Test
    public void testRelationshipToOne() {
        OrderMapper orderMapper = session.getMapper(OrderMapper.class);
        // 查询Order对象,检查是否同时查询了关联的Customer对象
        Order order = orderMapper.selectOrderWithCustomer(2);
        log.info("order = " + order);
    }


    @AfterEach
    public void clear() {
        session.commit();
        session.close();
    }
}

3. 🌕对多映射

§2.0 需求说明

查询客户和客户关联的订单信息。

§2.1 实体类设计

创建 pojo包 中的 Customer类Order类

/** 客户实体类 */
public class Customer {
    private Integer customerId;
    private String customerName;
    private List<Order> orderList;	// 对多:装对方类型的集合
}


/** 订单实体类 */
public class Order {
    private Integer orderId;
    private String orderName;
}

§2.2 CustomerMapper接口

public interface CustomerMapper {
    Customer selectCustomerWithOrderList(Integer customerId);
}

§2.3 CustomerMapper.xml配置文件

<collection> 标签中配置关联对象集合的字段。

创建 mappers目录 中的 OrderMapper.xml文件

<mapper namespace="com.example.mapper.CustomerMapper">

    <!-- 配置resultMap实现“对多”关联关系映射 -->
    <resultMap id="selectCustomerWithOrderListResultMap" type="customer">

        <!-- 映射Customer自身的属性 -->
        <id column="customer_id" property="customerId"/>
        <result column="customer_name" property="customerName"/>

        <!-- collection标签:映射“对多”的关联关系
				- property属性:集合属性名
				- ofType属性:集合的范型类型(这里用的是别名) -->
        <collection property="orderList" ofType="order">
            <!-- 映射Order的属性 -->
            <id column="order_id" property="orderId"/>
            <result column="order_name" property="orderName"/>
        </collection>

    </resultMap>

    
    <!-- 查询的sql语句 -->
    <select id="selectCustomerWithOrderList" resultMap="selectCustomerWithOrderListResultMap">
        SELECT c.customer_id, c.customer_name, o.order_id, o.order_name
        FROM t_customer c
        LEFT JOIN t_order o
        ON c.customer_id=o.customer_id
        WHERE c.customer_id=#{customerId}
    </select>

</mapper>

对应关系:

one2multi

§2.4 mybatis-config全局注册配置文件

<configuration>
	<!-- 给实体类起别名 -->
    <typeAlias>
    	<package name="com.example.pojo"/>
    </typeAlias>
    
    
    <!-- 注册Mapper配置文件 -->
    <mappers>
        <!-- 在mapper标签的resource属性中指定Mapper配置文件以“类路径根目录”为基准的相对路径 -->
        <mapper resource="mappers/CustomerMapper.xml"/>
    </mappers>

</configuration>

§2.5 运行测试

public class MyBatisTest {

    private SqlSession session;

    @BeforeEach
    public void init() throws IOException {
        session = new SqlSessionFactoryBuilder()
            .build(Resources.getResourceAsStream("mybatis-config.xml"))
            .openSession();
    }

    @Test
    public void testRelationshipToMulti() {
        CustomerMapper customerMapper = session.getMapper(CustomerMapper.class);
        // 查询Customer对象同时将关联的Order集合查询出来
        Customer customer = customerMapper.selectCustomerWithOrderList(1);
        log.info("customer.getCustomerId() = " + customer.getCustomerId());
        log.info("customer.getCustomerName() = " + customer.getCustomerName());
        List<Order> orderList = customer.getOrderList();
        for (Order order : orderList) {
            log.info("order = " + order);
        }
    }


    @AfterEach
    public void clear() {
        session.commit();
        session.close();
    }
}

4. ☀️多表映射优化之自动映射|autoMappingBehavior

mybatis-config.xml 文件中:

setting属性 属性含义 可选值 默认值
autoMappingBehavior 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示关闭自动映射;PARTIAL 只会自动映射没有定义嵌套结果映射的字段。 FULL 会自动映射任何复杂的结果集(无论是否嵌套)。 NONE, PARTIAL, FULL PARTIAL

我们可以将autoMappingBehavior设置为full,进行多表resultMap映射的时候,可以省略符合列和属性命名映射规则(列名=属性名,或者开启驼峰映射也可以自定映射)的result标签。

修改mybati-sconfig.xml

<configuration>
    <!--开启resultMap自动映射 -->
	<setting name="autoMappingBehavior" value="FULL"/>
</configuration>

修改CustomerMapper.xml

<mapper namespace="com.example.mapper.CustomerMapper">

    <resultMap id="selectCustomerWithOrderListResultMap" type="customer">
        <id column="customer_id" property="customerId"/>
        <!-- <result column="customer_name" property="customerName"/> -->

        <collection property="orderList" ofType="order">
            <id column="order_id" property="orderId"/>
            <!-- <result column="order_name" property="orderName"/> -->
        </collection>

    </resultMap>

    
    <select id="selectCustomerWithOrderList" resultMap="selectCustomerWithOrderListResultMap">
        SELECT c.customer_id, c.customer_name, o.order_id, o.order_name
        FROM t_customer c
        LEFT JOIN t_order o
        ON c.customer_id=o.customer_id
        WHERE c.customer_id=#{customerId}
    </select>

</mapper>

四、MyBatis动态sql语句

1. <if>和<where>条件标签

使用动态 SQL 最常见情景是根据条件包含 <if><where> 子句的一部分。

  1. 使用 <where> 标签会自动去掉标签体内前面多余的andor
  2. 使用 <if> 标签,让我们可以有选择的加入SQL语句的片段。这个SQL语句片段是否要加入整个SQL语句,就看if标签判断的结果是否为true。
    • 在if标签的test属性中,可以访问实体类的属性,不可以访问数据库表的字段。
    • 在if标签内部,需要访问接口的参数时还是正常写#{}

例:

<select id="selectEmployeeByCondition" resultType="employee">
 select emp_id, emp_name, emp_salary 
 from t_emp
 <where>
     <if test="empName != null">
         emp_name = #{empName}
     </if>
     <if test="empSalary != null and empSalary &gt; 2000">
         and emp_salary > #{empSalary}
     </if>
 </where>
</select>
  • 第一种情况:所有条件都满足,WHERE emp_name=? and emp_salary>?
  • 第二种情况:部分条件满足,WHERE emp_salary>?
  • 第三种情况:所有条件都不满足,没有where子句

2. <set>标签

使用 <set> 标签动态管理set子句,并且动态去掉两端多余的逗号。

例:

<update id="updateEmployeeDynamic">
 update t_emp
 <set>
     <if test="empName != null">
         emp_name=#{empName},
     </if>
     <if test="empSalary &lt; 3000">
         emp_salary=#{empSalary}
     </if>
 </set>
 where emp_id=#{empId}
</update>
  • 第一种情况:所有条件都满足,SET emp_name=?, emp_salary=?
  • 第二种情况:部分条件满足,SET emp_salary=?
  • 第三种情况:所有条件都不满足,update t_emp where emp_id=?
  • 注意:没有set子句的update语句会导致SQL语法错误。

3. <trim>字符串修饰标签

使用 <trim> 标签控制条件部分两端是否包含某些字符。

  • prefix属性:指定要动态添加的前缀。
  • suffix属性:指定要动态添加的后缀。
  • prefixOverrides属性:指定要动态去掉的前缀,使用“|”分隔有可能的多个值。
  • suffixOverrides属性:指定要动态去掉的后缀,使用“|”分隔有可能的多个值。

例:

<select id="selectEmployeeByConditionByTrim" resultType="com.example.mybatis.entity.Employee">
 select emp_id,emp_name,emp_age,emp_salary,emp_gender
 from t_emp

 <!-- 当前例子用where标签实现更简洁,但是trim标签更灵活,可以用在任何有需要的地方 -->
 <trim prefix="where" prefixOverrides="and|or" suffixOverrides="and|or">
     <if test="empName != null">
         emp_name=#{empName} 
     </if>
     <if test="empSalary &gt; 3000">
         and emp_salary>#{empSalary} and
     </if>
     <if test="empAge &lt;= 20">
         emp_age=#{empAge} or
     </if>
     <if test="empGender=='male'">
         emp_gender=#{empGender}
     </if>
 </trim>
</select>

4. <choose>/<when>/<otherwise>分支判断标签

在多个分支条件中,仅执行一个。

  • 从上到下依次执行条件判断
  • 遇到的第一个满足条件的分支会被采纳
  • 被采纳分支后面的分支都将不被考虑
  • 如果所有的when分支都不满足,那么就执行otherwise分支

例:

<select id="selectEmployeeByConditionByChoose" resultType="com.example.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 &lt; 3000">
         emp_salary > 3000
     </when>
     <otherwise>
         1=1
     </otherwise>
 </choose>
</select>
  • 第一种情况:第一个when满足条件,where emp_name=?
  • 第二种情况:第二个when满足条件,where emp_salary > 3000
  • 第三种情况:两个when都不满足,执行了<otherwise>中的语句where 1=1

5. <foreach>批量标签

使用 <foreach> 标签可以遍历一个集合,其有几个属性:

  • collection属性:要遍历的集合。

    collection的取值可以为:

    • @Param("属性名"):在接口中List类型的参数使用@Param注解指定一个具体的名字【推荐】。
    • arg0
    • list
  • item属性:遍历集合的过程中能得到每一个具体对象,在item属性中设置一个名字,将来通过这个名字引用遍历出来的对象。

  • separator属性:每一轮遍历的分隔符号,如果是最后一次不会追加。

  • open属性:在整个循环把字符串拼好后,在最后的整体字符串的前面要添加的字符串。

  • close属性:在整个循环把字符串拼好后,在最后的整体字符串的后面要添加的字符串。

  • index属性:这里起一个名字,便于后面引用。

    • 遍历List集合,这里能够得到List集合的索引值。
    • 遍历Map集合,这里能够得到Map集合的key。

注意:尤其是批量更新操作,如果一个<foreach>标签涉及多个语句,需要设置允许执行多语句。mabatis-config.xml 文件中添加allowMultiQueries=true

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                
                <!-- 在url后面添加 ?allowMultiQueries=true -->
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis-example?allowMultiQueries=true"/>
                
                <property name="username" value="root"/>
                <property name="password" value="12346"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mappers/EmployeeMapper.xml"/>
    </mappers>

</configuration>

例:

批量查询:

List<Employee> queryEmployeeBatch(@Param("idList") List<integer> ids);
<select id="queryEmployeeBatch" resultType="employee">
 select * from t_emp
 where emp_id in
 <foreach collextion="idList" item="id" open="(" seperator="," close=")">
		#{id}
 </foreach>
</select>

以上语句相当于:select * from t_emp where emp_id in (1,2,3,4)

批量插入:

int insertEmployeeBatch(@Param("empList") List<Employee> employeeList);
<insert id="insertEmployeeBatch">
 insert into t_emp (emp_name, emp_salary)
 values
 <foreach collextion="empList" item="emp" seperator=",">
		(#{emp.empNmae}, #{emp.empSalary})
 </foreach>
</insert>

以上语句相当于:insert into t_emp (emp_name, emp_salary) valuse ("张三", 6000), ("李四", 7500), ("王五", 7000)

批量删除:

int deleteEmployeeBatch(@Param("idList") List<integer> ids);
<delete id="deleteEmployeeBatch">
 delete from t_emp
 where id in
 <foreach collextion="idList" item="id" open="(" seperator="," close=")">
		#{id}
 </foreach>
</delete>

以上语句相当于:delete from t_emp where id in (1,2,3,4)

批量修改:

int updateEmployeeBatch(@Param("empList") List<Employee> employeeList);
<update id="updateEmployeeBatch">
 <foreach collection="empList" item="emp" separator=";">
     update t_emp 
     set emp_name=#{emp.empName} 
     where emp_id=#{emp.empId}
 </foreach>
</update>

以上语句相当于:update t_emp set emp_name='张三' where emp_id=1; ...重复update...

6. <sql>片段标签

<sql id="xxx"> 标签用于抽取重复的SQL片段。

<include refid="xxx" /> 标签用于引用已抽取的SQL片段。

例:

<sql id="selectSql">
	select * from t_emp
</sql>

<select ...>
 <include refid="selectSql"/>
 where...
</select>

五、MyBatis高级扩展

1. ☀️Mapper批量映射优化

Mapper配置文件很多时,在全局配置文件中一个一个注册太麻烦,通过Mapper批量映射可以优化配置操作。

§1.1 配置mybatis-config.xml

mybatis-config.xml 文件中配置Mapper接口类所在的包,表示其中所有的接口Mapper都被批量配置:

<configuration>
    <!-- 配置Mapper接口类所在的包 -->
    <mappers>
        <package name="com.example.mapper"/>
    </mappers>
</configuration>

§1.2 配置要求

资源创建要求:

  1. Mapper接口 和 MapperXML配置文件 名称一致。如:
    • Mapper接口:EmployeeMapper.java
    • Mapper配置文件:EmployeeMapper.xml
  2. Mapper接口 和 MapperXML配置文件 编译后在同一个包内

对于第2点要求有两种实现方法:

  1. 【不推荐】将 MapperXML配置文件 也加入到 Mapper接口 所在的包即可,如都放在 src/main/java/com/example/mapper 包目录中,然后再在maven的 pom.xml 文件中添加配置:

    <project ...>
    
    	<build>
        	<resources>
            	<resource>
                	<directory>src/main/java</directory>
                    <includes>
                        <!-- 将src/main/java目录中的所有内容都打包 -->
                    	<include>**/*.*</include>
                    </includes>
                </resource>
            </resources>
        </build>
    
    </project>
    
  2. 【推荐】在 resource 文件夹创建对应的包目录结构。如:

    • 将Mapper接口EmployeeMapper.java文件放在src/main/java/com/example/mapper包目录中;
    • 将Mapper配置文件EmployeeMapper.xml文件放在src/main/resources/com/example/mapper目录中。

    注意:在resources目录中创建多层文件夹需要使用/作为分隔符,如果使用.分隔则只是表明要创建一个名为xx.yy.zz的目录名,而不是多层目录。

2. PageHelper分页插件

2.1 插件机制介绍

MyBatis 对插件进行了标准化的设计,并提供了一套可扩展的插件机制。插件可以在用于语句执行过程中进行拦截,并允许通过自定义处理程序来拦截和修改 SQL 语句、映射语句的结果等。

MyBatis 的插件机制包括以下三个组件:

  1. Interceptor(拦截器):定义一个拦截方法 intercept,该方法在执行 SQL 语句、执行查询、查询结果的映射时会被调用。
  2. Invocation(调用):实际上是对被拦截的方法的封装,封装了 Object targetMethod methodObject[] args 这三个字段。
  3. InterceptorChain(拦截器链):对所有的拦截器进行管理,包括将所有的 Interceptor 链接成一条链,并在执行 SQL 语句时按顺序调用。

插件的开发非常简单,只需要实现 Interceptor 接口,并使用注解 @Intercepts 来标注需要拦截的对象和方法,然后在 MyBatis 的配置文件中添加插件即可。

PageHelper 是 MyBatis 中比较著名的分页插件,它提供了多种分页方式(例如 MySQL 和 Oracle 分页方式),支持多种数据库,并且使用非常简单。在使用该分页插件时,有两点注意事项:

  1. sql语句不要添加limit
  2. sql语句不要使用;结尾

2.2 ☀️PageHelper分页插件的使用

(§1) 导入依赖

pom.xml 文件中添加依赖。

<project ...>
    <dependencies>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.1.11</version>
        </dependency>
    </dependencies>
</project>
(§2) mybatis-config.xml配置分页插件
<configuration>
    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!-- value表示插件的配置类型为mysql -->
            <property name="helperDialect" value="mysql"/>
        </plugin>
    </plugins>
</configuration>
(§3) 分页插件使用

Mapper接口 和 MapperXML文件 正常写即可,注意sql语句不要使用;结尾,其中也不要包含limit

在运行测试方法中,查询方法中使用分页:

public class MyBatisTest {

    private SqlSession session;

    @BeforeEach
    public void init() throws IOException {
        session = new SqlSessionFactoryBuilder()
            .build(Resources.getResourceAsStream("mybatis-config.xml"))
            .openSession();
    }

    @Test
    public void testPageHelper() {
        
        EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
        
        // 1. 设置分页数据:(当前是第几页, 每页显示多少条数据)
        PageHelper.startPage(1, 10);
        // 2. 调用sql方法
        List<Employee> list = mapper.queryList();
        // 3. 将查询数据封装到一个PageInfo的分页实体类中(包含一共有多少页、一共有多少条等等)
        PageInfo<Employee> pageInfo = new PageInfo<>(list);

        // 4. 获取分页的数据
        // 4.1 获取当前页的数据
        List<Employee> list1 = pageInfo.getList();
        // 4.2 获取总页数
        int pages = pageInfo.getPages();
        // 4.3 获取总条数
        long total = pageInfo.getTotal();
        // 4.4 获取当前是第几页
        int pageNum = pageInfo.getPageNum();
        // 4.5 获取页容量
        int pageSize = pageInfo.getPageSize();
        // ......
        
    }


    @AfterEach
    public void clear() {
        session.commit();
        session.close();
    }
}

注意:前3步只能装一个查询语句,不能将两条查询装到一个分页区,如果还需分页,则重新写前3步。

3. 逆向工程和MyBatisX插件

3.1 ORM思维介绍

ORM(Object-Relational Mapping,对象-关系映射)是一种将数据库和面向对象编程语言中的对象之间进行转换的技术。它将对象和关系数据库的概念进行映射,最后我们就可以通过方法调用进行数据库操。

ORM 框架通常有半自动和全自动两种方式。

  • 半自动ORM:通常需要程序员手动编写 SQL 语句或者配置文件,将实体类和数据表进行映射,还需要手动将查询的结果集转换成实体对象。
  • 全自动ORM:则是将实体类和数据表进行自动映射,使用 API 进行数据库操作时,ORM 框架会自动执行 SQL 语句并将查询结果转换成实体对象,程序员无需再手动编写 SQL 语句和转换代码。

下面是半自动和全自动 ORM 框架的区别:

  1. 映射方式:半自动 ORM 框架需要程序员手动指定实体类和数据表之间的映射关系,通常使用 XML 文件或注解方式来指定;全自动 ORM 框架则可以自动进行实体类和数据表的映射,无需手动干预。
  2. 查询方式:半自动 ORM 框架通常需要程序员手动编写 SQL 语句并将查询结果集转换成实体对象;全自动 ORM 框架可以自动组装 SQL 语句、执行查询操作,并将查询结果转换成实体对象。
  3. 性能:由于半自动 ORM 框架需要手动编写 SQL 语句,因此程序员必须对 SQL 语句和数据库的底层知识有一定的了解,才能编写高效的 SQL 语句;而全自动 ORM 框架通过自动优化生成的 SQL 语句来提高性能,程序员无需进行优化。
  4. 学习成本:半自动 ORM 框架需要程序员手动编写 SQL 语句和映射配置,要求程序员具备较高的数据库和 SQL 知识;全自动 ORM 框架可以自动生成 SQL 语句和映射配置,程序员无需了解过多的数据库和 SQL 知识。

常见的半自动 ORM 框架包括 MyBatis 等;常见的全自动 ORM 框架包括 Hibernate、Spring Data JPA、MyBatis-Plus 等。

3.2 逆向工程

MyBatis 的逆向工程是一种自动化生成持久层代码和映射文件的工具,它可以根据数据库表结构和设置的参数生成对应的实体类、Mapper.xml 文件、Mapper 接口等代码文件,简化了开发者手动生成的过程。逆向工程使开发者可以快速地构建起 DAO 层,并快速上手进行业务开发。

MyBatis 的逆向工程有两种方式:

  1. 通过 MyBatis Generator 插件实现
  2. 通过 Maven 插件实现

无论是哪种方式,逆向工程一般需要指定一些配置参数,例如数据库连接 URL、用户名、密码、要生成的表名、生成的文件路径等等。

总的来说,MyBatis 的逆向工程为程序员提供了一种方便快捷的方式,能够快速地生成持久层代码和映射文件,是半自动ORM思维向全自动ORM思维发展的过程,提高程序员的开发效率。

注意:逆向工程只能生成单表CRUD的操作,多表查询依然需要自己编写。

3.3 🆕MyBatisX逆向工程插件的使用

使用MyBatisX逆向工程插件,可以自动根据数据库生成对应的实体类和service、mapper。

(§1) 安装插件

在 IntelliJ IDEA 中打开插件市场,搜索 MyBatisX 插件并安装。

(§2) 使用 IntelliJ IDEA连接数据库
§① 连接数据库

image_yKs2Z2_8sQ

§② 填写数据库连接信息

image_bDboqlZFKD

§③ 展示库表

image_mCMuBhwZl2

§④ 逆向工程使用

image_DkwlIx_BM9

image_Zvkvn8wk_V

image_KXYfK5CQd-

#Part2 MyBatis-Plus

一、MyBatis-Plus简介

1. 简介

MyBatis-Plus(简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

官方文档:https://baomidou.com/introduce/

特性:

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

支持数据库:

  • MySQL,Oracle,DB2,H2,HSQL,SQLite,PostgreSQL,SQLServer,Phoenix,Gauss ,ClickHouse,Sybase,OceanBase,Firebird,Cubrid,Goldilocks,csiidb,informix,TDengine,redshift
  • 达梦数据库,虚谷数据库,人大金仓数据库,南大通用(华库)数据库,南大通用数据库,神通数据库,瀚高数据库,优炫数据库

mybatis-plus总结:

  • 自动生成单表的CRUD功能
  • 提供丰富的条件拼接方式
  • 全自动ORM类型持久层框架

2. 🌕快速入门

§2.1 创建数据库

现有一张 User 表,其表结构如下:

id name age email
1 Jone 18 test1@baomidou.com
2 Jack 20 test2@baomidou.com
3 Tom 28 test3@baomidou.com
4 Sandy 21 test4@baomidou.com
5 Billie 24 test5@baomidou.com
DROP TABLE IF EXISTS user;

CREATE TABLE user
(
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    age INT(11) NULL DEFAULT NULL COMMENT '年龄',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
    PRIMARY KEY (id)
);

INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');

§2.2 导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- SpringBoot父工程 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.5</version>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>springboot-starter-mybatis-plus-06</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- SpringBoot启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        
        <!-- 测试环境 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        
        <!-- mybatis-plus启动器  -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>

        <!-- 数据库相关配置启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!-- druid启动器的依赖  -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-3-starter</artifactId>
            <version>1.2.18</version>
        </dependency>

        <!-- 驱动类-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>
        
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
        </dependency>

    </dependencies>

    <!--    SpringBoot应用打包插件-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    
</project>

§2.3 配置文件

# 连接池配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      url: jdbc:mysql:///day01
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver

§2.4 启动类

@MapperScan("com.example.mapper")
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class,args);
    }
    
}

§2.5 实体类

@Data
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

§2.6 ☀️Mapper接口

创建 mapper包 下的 UserMapper接口,并使其继承自 BaseMapper<实体类>

BaseMapper 中提供了单表的CRUD操作。至此MyBatis-Plus的使用已完成。

public interface UserMapper extends BaseMapper<User> {

}

§2.7 测试使用

@SpringBootTest //springboot下测试环境注解
public class SampleTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testSelect() {
        System.out.println(("----- selectAll method test ------"));
        List<User> userList = userMapper.selectList(null);
        userList.forEach(System.out::println);
    }
}

二、MyBatis-Plus核心功能

0. Mybatis-Plus的配置

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.loggong.stdout.StdOutImpl	# 使用控制台输出sql语句(可选)
    map-underscore-to-camel-case: true	# 开启驼峰式自动映射(mybatis-plus默认为开启,这里不配置也行)

1. 基于Mapper接口CRUD

通用 CRUD 封装在 BaseMapper 接口中, Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器,内部包含常见的单表操作

官方持久层接口文档:https://baomidou.com/guides/data-interface/#mapper-interface

1.1 insert

// 插入一条记录
int insert(T entity);

功能描述: 插入一条记录。

返回值: int,表示插入操作影响的行数,通常为 1,表示插入成功。

参数说明:

类型 参数名 描述
T entity 实体对象

示例(insert):

User user = new User();

user.setName("John Doe");

user.setEmail("john.doe@example.com");

int rows = userMapper.insert(user); // 调用 insert 方法

if (rows > 0) {

 System.out.println("User inserted successfully.");

} else {

 System.out.println("Failed to insert user.");

}

生成的 SQL:

INSERT INTO user (name, email) VALUES (?, ?)

1.2 delete

// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);

// 删除(根据ID 批量删除)
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);

// 根据 ID 删除
int deleteById(Serializable id);

// 根据 columnMap 条件,删除记录
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

功能描述: 删除符合条件的记录。

返回值: int,表示删除操作影响的行数,通常为 1,表示删除成功。

参数说明:

类型 参数名 描述
Wrapper<T> wrapper 实体对象封装操作类(可以为 null)
Collection<? extends Serializable> idList 主键 ID 列表(不能为 null 以及 empty)
Serializable id 主键 ID
Map<String, Object> columnMap 表字段 map 对象

示例(delete):

// 假设有一个 QueryWrapper 对象,设置查询条件为 age > 25,删除满足条件的用户

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 25);

int rows = userMapper.delete(queryWrapper); // 调用 delete 方法

if (rows > 0) {

 System.out.println("Users deleted successfully.");

} else {

 System.out.println("No users deleted.");

}

生成的 SQL:

DELETE FROM user WHERE age > 25

示例(deleteBatchIds):

// 假设有一组 ID 列表,批量删除用户

List<Integer> ids = Arrays.asList(1, 2, 3);

int rows = userMapper.deleteBatchIds(ids); // 调用 deleteBatchIds 方法

if (rows > 0) {

 System.out.println("Users deleted successfully.");

} else {

 System.out.println("No users deleted.");

}

生成的 SQL:

DELETE FROM user WHERE id IN (1, 2, 3)

示例(deleteById):

// 根据 ID 删除单个用户

int userId = 1;

int rows = userMapper.deleteById(userId); // 调用 deleteById 方法

if (rows > 0) {

 System.out.println("User deleted successfully.");

} else {

 System.out.println("No user deleted.");

}

生成的 SQL:

DELETE FROM user WHERE id = 1

示例(deleteByMap):

// 假设有一个 columnMap,设置查询条件为 age = 30,删除满足条件的用户

Map<String, Object> columnMap = new HashMap<>();

columnMap.put("age", 30);

int rows = userMapper.deleteByMap(columnMap); // 调用 deleteByMap 方法

if (rows > 0) {

 System.out.println("Users deleted successfully.");

} else {

 System.out.println("No users deleted.");

}

生成的 SQL:

DELETE FROM user WHERE age = 30

1.3 update

// 根据 whereWrapper 条件,更新记录
int update(@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper<T> whereWrapper);

// 根据 ID 修改
int updateById(@Param(Constants.ENTITY) T entity);

功能描述: 更新符合条件的记录。

返回值: int,表示更新操作影响的行数,通常为 1,表示更新成功。

参数说明:

类型 参数名 描述
T entity 实体对象 (set 条件值,可为 null)
Wrapper<T> updateWrapper 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)

示例(update):

// 假设有一个 UpdateWrapper 对象,设置查询条件为 age > 25,更新满足条件的用户的邮箱

UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();

updateWrapper.gt("age", 25);

User updateUser = new User();

updateUser.setEmail("new.email@example.com");

int rows = userMapper.update(updateUser, updateWrapper); // 调用 update 方法

if (rows > 0) {

 System.out.println("Users updated successfully.");

} else {

 System.out.println("No users updated.");

}

生成的 SQL:

UPDATE user SET email = ? WHERE age > 25

示例(updateById):

// 假设要更新 ID 为 1 的用户的邮箱

User updateUser = new User();

updateUser.setId(1);

updateUser.setEmail("new.email@example.com");

int rows = userMapper.updateById(updateUser); // 调用 updateById 方法

if (rows > 0) {

 System.out.println("User updated successfully.");

} else {

 System.out.println("No user updated.");

}

生成的 SQL:

UPDATE user SET email = ? WHERE id = 1

1.4 select

// 根据 ID 查询
T selectById(Serializable id);

// 根据 entity 条件,查询一条记录
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);



// 查询(根据ID 批量查询)
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);

// 根据 entity 条件,查询全部记录
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 查询(根据 columnMap 条件)
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

// 根据 Wrapper 条件,查询全部记录
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);



// 根据 entity 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 根据 Wrapper 条件,查询总记录数
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

功能描述: 查询符合条件的记录。

返回值: 查询结果,可能是实体对象、Map 对象或其他类型。

参数说明:

类型 参数名 描述
Serializable id 主键 ID
Wrapper<T> queryWrapper 实体对象封装操作类(可以为 null)
Collection<? extends Serializable> idList 主键 ID 列表(不能为 null 以及 empty)
Map<String, Object> columnMap 表字段 map 对象
IPage<T> page 分页查询条件(可以为 RowBounds.DEFAULT)

示例(selectById):

// 根据 ID 查询单个用户

int userId = 1;

User user = userMapper.selectById(userId); // 调用 selectById 方法

System.out.println("User: " + user);

生成的 SQL:

SELECT * FROM user WHERE id = 1

示例(selectOne):

// 假设有一个 QueryWrapper 对象,设置查询条件为 age > 25,查询一条满足条件的用户

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 25);

User user = userMapper.selectOne(queryWrapper); // 调用 selectOne 方法

System.out.println("User: " + user);

生成的 SQL:

SELECT * FROM user WHERE age > 25

示例(selectBatchIds):

// 假设有一组 ID 列表,批量查询用户

List<Integer> ids = Arrays.asList(1, 2, 3);

List<User> users = userMapper.selectBatchIds(ids); // 调用 selectBatchIds 方法

for (User u : users) {

 System.out.println("User: " + u);

}

生成的 SQL:

SELECT * FROM user WHERE id IN (1, 2, 3)

示例(selectList):

// 假设有一个 QueryWrapper 对象,设置查询条件为 age > 25,查询所有满足条件的用户

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 25);

List<User> users = userMapper.selectList(queryWrapper); // 调用 selectList 方法

for (User u : users) {

 System.out.println("User: " + u);

}

生成的 SQL:

SELECT * FROM user WHERE age > 25

示例(selectByMap):

// 假设有一个 columnMap,设置查询条件为 age > 30,查询满足条件的用户

Map<String, Object> columnMap = new HashMap<>();

columnMap.put("age", 30);

List<User> users = userMapper.selectByMap(columnMap); // 调用 selectByMap 方法

for (User u : users) {

 System.out.println("User: " + u);

}

生成的 SQL:

SELECT * FROM user WHERE age > 30

示例(selectMaps):

// 假设有一个 QueryWrapper 对象,设置查询条件为 age > 25,查询所有满足条件的用户,并将结果映射为 Map

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 25);

List<Map<String, Object>> userMaps = userMapper.selectMaps(queryWrapper); // 调用 selectMaps 方法

for (Map<String, Object> userMap : userMaps) {

 System.out.println("User Map: " + userMap);

}

生成的 SQL:

SELECT * FROM user WHERE age > 25

示例(selectObjs):

// 假设有一个 QueryWrapper 对象,设置查询条件为 age > 25,查询所有满足条件的用户,但只返回每个记录的第一个字段的值

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 25);

List<Object> userIds = userMapper.selectObjs(queryWrapper); // 调用 selectObjs 方法

for (Object userId : userIds) {

 System.out.println("User ID: " + userId);

}

生成的 SQL:

SELECT id FROM user WHERE age > 25

示例(selectPage):

// 假设要进行分页查询,每页显示10条记录,查询第1页,查询条件为 age > 25

IPage<User> page = new Page<>(1, 10);

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 25);

IPage<User> userPage = userMapper.selectPage(page, queryWrapper); // 调用 selectPage 方法

List<User> userList = userPage.getRecords();

long total = userPage.getTotal();

System.out.println("Total users (age > 25): " + total);

for (User user : userList) {

 System.out.println("User: " + user);

}

生成的 SQL:

SELECT * FROM user WHERE age > 25 LIMIT 10 OFFSET 0

示例(selectMapsPage):

// 假设要进行分页查询,每页显示10条记录,查询第1页,查询条件为 age > 25,并将结果映射为 Map

IPage<Map<String, Object>> = new Page<>(1, 10);

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 25);

IPage<Map<String, Object>> userPageMaps = userMapper.selectMapsPage(page, queryWrapper); // 调用 selectMapsPage 方法

List<Map<String, Object>> userMapList = userPageMaps.getRecords();

long total = userPageMaps.getTotal();

System.out.println("Total users (age > 25): " + total);

for (Map<String, Object> userMap : userMapList) {

 System.out.println("User Map: " + userMap);

}

生成的 SQL:

SELECT * FROM user WHERE age > 25 LIMIT 10 OFFSET 0

示例(selectCount):

// 假设有一个 QueryWrapper 对象,设置查询条件为 age > 25,查询总记录数

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 25);

Integer totalUsers = userMapper.selectCount(queryWrapper); // 调用 selectCount 方法

System.out.println("Total users (age > 25): " + totalUsers);

生成的 SQL:

SELECT COUNT(*) FROM user WHERE age > 25

2. 基于Service接口CRUD

通用 Service CRUD 封装 IService 接口,进一步封装 CRUD 采用以下前缀命名方式区分 Mapper 层避免混淆:

  • get:查询单行
  • remove:删除
  • list:查询集合
  • page:分页

对比Mapper接口CRUD区别:

  • service添加了批量方法
  • service层的方法自动添加事务

官方服务层接口文档:https://baomidou.com/guides/data-interface/#service-interface

2.0 IService接口使用方式

IService接口提供了一半的CRUD方法实现,另一半在ServiceImpl类中实现。

  1. 接口继承IService接口:

    public interface UserService extends IService<User> {
    
    }
    
  2. 接口实现类继承ServiceImpl实现类:

    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService{
    
    }
    

2.1 save

// 插入一条记录(选择字段,策略插入)
boolean save(T entity);

// 插入(批量)
boolean saveBatch(Collection<T> entityList);

// 插入(批量)
boolean saveBatch(Collection<T> entityList, int batchSize);

功能描述: 插入记录,根据实体对象的字段进行策略性插入。

返回值: boolean,表示插入操作是否成功。

参数说明:

类型 参数名 描述
T entity 实体对象
Collection<T> entityList 实体对象集合
int batchSize 插入批次数量

示例(save):

// 假设有一个 User 实体对象

User user = new User();

user.setName("John Doe");

user.setEmail("john.doe@example.com");

boolean result = userService.save(user); // 调用 save 方法

if (result) {

 System.out.println("User saved successfully.");

} else {

 System.out.println("Failed to save user.");

}

生成的 SQL:

INSERT INTO user (name, email) VALUES ('John Doe', 'john.doe@example.com')

示例(saveBatch):

// 假设有一组 User 实体对象

List<User> users = Arrays.asList(

 new User("Alice", "alice@example.com"),

 new User("Bob", "bob@example.com"),

 new User("Charlie", "charlie@example.com")

);

// 使用默认批次大小进行批量插入

boolean result = userService.saveBatch(users); // 调用 saveBatch 方法,默认批次大小

if (result) {

 System.out.println("Users saved successfully.");

} else {

 System.out.println("Failed to save users.");

}

生成的 SQL(假设默认批次大小为 3):

INSERT INTO user (name, email) VALUES

('Alice', 'alice@example.com'),

('Bob', 'bob@example.com'),

('Charlie', 'charlie@example.com')

示例(saveBatch 指定批次大小):

// 假设有一组 User 实体对象

List<User> users = Arrays.asList(

 new User("David", "david@example.com"),

 new User("Eve", "eve@example.com"),

 new User("Frank", "frank@example.com"),

 new User("Grace", "grace@example.com")

);

// 指定批次大小为 2进行批量插入

boolean result = userService.saveBatch(users, 2); // 调用 saveBatch 方法,指定批次大小

if (result) {

 System.out.println("Users saved successfully.");

} else {

 System.out.println("Failed to save users.");

}

生成的 SQL(指定批次大小为 2):

-- 第一批次

INSERT INTO user (name, email) VALUES

('David', 'david@example.com'),

('Eve', 'eve@example.com')




-- 第二批次

INSERT INTO user (name, email) VALUES

('Frank', 'frank@example.com'),

('Grace', 'grace@example.com')

2.2 saveOrUpdate

// 主键属性值存在则更新记录,否则插入一条记录
boolean saveOrUpdate(T entity);

// 根据updateWrapper尝试更新,否继续执行saveOrUpdate(T)方法
boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper);

// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList);

// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);

功能描述: 根据实体对象的主键 ID 进行判断,存在则更新记录,否则插入记录。

返回值: boolean,表示插入或更新操作是否成功。

参数说明:

类型 参数名 描述
T entity 实体对象
Wrapper<T> updateWrapper 实体对象封装操作类 UpdateWrapper
Collection<T> entityList 实体对象集合
int batchSize 插入批次数量

示例(saveOrUpdate):

// 假设有一个 User 实体对象,其中 id 是 TableId 注解的属性

User user = new User();

user.setId(1);

user.setName("John Doe");

user.setEmail("john.doe@example.com");

boolean result = userService.saveOrUpdate(user); // 调用 saveOrUpdate 方法

if (result) {

 System.out.println("User updated or saved successfully.");

} else {

 System.out.println("Failed to update or save user.");

}

生成的 SQL(假设 id 为 1 的记录已存在):

UPDATE user SET name = 'John Doe', email = 'john.doe@example.com' WHERE id = 1

生成的 SQL(假设 id 为 1 的记录不存在):

INSERT INTO user (id, name, email) VALUES (1, 'John Doe', 'john.doe@example.com')

示例(saveOrUpdateBatch):

// 假设有一组 User 实体对象,每个对象都有 id 属性

List<User> users = Arrays.asList(

 new User(1, "Alice", "alice@example.com"),

 new User(2, "Bob", "bob@example.com"),

 new User(3, "Charlie", "charlie@example.com")

);

// 使用默认批次大小进行批量修改插入

boolean result = userService.saveOrUpdateBatch(users); // 调用 saveOrUpdateBatch 方法,默认批次大小

if (result) {

 System.out.println("Users updated or saved successfully.");

} else {

 System.out.println("Failed to update or save users.");

}

生成的 SQL(假设 id 为 1 和 2 的记录已存在,id 为 3 的记录不存在):

UPDATE user SET name = 'Alice', email = 'alice@example.com' WHERE id = 1

UPDATE user SET name = 'Bob', email = 'bob@example.com' WHERE id = 2

INSERT INTO user (id, name, email) VALUES (3, 'Charlie', 'charlie@example.com')

示例(saveOrUpdateBatch 指定批次大小):

// 假设有一组 User 实体对象

List<User> users = Arrays.asList(

 new User(4, "David", "david@example.com"),

 new User(5, "Eve", "eve@example.com"),

 new User(6, "Frank", "frank@example.com")

);

// 指定批次大小为 2进行批量修改插入

boolean result = userService.saveOrUpdateBatch(users, 2); // 调用 saveOrUpdateBatch 方法,指定批次大小

if (result) {

 System.out.println("Users updated or saved successfully.");

} else {

 System.out.println("Failed to update or save users.");

}

生成的 SQL(假设指定批次大小为 2):

-- 第一批次

UPDATE user SET name = 'David', email = 'david@example.com' WHERE id = 4

UPDATE user SET name = 'Eve', email = 'eve@example.com' WHERE id = 5


-- 第二批次

INSERT INTO user (id, name, email) VALUES (6, 'Frank', 'frank@example.com')

2.3 remove

// 根据 queryWrapper 设置的条件,删除记录
boolean remove(Wrapper<T> queryWrapper);

// 根据 ID 删除
boolean removeById(Serializable id);

// 根据 columnMap 条件,删除记录
boolean removeByMap(Map<String, Object> columnMap);

// 删除(根据ID 批量删除)
boolean removeByIds(Collection<? extends Serializable> idList);

功能描述: 通过指定条件删除符合条件的记录。

返回值: boolean,表示删除操作是否成功。

参数说明:

类型 参数名 描述
Wrapper<T> queryWrapper 实体包装类 QueryWrapper
Serializable id 主键 ID
Map<String, Object> columnMap 表字段 map 对象
Collection<? extends Serializable> idList 主键 ID 列表

示例(remove):

// 假设有一个 QueryWrapper 对象,设置删除条件为 name = 'John Doe'

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.eq("name", "John Doe");

boolean result = userService.remove(queryWrapper); // 调用 remove 方法

if (result) {

 System.out.println("Record deleted successfully.");

} else {

 System.out.println("Failed to delete record.");

}

生成的 SQL:

DELETE FROM user WHERE name = 'John Doe'

示例(removeById):

// 假设要删除 ID 为 1 的用户

boolean result = userService.removeById(1); // 调用 removeById 方法

if (result) {

 System.out.println("User deleted successfully.");

} else {

 System.out.println("Failed to delete user.");

}

生成的 SQL:

DELETE FROM user WHERE id = 1

示例(removeByMap):

// 假设有一个 columnMap,设置删除条件为 age = 30

Map<String, Object> columnMap = new HashMap<>();

columnMap.put("age", 30);

boolean result = userService.removeByMap(columnMap); // 调用 removeByMap 方法

if (result) {

 System.out.println("Records deleted successfully.");

} else {

 System.out.println("Failed to delete records.");

}

生成的 SQL:

DELETE FROM user WHERE age = 30

示例(removeByIds):

// 假设有一组 ID 列表,批量删除用户

List<Integer> ids = Arrays.asList(1, 2, 3);

boolean result = userService.removeByIds(ids); // 调用 removeByIds 方法

if (result) {

 System.out.println("Users deleted successfully.");

} else {

 System.out.println("Failed to delete users.");

}

生成的 SQL:

DELETE FROM user WHERE id IN (1, 2, 3)

2.4 update

// 根据 UpdateWrapper 条件,更新记录 需要设置sqlset
boolean update(Wrapper<T> updateWrapper);

// 根据 whereWrapper 条件,更新记录
boolean update(T updateEntity, Wrapper<T> whereWrapper);

// 根据 ID 选择修改
boolean updateById(T entity);

// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList);

// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList, int batchSize);

功能描述: 通过指定条件更新符合条件的记录。

返回值: boolean,表示更新操作是否成功。

参数说明:

类型 参数名 描述
Wrapper<T> updateWrapper 实体对象封装操作类 UpdateWrapper
T entity 实体对象
Collection<T> entityList 实体对象集合
int batchSize 更新批次数量

示例(update UpdateWrapper 形式):

// 假设有一个 UpdateWrapper 对象,设置更新条件为 name = 'John Doe',更新字段为 email

UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();

updateWrapper.eq("name", "John Doe").set("email", "john.doe@newdomain.com");

boolean result = userService.update(updateWrapper); // 调用 update 方法

if (result) {

 System.out.println("Record updated successfully.");

} else {

 System.out.println("Failed to update record.");

}

生成的 SQL:

UPDATE user SET email = 'john.doe@newdomain.com' WHERE name = 'John Doe'

示例(update WhereWrapper 形式):

// 假设有一个 User 实体对象,设置更新字段为 name,以及一个 whereWrapper 设置更新条件为 id = 1

User updateEntity = new User();

updateEntity.setName("Updated Name");

QueryWrapper<User> whereWrapper = new QueryWrapper<>();

whereWrapper.eq("id", 1);

boolean result = userService.update(updateEntity, whereWrapper); // 调用 update 方法

if (result) {

 System.out.println("Record updated successfully.");

} else {

 System.out.println("Failed to update record.");

}

生成的 SQL:

UPDATE user SET name = 'Updated Name' WHERE id = 1

示例(updateById):

// 假设有一个 User 实体对象,设置更新字段为 email,根据 ID 更新

User updateEntity = new User();

updateEntity.setId(1);

updateEntity.setEmail("updated.email@example.com");

boolean result = userService.updateById(updateEntity); // 调用 updateById 方法

if (result) {

 System.out.println("Record updated successfully.");

} else {

 System.out.println("Failed to update record.");

}

生成的 SQL:

UPDATE user SET email = 'updated.email@example.com' WHERE id = 1

示例(updateBatchById):

// 假设有一组 User 实体对象,批量更新

List<User> users = Arrays.asList(

 new User(1, null, "new.email1@example.com"),

 new User(2, null, "new.email2@example.com")

);

boolean result = userService.updateBatchById(users); // 调用 updateBatchById 方法,默认批次大小

if (result) {

 System.out.println("Records updated successfully.");

} else {

 System.out.println("Failed to update records.");

}

生成的 SQL(假设默认批次大小为 2):

UPDATE user SET email = 'new.email1@example.com' WHERE id = 1

UPDATE user SET email = 'new.email2@example.com' WHERE id = 2

示例(updateBatchById 指定批次大小):

// 假设有一组 User 实体对象,批量更新,并指定批次大小为 1

List<User> users = Arrays.asList(

 new User(1, null, "new.email1@example.com"),

 new User(2, null, "new.email2@example.com")

);

boolean result = userService.updateBatchById(users, 1); // 调用 updateBatchById 方法,指定批次大小

if (result) {

 System.out.println("Records updated successfully.");

} else {

 System.out.println("Failed to update records.");

}

生成的 SQL(假设指定批次大小为 1):

-- 第一批次

UPDATE user SET email = 'new.email1@example.com' WHERE id = 1

-- 第二批次

UPDATE user SET email = 'new.email2@example.com' WHERE id = 2

2.5 get

// 根据 ID 查询
T getById(Serializable id);

// 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")
T getOne(Wrapper<T> queryWrapper);

// 根据 Wrapper,查询一条记录
T getOne(Wrapper<T> queryWrapper, boolean throwEx);

// 根据 Wrapper,查询一条记录
Map<String, Object> getMap(Wrapper<T> queryWrapper);

// 根据 Wrapper,查询一条记录
<V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

功能描述: 根据指定条件查询符合条件的记录。

返回值: 查询结果,可能是实体对象、Map 对象或其他类型。

参数说明:

类型 参数名 描述
Serializable id 主键 ID
Wrapper queryWrapper 实体对象封装操作类 QueryWrapper
boolean throwEx 有多个 result 是否抛出异常
T entity 实体对象
Function<? super Object, V> mapper 转换函数

示例(getById):

// 假设要查询 ID 为 1 的用户

User user = userService.getById(1); // 调用 getById 方法

if (user != null) {

 System.out.println("User found: " + user);

} else {

 System.out.println("User not found.");

}

生成的 SQL:

SELECT * FROM user WHERE id = 1

示例(getOne):

// 假设有一个 QueryWrapper 对象,设置查询条件为 name = 'John Doe'

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.eq("name", "John Doe");

User user = userService.getOne(queryWrapper); // 调用 getOne 方法

if (user != null) {

 System.out.println("User found: " + user);

} else {

 System.out.println("User not found.");

}

生成的 SQL:

SELECT * FROM user WHERE name = 'John Doe' LIMIT 1

示例(getOne 不抛出异常):

// 假设有一个 QueryWrapper 对象,设置查询条件为 name = 'John Doe',并且不抛出异常

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.eq("name", "John Doe");

User user = userService.getOne(queryWrapper, false); // 调用 getOne 方法

if (user != null) {

 System.out.println("User found: " + user);

} else {

 System.out.println("User not found.");

}

生成的 SQL:

SELECT * FROM user WHERE name = 'John Doe'

示例(getMap):

// 假设有一个 QueryWrapper 对象,设置查询条件为 name = 'John Doe',并将结果映射为 Map

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.eq("name", "John Doe");

Map<String, Object> userMap = userService.getMap(queryWrapper); // 调用 getMap 方法

if (userMap != null) {

 System.out.println("User found: " + userMap);

} else {

 System.out.println("User not found.");

}

生成的 SQL:

SELECT * FROM user WHERE name = 'John Doe' LIMIT 1

示例(getObj):

// 假设有一个 QueryWrapper 对象,设置查询条件为 name = 'John Doe',并将结果转换为 String

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.eq("name", "John Doe");

String userName = userService.getObj(queryWrapper, obj -> ((User) obj).getName()); // 调用 getObj 方法

if (userName != null) {

 System.out.println("User name found: " + userName);

} else {

 System.out.println("User name not found.");

}

生成的 SQL:

SELECT * FROM user WHERE name = 'John Doe' LIMIT 1

2.6 list

// 查询所有
List<T> list();

// 查询列表
List<T> list(Wrapper<T> queryWrapper);

// 查询(根据ID 批量查询)
Collection<T> listByIds(Collection<? extends Serializable> idList);

// 查询(根据 columnMap 条件)
Collection<T> listByMap(Map<String, Object> columnMap);

// 查询所有列表
List<Map<String, Object>> listMaps();

// 查询列表
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);

// 查询全部记录
List<Object> listObjs();

// 查询全部记录
<V> List<V> listObjs(Function<? super Object, V> mapper);

// 根据 Wrapper 条件,查询全部记录
List<Object> listObjs(Wrapper<T> queryWrapper);

// 根据 Wrapper 条件,查询全部记录
<V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

功能描述: 查询符合条件的记录。

返回值: 查询结果,可能是实体对象、Map 对象或其他类型。

参数说明:

类型 参数名 描述
Wrapper queryWrapper 实体对象封装操作类 QueryWrapper
Collection<? extends Serializable> idList 主键 ID 列表
Map<String, Object> columnMap 表字段 map 对象
Function<? super Object, V> mapper 转换函数

示例(list):

// 查询所有用户

List<User> users = userService.list(); // 调用 list 方法

for (User user : users) {

 System.out.println("User: " + user);

}

生成的 SQL:

SELECT * FROM user

示例(list QueryWrapper 形式):

// 假设有一个 QueryWrapper 对象,设置查询条件为 age > 25

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 25);

List<User> users = userService.list(queryWrapper); // 调用 list 方法

for (User user : users) {

 System.out.println("User: " + user);

}

生成的 SQL:

SELECT * FROM user WHERE age > 25

示例(listByIds):

// 假设有一组 ID 列表,批量查询用户

List<Integer> ids = Arrays.asList(1, 2, 3);

Collection<User> users = userService.listByIds(ids); // 调用 listByIds 方法

for (User user : users) {

 System.out.println("User: " + user);

}

生成的 SQL:

SELECT * FROM user WHERE id IN (1, 2, 3)

示例(listByMap):

// 假设有一个 columnMap,设置查询条件为 age = 30

Map<String, Object> columnMap = new HashMap<>();

columnMap.put("age", 30);

Collection<User> users = userService.listByMap(columnMap); // 调用 listByMap 方法

for (User user : users) {

 System.out.println("User: " + user);

}

生成的 SQL:

SELECT * FROM user WHERE age = 30

示例(listMaps):

// 查询所有用户,并将结果映射为 Map

List<Map<String, Object>> userMaps = userService.listMaps(); // 调用 listMaps 方法

for (Map<String, Object> userMap : userMaps) {

 System.out.println("User Map: " + userMap);

}

生成的 SQL:

SELECT * FROM user

示例(listMaps QueryWrapper 形式):

// 假设有一个 QueryWrapper 对象,设置查询条件为 age > 25,并将结果映射为 Map

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 25);

List<Map<String, Object>> userMaps = userService.listMaps(queryWrapper); // 调用 listMaps 方法

for (Map<String, Object> userMap : userMaps) {

 System.out.println("User Map: " + userMap);

}

生成的 SQL:

SELECT * FROM user WHERE age > 25

示例(listObjs):

// 查询所有用户,并将结果转换为 String 列表

List<String> userNames = userService.listObjs(obj -> ((User) obj).getName()); // 调用 listObjs 方法

for (String userName : userNames) {

 System.out.println("User Name: " + userName);

}

生成的 SQL:

SELECT * FROM user

示例(listObjs QueryWrapper 形式):

// 假设有一个 QueryWrapper 对象,设置查询条件为 age > 25,并将结果转换为 String 列表

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 25);

List<String> userNames = userService.listObjs(queryWrapper, obj -> ((User) obj).getName()); // 调用 listObjs 方法

for (String userName : userNames) {

 System.out.println("User Name: " + userName);

}

生成的 SQL:

SELECT * FROM user WHERE age > 25

2.7 page

// 无条件分页查询
IPage<T> page(IPage<T> page);

// 条件分页查询
IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper);

// 无条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page);

// 条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper);

功能描述: 分页查询符合条件的记录。

返回值: 分页查询结果,包含记录列表和总记录数。

参数说明:

类型 参数名 描述
IPage page 翻页对象
Wrapper queryWrapper 实体对象封装操作类 QueryWrapper

示例(page):

// 假设要进行无条件的分页查询,每页显示10条记录,查询第1页

IPage<User> page = new Page<>(1, 10);

IPage<User> userPage = userService.page(page); // 调用 page 方法

List<User> userList = userPage.getRecords();

long total = userPage.getTotal();

System.out.println("Total users: " + total);

for (User user : userList) {

 System.out.println("User: " + user);

}

生成的 SQL:

SELECT * FROM user LIMIT 10 OFFSET 0

示例(page QueryWrapper 形式):

// 假设有一个 QueryWrapper 对象,设置查询条件为 age > 25,进行有条件的分页查询

IPage<User> page = new Page<>(1, 10);

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 25);

IPage<User> userPage = userService.page(page, queryWrapper); // 调用 page 方法

List<User> userList = userPage.getRecords();

long total = userPage.getTotal();

System.out.println("Total users (age > 25): " + total);

for (User user : userList) {

 System.out.println("User: " + user);

}

生成的 SQL:

SELECT * FROM user WHERE age > 25 LIMIT 10 OFFSET 0

示例(pageMaps):

// 假设要进行无条件的分页查询,并将结果映射为 Map,每页显示10条记录,查询第1页

IPage<Map<String, Object>> page = new Page<>(1, 10);

IPage<Map<String, Object>> userPageMaps = userService.pageMaps(page); // 调用 pageMaps 方法

List<Map<String, Object>> userMapList = userPageMaps.getRecords();

long total = userPageMaps.getTotal();

System.out.println("Total users: " + total);

for (Map<String, Object> userMap : userMapList) {

 System.out.println("User Map: " + userMap);

}

生成的 SQL:

SELECT * FROM user LIMIT 10 OFFSET 0

示例(pageMaps QueryWrapper 形式):

// 假设有一个 QueryWrapper 对象,设置查询条件为 age > 25,进行有条件的分页查询,并将结果映射为 Map

IPage<Map<String, Object>> page = new Page<>(1, 10);

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 25);

IPage<Map<String, Object>> userPageMaps = userService.pageMaps(page, queryWrapper); // 调用 pageMaps 方法

List<Map<String, Object>> userMapList = userPageMaps.getRecords();

long total = userPageMaps.getTotal();

System.out.println("Total users (age > 25): " + total);

for (Map<String, Object> userMap : userMapList) {

 System.out.println("User Map: " + userMap);

}

生成的 SQL:

SELECT * FROM user WHERE age > 25 LIMIT 10 OFFSET 0

2.8 count

// 自3.4.3.2开始,返回值从int修改为long
// 查询总记录数
long count();

// 根据 Wrapper 条件,查询总记录数
long count(Wrapper<T> queryWrapper);

功能描述: 查询符合条件的记录总数。

返回值: 符合条件的记录总数。

参数说明:

类型 参数名 描述
Wrapper queryWrapper 实体对象封装操作类 QueryWrapper

示例(count):

// 查询用户表中的总记录数

int totalUsers = userService.count(); // 调用 count 方法

System.out.println("Total users: " + totalUsers);

生成的 SQL:

SELECT COUNT(*) FROM user

示例(count QueryWrapper 形式):

// 假设有一个 QueryWrapper 对象,设置查询条件为 age > 25,查询满足条件的用户总数

QueryWrapper<User> queryWrapper = new QueryWrapper<>();

queryWrapper.gt("age", 25);

int totalUsers = userService.count(queryWrapper); // 调用 count 方法

System.out.println("Total users (age > 25): " + totalUsers);

生成的 SQL:

SELECT COUNT(*) FROM user WHERE age > 25

3. 分页查询实现

官方文档:https://baomidou.com/plugins/pagination/

3.1 官方分页插件

(§1) 导入分页插件

在配置类中,定义一个方法,在 MybatisPlusInterceptor 插件中添加分页插件 PaginationInnerInterceptor

@MapperScan("com.example.mapper")
@SpringBootApplication	// 其中包含@Configuration配置类
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class,args);
    }
    
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // mybatis-plus的插件集合对象
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
(§2) 使用分页查询
@SpringBootTest
public class MybatisPlusTest {
    
    @Autowired
    private UserMapper userMapper;
    
    @Test
    public void testPageQuery(){
        // 设置分页参数(页码, 页容量)
        Page<User> page = new Page<>(1, 5);
        userMapper.selectPage(page, null);
        
        // 获取当前分页的数据
        List<User> list = page.getRecords();
        list.forEach(System.out::println);
        
        System.out.println("当前页码:"+page.getCurrent());
        System.out.println("每页显示的条数:"+page.getSize());
        System.out.println("总记录数:"+page.getTotal());
        System.out.println("总页数:"+page.getPages());
        System.out.println("是否有上一页:"+page.hasPrevious());
        System.out.println("是否有下一页:"+page.hasNext());
    }
}

3.2 自定义分页

(§1) 定义Mapper接口
public interface UserVoMapper extends BaseMapper<UserVo> {
    
    // s1. 传入参数携带Ipage接口
    // s2. 返回值类型为IPage
    IPage<UserVo> selectPageVo(IPage<?> page, Integer state);
    // 或者自定义分页类
    MyPage selectPageVo(MyPage page);
    // 或者返回 List
    List<UserVo> selectPageVo(IPage<UserVo> page, Integer state);
}
(§2) 定义MapperXML接口实现

在yaml配置文件中,mybatis-plus.mapper-locations: classpath:/mapper/**.xml默认指定了MapperXML文件放在mapper目录中。

创建 resources/mapper目录 下的 UserVoMapper.xml接口实现文件

<mapper namespace="com.example.mapper.UserVoMapper">

    <!-- 注意不能以分号结尾 -->
    <!-- com.example.pojo.vo.UserVo可以在yaml文件中设置:mybatis-plus.type-aliases-package=com.example.pojo,
 		这样在此处就可以写别名“userVo” -->
    <select id="selectPageVo" resultType="com.example.pojo.vo.UserVo">
        SELECT id,name FROM user WHERE state=#{state}
    </select>

</mapper>
(§3) 使用分页查询
@SpringBootTest
public class MybatisPlusTest {
    
    @Autowired
    private UserMapper userMapper;
    
    @Test
    public void testPageQuery(){
        // 设置分页参数(页码, 页容量)
        IPage page = new Page<>(1, 5);
        userMapper.selectPageVo(page, null);
        
        // 获取当前分页的数据
        List<User> list = page.getRecords();
        list.forEach(System.out::println);
        
        System.out.println("当前页码:"+page.getCurrent());
        System.out.println("每页显示的条数:"+page.getSize());
        System.out.println("总记录数:"+page.getTotal());
        System.out.println("总页数:"+page.getPages());
        System.out.println("是否有上一页:"+page.hasPrevious());
        System.out.println("是否有下一页:"+page.hasNext());
    }
}

4. 条件构造器Wrapper

官方文档:https://baomidou.com/guides/wrapper/

4.0 条件构造器基本使用

(1) 代码示例
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", "John"); 			// 添加等于条件
queryWrapper.ne("age", 30); 				// 添加不等于条件
queryWrapper.like("email", "@gmail.com"); 	// 添加模糊匹配条件
/* 等同于: 
delete from user where name = "John" and age != 30 and email like "%@gmail.com%" */
// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
(2) 继承结构

image_JPU5SqXQMh

Wrapper:条件构造抽象类,最顶端父类

  • AbstractWrapper:用于查询条件封装,生成 sql 的 where 条件
    • QueryWrapper:查询/删除条件封装
    • UpdateWrapper:修改条件封装
    • AbstractLambdaWrapper:使用Lambda 语法
      • LambdaQueryWrapper:用于Lambda语法使用的查询Wrapper
      • LambdaUpdateWrapper:Lambda 更新封装Wrapper

4.1 基于QueryWrapper组装条件

image_k_cPdiIiy4

(1) 组装查询条件
@Test
public void test01(){
    // 查询用户名包含a,年龄在20到30之间,并且邮箱不为null的用户信息
    /* 
    	SELECT id,username AS name,age,email,is_deleted 
    	FROM t_user 
    	WHERE is_deleted=0 AND (username LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL) 
    */
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.like("username", "a")
            .between("age", 20, 30)
            .isNotNull("email");
    List<User> list = userMapper.selectList(queryWrapper);
    list.forEach(System.out::println);
}
(2) 组装排序条件
@Test
public void test02(){
    // 按年龄降序查询用户,如果年龄相同则按id升序排列
    /* 
    	SELECT id,username AS name,age,email,is_deleted 
    	FROM t_user 
    	WHERE is_deleted=0 
    	ORDER BY age DESC,id ASC 
    */
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper
            .orderByDesc("age")
            .orderByAsc("id");
    List<User> users = userMapper.selectList(queryWrapper);
    users.forEach(System.out::println);
}
(3) 组装删除条件
@Test
public void test03(){
    // 删除email为空的用户
    /* 
    	DELETE FROM t_user 
    	WHERE (email IS NULL)
    */
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.isNull("email");
    //条件构造器也可以构建删除语句的条件
    int result = userMapper.delete(queryWrapper);
    System.out.println("受影响的行数:" + result);
}
(4) 组装修改条件(and和or关键字使用)
@Test
public void test04() {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    // 将年龄大于20并且用户名中包含有a或邮箱为null的用户信息修改
    /* 
    	UPDATE t_user SET age=?, email=? 
    	WHERE username LIKE ? AND age > ? OR email IS NULL)
    */
    queryWrapper
            .like("username", "a")
            .gt("age", 20)	// 条件直接调用方法,默认使用and拼接
            .or().isNull("email");
    User user = new User();
    user.setAge(18);
    user.setEmail("user@example.com");
    int result = userMapper.update(user, queryWrapper);
    System.out.println("受影响的行数:" + result);
}
(5) 查询指定列
@Test
public void test05() {
    // 查询用户信息id>1的username和age字段
    /*
    	SELECT username,age 
    	FROM t_user
    	WHERE id > 1
    */
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.gt("id", 1L);
    queryWrapper.select("username", "age");
    // selectMaps()返回Map集合列表,通常配合select()使用,避免User对象中没有被查询到的列值为null
    List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
    maps.forEach(System.out::println);
}
(6) condition判断组织条件
@Test
public void test06(String name, int age){
    /* 前端传入两个参数name和age,实现:
    	当name不为空再作为条件查询(name=xxx)
    	当age>18再作为条件查询(age=xxx) */

    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    //方案1:手动判断
    if (StringUtils.isNotBlank(name)){
        queryWrapper.eq("name",name);
    }
    if (age > 18){
        queryWrapper.eq("age",age);
    }
    
    //方案2:拼接condition判断
    /* eq(condition, 列名, 值)
    	每个条件拼接方法都condition参数,这是一个比较运算,为true追加当前条件。
    */
    queryWrapper.eq(StringUtils.isNotBlank(name), "name", name)
            .eq(age > 1, "age", age);   
}

4.2 基于UpdateWrapper组装条件

使用queryWrapper + 实体类形式可以实现修改,但是无法将列值修改为null值,而使用updateWrapper可以随意设置列的值,包括null值。

@Test
public void test04() {
    // 将年龄大于20并且用户名中包含有a或邮箱为null的用户信息修改
    /* 
    	UPDATE t_user SET age=?, email=? 
    	WHERE username LIKE ? AND age > ? OR email IS NULL)
    */
    UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
    updateWrapper.like("username", "a")
            .gt("age", 20)
            .or().isNull("email");
    updateWrapper.set("age", 18)
        		.set("email", null)
    // 如果使用updateWrapper,实体对象写null即可
    int result = userMapper.update(null, updateWrapper);
    System.out.println("result = " + result);
}

4.3 基于LambdaQueryWrapper组装条件

(1) 方法引用和lambda表达式

方法引用是 Java 8 中引入的一种语法特性,它提供了一种简洁的方式来直接引用已有的方法或构造函数。方法引用可以替代 Lambda 表达式,使代码更简洁、更易读。

Java 8 支持以下几种方法引用的形式:

  1. 静态方法引用: 引用静态方法,语法为 类名::静态方法名
  2. 实例方法引用: 引用实例方法,语法为 实例对象::实例方法名
  3. 对象方法引用: 引用特定对象的实例方法,语法为 类名::实例方法名
  4. 构造函数引用: 引用构造函数,语法为 类名::new
(2) 使用案例

LambdaQueryWrapper支持使用方法引用的方式指定属性。

示例:组装查询条件

@Test
public void test01(){
 // 查询用户名包含a,年龄在20到30之间,并且邮箱不为null的用户信息
 /* 
 	SELECT id,username AS name,age,email,is_deleted 
 	FROM t_user 
 	WHERE is_deleted=0 AND (username LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL) 
 */
	LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
 lambdaQueryWrapper.like(User::getName, "a")	// 类名::方法名
         .between(User::getAge, 20, 30)
         .isNotNull(User::getEmail);
 List<User> list = userMapper.selectList(lambdaQueryWrapper);
 list.forEach(System.out::println);
}

4.4 基于LambdaUpdateWrapper组装条件

使用LambdaUpdateWrapper可以随意设置列的值,包括null值。

示例:组装修改条件

@Test
public void test04() {
 // 将年龄大于20并且用户名中包含有a或邮箱为null的用户信息修改
 /* 
 	UPDATE t_user SET age=?, email=? 
 	WHERE username LIKE ? AND age > ? OR email IS NULL)
 */
	LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
 updateWrapper
         .like(User::getName, "a")
         .gt(User::getAge, 20)
         .or().isNull(User::getEmail);
	updateWrapper
     	.set(User::getAge, 18)
     	.set(User::getEmail, null)

 int result = userMapper.update(user, queryWrapper);
 System.out.println("受影响的行数:" + result);
}

5. 核心注解使用

5.1 @TableName

(1) 注解使用
  • 描述:表名注解,标识实体类对应的表
  • 使用位置:实体类
  • 特殊的:若不加该注解,则使用实体类的名字作为表名(忽略大小写)。
@TableName("tb_user") // 对应数据库表名
@Data
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
}
(2) 全局前缀

在yaml配置文件中,通过 mybatis-plus.global-config.db-config.table-prefix 可以设置全局表名前缀。

这样实体类在不加 @TableName 注解时,可以默认以前缀+实体类名字作为表名。

mybatis-plus:
  global-config:
    db-config:
      table-prefix: tb_ # 表名前缀

5.2 @TableId

(1) 注解使用
  • 描述:主键注解,标识主键字段
  • 使用位置:实体类主键字段
  • 必须使用场景:
    1. 主键的列名和属性名不一致时
    2. 需要指定插入数据时如何生成主键(主键策略)
@TableName("tb_user")
@Data
public class User {
    @TableId(value="主键列名", type=主键策略)
    private Long id;
    private String name;
    private Integer age;
    private String email;
}
注解的属性 类型 必须指定 默认值 描述
value String "" 主键字段名
type Enum IdType.NONE 指定主键类型

type属性可选值:

type属性值 描述
AUTO 数据库 ID 自增(前提在MySQL数据库中配置主键自增)
ASSIGN_ID(默认) 分配 ID(主键类型为 Long(数据库中对应BIGINT) 或 String(数据库中对应VARCHAR(64))),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法)

全局配置修改主键策略:

在yaml配置文件中,通过 mybatis-plus.global-config.db-config.id-type 可以设置全局主键策略。

mybatis-plus:
  global-config:
    db-config:
      id-type: auto	# 配置MyBatis-Plus的主键策略
(2) 雪花算法

雪花算法(Snowflake Algorithm)是一种用于生成唯一ID的算法。它由Twitter公司提出,用于解决分布式系统中生成全局唯一ID的需求。雪花算法是一种简单但有效的生成唯一ID的算法,广泛应用于分布式系统中,如微服务架构、分布式数据库、分布式锁等场景,以满足全局唯一标识的需求。

在传统的自增ID生成方式中,使用单点数据库生成ID会成为系统的瓶颈,而雪花算法通过在分布式系统中生成唯一ID,避免了单点故障和性能瓶颈的问题。

雪花算法生成的ID是一个64位的整数,由以下几个部分组成:

  1. 时间戳:41位,精确到毫秒级,可以使用69年。
  2. 节点ID:10位,用于标识分布式系统中的不同节点。
  3. 序列号:12位,表示在同一毫秒内生成的不同ID的序号。

通过将以上三个部分组合在一起,雪花算法可以在分布式系统中生成全局唯一的ID,并保证ID的生成顺序性。

雪花算法的工作方式如下:

  1. 当前时间戳从某一固定的起始时间开始计算,可以用于计算ID的时间部分。
  2. 节点ID是分布式系统中每个节点的唯一标识,可以通过配置或自动分配的方式获得。
  3. 序列号用于记录在同一毫秒内生成的不同ID的序号,从0开始自增,最多支持4096个ID生成。

需要注意的是,雪花算法依赖于系统的时钟,需要确保系统时钟的准确性和单调性,否则可能会导致生成的ID不唯一或不符合预期的顺序。

要点总结:雪花算法生成的内容,需要使用 Long/BIGINT 或者 String/VARCHAR(64) 类型主键。

5.2 @TableField

  • 描述:字段注解,标识非主键字段
  • 必须使用场景:
    1. 属性名和列名不同时
    2. 属性不是数据库中的列时
@TableName("tb_user")
public class User {
    @TableId
    private Long id;
    @TableField("nickname")
    private String name;
    private Integer age;
    private String email;
    @TableField(exist=false)
    private int temp;
}
注解的属性 类型 必须指定 默认值 描述
value String "" 数据库字段名
exist boolean true 是否为数据库表字段

三、MyBatis-Plus高级扩展

1. 逻辑删除|@TableLogic

§1.1 表中添加逻辑删除字段

逻辑删除字段可以是一个布尔类型、整数类型或枚举类型。

# int 类型,1标识逻辑删除,0表示未逻辑删除
ALTER TABLE tb_user 
ADD deleted INT DEFAULT 0;

§1.2 指定逻辑删除字段的属性值

(§1) 单一指定

第一步:在逻辑删除的字段上添加 @TableLogic 注解。

  1. 插入:逻辑删除字段的值不受限制。
  2. 查找:自动添加条件,过滤掉标记为已删除的记录。
  3. 更新:防止更新已删除的记录。
  4. 删除:将删除操作转换为更新操作,标记记录为已删除。
@Data
public class User {

    private Integer id;
    private String name;
    private Integer age;
    private String email;
    
    @TableLogic	// 逻辑删除字段,默认逻辑删除值为1,未逻辑删除为0
    private Integer deleted;
}
(§2) 全局指定

第二步:在yaml配置文件中添加:

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted 	# 全局逻辑删除的实体类属性名
      logic-delete-value: 1 		# 逻辑已删除值(默认为1)
      logic-not-delete-value: 0 	# 逻辑未删除值(默认为0)

§1.3 使用测试

@Test
public void test(){
    // 逻辑删除
    // 实际执行的sql语句:UPDATE user SET deleted=1 WHERE id=? AND deleted=0
    userMapper.deleteById(5);
}

2. 乐观锁

2.1 乐观锁介绍

(1) 悲观锁和乐观锁

乐观锁和悲观锁是在并发编程中用于处理并发访问和资源竞争的两种不同的锁机制。

悲观锁:

悲观锁的基本思想是,在整个数据访问过程中,将共享资源锁定,以确保其他线程或进程不能同时访问和修改该资源。悲观锁的核心思想是"先保护,再修改"。在悲观锁的应用中,线程在访问共享资源之前会获取到锁,并在整个操作过程中保持锁的状态,阻塞其他线程的访问。只有当前线程完成操作后,才会释放锁,让其他线程继续操作资源。这种锁机制可以确保资源独占性和数据的一致性,但是在高并发环境下,悲观锁的效率相对较低。

乐观锁:

乐观锁的基本思想是,认为并发冲突的概率较低,因此不需要提前加锁,而是在数据更新阶段进行冲突检测和处理。乐观锁的核心思想是"先修改,后校验"。在乐观锁的应用中,线程在读取共享资源时不会加锁,而是记录特定的版本信息。当线程准备更新资源时,会先检查该资源的版本信息是否与之前读取的版本信息一致,如果一致则执行更新操作,否则说明有其他线程修改了该资源,需要进行相应的冲突处理。乐观锁通过避免加锁操作,提高了系统的并发性能和吞吐量,但是在并发冲突较为频繁的情况下,乐观锁会导致较多的冲突处理和重试操作。

(2) 具体技术和方案

乐观锁实现方案和技术:

  • 版本号/时间戳:为数据添加一个版本号或时间戳字段,每次更新数据时,比较当前版本号或时间戳与期望值是否一致,若一致则更新成功,否则表示数据已被修改,需要进行冲突处理。
  • CAS(Compare-and-Swap):使用原子操作比较当前值与旧值是否一致,若一致则进行更新操作,否则重新尝试。
  • 无锁数据结构:采用无锁数据结构,如无锁队列、无锁哈希表等,通过使用原子操作实现并发安全。

悲观锁实现方案和技术:

  • 锁机制:使用传统的锁机制,如互斥锁(Mutex Lock)或读写锁(Read-Write Lock)来保证对共享资源的独占访问。
  • 数据库锁:在数据库层面使用行级锁或表级锁来控制并发访问。
  • 信号量(Semaphore):使用信号量来限制对资源的并发访问。
(3) 版本号乐观锁实现流程
  1. 每条数据添加一个版本号字段version
  2. 取出记录时,获取当前 version
  3. 更新提交时,检查获取版本号是不是数据库当前最新版本号。
    • 如果是,则证明没有人修改数据,执行 set 数据更新,version = version + 1
    • 如果不是,则证明有人已经修改了,我们现在的其他记录就是失效数据,就更新失败。

2.2 使用MyBatis-Plus实现版本号乐观锁

(§1) 添加版本号更新插件

在配置类中,定义一个方法,在 MybatisPlusInterceptor 插件中添加乐观锁的版本号插件 OptimisticLockerInnerInterceptor

@MapperScan("com.example.mapper")
@SpringBootApplication	// 其中包含@Configuration配置类
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class,args);
    }
    
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // mybatis-plus的插件集合对象
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加乐观锁版本号插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}
(§2) 表中添加版本号字段
ALTER TABLE tb_user 
ADD VERSION INT DEFAULT 1;  # int 类型 乐观锁字段
(§3) 指定版本号字段的属性值

添加 @Version 注解后,MyBatis-Plus 在更新数据时会自动比较 version 字段,在更新成功后自动实现 version = version + 1

@Data
public class User {

    private Integer id;
    private String name;
    private Integer age;
    private String email;
    
    @Version	// 乐观锁版本号字段
    private Integer version;
}

3. 防止全表更新和删除

针对 updatedelete 语句,阻止恶意的全表更新和删除

具体实现:

在配置类中,定义一个方法,在 MybatisPlusInterceptor 插件中添加防止插件拦截器 BlockAttackInnerInterceptor

@MapperScan("com.example.mapper")
@SpringBootApplication	// 其中包含@Configuration配置类
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class,args);
    }
    
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // mybatis-plus的插件集合对象
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 防止全表更新和删除的拦截器插件
		interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return interceptor;
    }
}

四、MyBatisX

1. MyBatisX插件逆向工程

使用MyBatisX逆向工程插件,可以自动根据数据库生成对应的实体类和service、mapper。

具体实现见:#Part1 五、3. 3.3🆕MyBatisX逆向工程插件的使用

2. MyBatisX代码生成

MybatisX 支持 JPA 风格的代码提示,包括新增、查询、修改和删除操作的自动代码生成。

官方文档:https://baomidou.com/guides/mybatis-x/#jpa-风格提示

使用示例:具体详情见官网演示。

public interface UserMapper {
 // 输入以下JPA风格的代码,然后对其按 Alt+Enter,
 // 选择 [MybatisX]Generate Mybatis Sql,
 // 即可自动生成对应接口,以及在UserMapper.xml文件中生成对应的sql语句
 selectIdAndAgeByIdAndTitleOrderByCreateTimeDesc
}
posted @ 2024-09-23 23:35  雪与锄  阅读(75)  评论(0)    收藏  举报