1 JSP

1.1 [1]jsp

01.java代码(脚本Scriptlet)
    <%  局部变量、java语句%>
    <%! 全局变量、定义方法%>
    <%= 输出表达式 %>

02.指令
    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.Date"%>
    ①language:jsp页面使用的脚本语言
    ②import:导入类
    ③pageEncoding:jsp文件自身编码  jsp ->java
    ④contentType:浏览器解析jsp的编码

03.注释
    html注释  <!--  -->
    java注释  /*     */
    jsp注释   <%-- --%>

04.9大内置对象
    1.request:用户端请求,此请求会包含来自GET/POST请求的参数
    2.response:网页传回用户端的回应
    3.pageContext:网页的属性是在这里管理
    4.session:与请求有关的会话期
    5.application servlet:正在执行的内容
    6.out:用来传送回应的输出
    7.config:servlet的构架部件
    8.page:JSP网页本身
    9.exception:针对错误网页,未捕捉的例外

05.5个作用域
    page          【只在一个页面中保存属性,跳转页面无效】
    requet        【只在一次请求中保存属性,服务器跳转有效,浏览器跳转无效】
    session       【在一个会话范围中保存属性,无论何种跳转均有效,关闭浏览器后无效】
    application   【在整个服务器中保存,所有用户都可以使用】
    pageContext   【内置对象中最重要的一个对象,它代表着JSP页面编译后的内容(也就是JSP页面的运行环境)】

06.PageContext,ServletRequest,HttpSession,ServletContext;
    1.PageContext:作用范围是整个JSP页面,是四大作用域中最小的一个;生命周期是当对JSP的请求时开始,当响应结束时销毁
    2.ServletRequest域:作用范围是整个请求链(请求转发也存在);生命周期是在service方法调用前由服务器创建,传入service方法。整个请求结束,request生命结束
    3.HttpSession域:作用范围是一次会话。生命周期是在第一次调用request.getSession()方法时,服务器会检查是否已经有对应的session,如果没有就在内存中创建一个session并返回
    4.ServletContext域:作用范围是整个Web应用。当Web应用被加载进容器时创建代表整个web应用的ServletContext对象,当服务器关闭或Web应用被移除时,ServletContext对象跟着销毁
    ---------------------------------------------------------------------------------------------------------
    作用域从小到大为:PageContext(jsp页面),ServletRequest(一次请求),HttpSession(一次会话),ServletContext(整个web应用)

1.2 [1]jdbc

01.JDBC
    a.定义
        JDBC接口及相关类在java.sql包和javax.sql包里
        连接数据库,执行SQL查询,存储过程,并处理返回的结果
    b.JDBC三大功能
        1.与数据库建立连接
        2.发送SQL语句
        3.返回处理结果
    c.具体通过以下类/接口实现
        1.DriverManager:管理jdbc驱动
        2.Connection:连接,通过DriverManager产生Connection
        3.Statement:增删改查,通过Connection产生
          PreparedStatement:增删改查,通过Connection产生
          CallableStatement:调用数据库中的存储过程/存储函数,通过Connection产生
        4.Result:返回的结果集,上面的Statement等产生
    d.Connection产生操作数据库的对象
        Connection产生Statement对象:createStatement()
        Connection产生PreparedStatement对象:prepareStatement()
        Connection产生CallableStatement对象:prepareCall();
    e.Statement操作数据库
        增删改:executeUpdate()
        查询:executeQuery();
    f.PreparedStatement操作数据库
        public interface PreparedStatement extends Statement   继承statement
        增删改:executeUpdate()
        查询:executeQuery()
    g.ResultSet:保存结果集
        next():光标下移,判断是否有下一条数据
        previous():true/false
        getXxx(字段名|位置):获取具体的字段值,从1开始
    h.常见JDBC异常
        java.sql.SQLException:JDBC异常的基类
        java.sql.BatchUpdateException:当批处理操作执行失败的时候可能会抛出这个异常。这取决于具体的JDBC驱动的实现,它也可能直接抛出基类异常java.sql.SQLException中
        java.sql.SQLWarning:SQL操作出现的警告信息
        java.sql.DataTruncation:字段值由于某些非正常原因被截断了

02.Statement和PreparedStatement区别
    a.SQL语句处理
        Statement:适用于静态 SQL 语句,每次执行 SQL 时都需要重新解析
        PreparedStatement:适用于动态 SQL 语句,可以预编译 SQL 语句,支持参数化查询,提升性能并防止 SQL 注入
    b.性能
        Statement:每次执行 SQL 语句都要重新编译,性能较低
        PreparedStatement:预编译 SQL 语句,提升了执行效率,尤其在重复执行时
    c.安全性
        Statement:容易受到 SQL 注入攻击,因为 SQL 语句和参数直接拼接
        PreparedStatement:使用参数化查询,能有效防止 SQL 注入
    d.功能支持
        Statement:不支持批量处理
        PreparedStatement:支持批量处理,提高插入或更新的效率
    e.总结
        Statement:适用于简单静态 SQL,性能较低
        PreparedStatement:适用于动态 SQL,性能更高,安全性更好
        PreparedStatement继承于Statement,PreparedStatement实例包含已编译的SQL语句,所以其执行,速度要快于Statement对象

03.有哪些不同的结果集
    a.一共有三种ResultSet对象
        ResultSet.TYPE_FORWARD_ONLY:这是默认的类型,它的游标只能往下移
        ResultSet.TYPE_SCROLL_INSENSITIVE:游标可以上下移动,一旦它创建后,数据库里的数据再发生修改,对它来说是透明的
        ResultSet.TYPE_SCROLL_SENSITIVE:游标可以上下移动,如果生成后数据库还发生了修改操作,它是能够感知到的
    b.ResultSet中有两种并发类型
        ResultSet.CONCUR_READ_ONLY:ResultSet是只读的,这是默认类型
        ResultSet.CONCUR_UPDATABLE:我们可以使用的ResultSet的更新方法来更新里面的数据

04.DriverManager用来做什么
    JDBC的DriverManager是一个工厂类,我们通过它来创建数据库连接。当JDBC的Driver类被加载进来时
    它会自己注册到DriverManager类里面,然后我们会把数据库配置信息传成DriverManager.getConnection()方法
    DriverManager会使用注册到它里面的驱动来获取数据库连接,并返回给调用的程序

05.RowSet和ResultSet区别
    RowSet继承自ResultSet,因此它有ResultSet的全部功能,同时它自己添加了些额外的特性
    RowSet一个最大的好处是它可以是离线的,这样使得它更轻量级,同时便于在网络间进行传输

1.3 [1]servlet

01.Servlet生命周期
    实例化
    初始化
    处理请求
    销毁

02.生命周期方法
    与servlet的init方法相同,仅被调用一次
    它在每次请求时都被调用,与servlet的service()方法相同
    与Servlet的destroy()方法相同,它仅被调用一次

1.4 [1]jsp、servlet

01.Servlet
    一种服务器端的Java应用程序
    由 Web 容器加载和管理
    用于生成动态 Web 内容
    负责处理客户端请求

02.Jsp
    是 Servlet 的扩展,本质上还是 Servlet
    每个 Jsp 页面就是一个 Servlet 实例
    Jsp 页面会被 Web 容器编译成 Servlet,Servlet 再负责响应用户请求

03.区别
    Servlet 适合动态输出 Web 数据和业务逻辑处理,对于 html 页面内容的修改非常不方便;Jsp 是在 Html 代码中嵌入 Java 代码,适合页面的显示
    内置对象不同,获取内置对象的方式不同

1.5 [1]jpa、hibernate

01.定义
    JPA(Java Persistence API):是一种规范,定义了对象关系映射(ORM)的标准接口。JPA 本身不提供具体实现
    Hibernate:是 JPA 的一种具体实现,同时也是一个功能丰富的 ORM 框架,提供了超出 JPA 规范的额外功能

02.角色
    JPA:作为规范,提供了一组接口和注解,供开发者使用而不依赖于特定的实现
    Hibernate:作为实现,提供了 JPA 规范的功能,并扩展了许多高级特性,如缓存、批量处理等

03.使用
    JPA:可以与任何符合 JPA 规范的实现一起使用,如 Hibernate、EclipseLink 等
    Hibernate:可以独立使用,也可以作为 JPA 的实现来使用

04.功能
    JPA:提供基本的 ORM 功能,如实体映射、查询语言(JPQL)等
    Hibernate:除了支持 JPA 的功能外,还提供了更强大的特性,如自定义类型、拦截器、过滤器等

1.6 [2]mybatis、jdbc

01.MyBatis、JDBC
    a.总结
        JDBC 适合简单场景,但在复杂 SQL 处理和结果映射上较繁琐
        MyBatis 提供更高效的 SQL 管理、动态 SQL 处理和结果映射,且集成了连接池管理
    b.SQL 语句管理
        JDBC:SQL 语句通常直接写在代码中,导致代码混乱且难以维护
        MyBatis:SQL 语句配置在 Mapper XML 文件中,Java 代码和 SQL 语句分离,维护更简洁
    c.动态 SQL 处理
        JDBC:动态 SQL 拼接复杂,手动处理 SQL 条件和占位符,容易出错
        MyBatis:提供动态 SQL 标签(如 <where />、<if />)和 OGNL 表达式,简化动态 SQL 拼接
    d.结果集解析
        JDBC:需要手动解析 SQL 执行结果并将其映射为 Java 对象
        MyBatis:自动将 SQL 执行结果映射成 Java 对象,简化结果集处理
    e.数据库连接管理
        JDBC:需要手动管理数据库连接的创建和释放,频繁操作可能浪费资源
        MyBatis:支持配置数据库连接池,通过连接池管理数据库连接,优化资源使用。虽然 MyBatis 自带连接池,但一般推荐使用性能更好的开源连接池

02.Statement和PreparedStatement区别
    a.SQL语句处理
        Statement:适用于静态 SQL 语句,每次执行 SQL 时都需要重新解析
        PreparedStatement:适用于动态 SQL 语句,可以预编译 SQL 语句,支持参数化查询,提升性能并防止 SQL 注入
    b.性能
        Statement:每次执行 SQL 语句都要重新编译,性能较低
        PreparedStatement:预编译 SQL 语句,提升了执行效率,尤其在重复执行时
    c.安全性
        Statement:容易受到 SQL 注入攻击,因为 SQL 语句和参数直接拼接
        PreparedStatement:使用参数化查询,能有效防止 SQL 注入
    d.功能支持
        Statement:不支持批量处理
        PreparedStatement:支持批量处理,提高插入或更新的效率
    e.总结
        Statement:适用于简单静态 SQL,性能较低
        PreparedStatement:适用于动态 SQL,性能更高,安全性更好
        PreparedStatement继承于Statement,PreparedStatement实例包含已编译的SQL语句,所以其执行,速度要快于Statement对象

1.7 [2]mybatis、hibernate

01.基本概念
    MyBatis:基于 SQL 的 ORM 框架,使用 XML 或注解直接编写 SQL 语句,灵活性高
    Hibernate:全功能的 ORM 框架,使用 HQL 或 Criteria 查询,提供完整的对象-关系映射和自动生成 SQL

02.映射方式
    a.MyBatis
        手动编写 SQL 语句,使用 XML 文件或注解进行 SQL 映射
        优点:灵活、可控性高
        缺点:开发量大,需要手动维护 SQL
    b.Hibernate
        自动生成 SQL,使用注解或 XML 文件进行对象-关系映射
        优点:自动化程度高,减少手写 SQL
        缺点:学习曲线陡峭,复杂查询性能可能不如手写 SQL

03.缓存机制
    a.MyBatis
        一级缓存:SqlSession 级别缓存,默认开启
        二级缓存:Mapper 级别缓存,需要手动配置
    b.Hibernate
        一级缓存:Session 级别缓存,默认开启
        二级缓存:SessionFactory 级别缓存,支持多种缓存提供者(如 Ehcache)

04.查询方式
    a.MyBatis
        直接编写 SQL 语句,灵活使用原生 SQL
        优点:性能高,适合复杂查询
        缺点:开发和维护成本高
    b.Hibernate
        使用 HQL、Criteria API 或原生 SQL
        优点:简化查询开发,支持对象化操作
        缺点:对复杂查询的优化能力有限

05.事务管理
    a.MyBatis
        依赖于外部事务管理器(如 Spring),自身不提供事务管理
    b.Hibernate
        内置事务管理功能,支持声明式事务管理

06.学习曲线
    a.MyBatis
        较低,容易上手,适合需要直接控制 SQL 的应用
    b.Hibernate
        较高,功能全面,适合需要自动化 ORM 的复杂应用

1.8 [2]mybatis、mybatisplus

01.基本定位
    MyBatis:是一个半自动化的 ORM 框架,需要手动编写 SQL 语句
    MyBatis Plus:是在 MyBatis 基础上进行的增强和扩展,提供了 CRUD 的自动化功能,减少了重复代码

02.代码生成
    MyBatis:需要自己编写 Mapper 接口和对应的 SQL 映射文件
    MyBatis Plus:提供了代码生成器,可以自动生成 Mapper、Service、Controller 等代码

03.内置方法
    MyBatis:需要手动编写所有的 SQL 语句
    MyBatis Plus:提供了丰富的内置方法(如增删改查),减少了手动编写 SQL 的需求

04.插件支持
    MyBatis:支持自定义插件,但需要自己实现
    MyBatis Plus:内置了多种常用插件(如分页插件、性能分析插件),使用方便

05.分页处理
    MyBatis:需要手动编写分页 SQL 或使用第三方分页插件
    MyBatis Plus:内置分页插件,使用简单

06.更新策略
    MyBatis:更新需要自己编写复杂的 SQL 语句
    MyBatis Plus:提供了多种更新策略,可以通过注解和配置文件灵活设置

07.ActiveRecord 模式
    MyBatis:不支持 ActiveRecord 模式
    MyBatis Plus:支持 ActiveRecord 模式,实体类可以直接进行 CRUD 操作

1.9 [2]mybatis、sqlalchemy

01.MyBatis
    类型:Java 持久层框架
    工作原理:基于 XML 或注解的 SQL 映射,手动编写 SQL 语句,映射到 Java 对象
    优点:对 SQL 控制力强,适合复杂查询,灵活性高
    缺点:需要编写大量 SQL,代码冗长

02.SQLAlchemy
    类型:Python ORM 框架
    工作原理:提供 ORM(对象关系映射)和 SQL 表达式语言,自动生成 SQL 语句,映射到 Python 对象
    优点:自动化程度高,易于操作对象和关系,支持复杂查询和事务处理
    缺点:对 SQL 控制力较弱,性能开销较大

2 MyBatis

2.1 [1]组件:4个

00.四大核心组件
    SqlSessionFactory(SQL会话工厂):创建 SqlSession 实例的工厂
    SqlSession(SQL会话):执行SQL、获取映射器以及管理事务的接口
    Mapper接口(映射器):映射器接口,定义数据库操作方法
    Configuration配置文件(Mapper XML):映射配置文件包含了SQL语句与Mapper接口方法的映射规则

01.SqlSessionFactory(SQL会话工厂)
    a.定义
        SqlSessionFactory 是用于创建 SqlSession 实例的工厂。它是 MyBatis 的核心对象之一,负责管理 MyBatis 的配置和资源
    b.原理
        SqlSessionFactory 通过读取 MyBatis 的配置文件(如 mybatis-config.xml)来初始化配置和映射关系。它是线程安全的,通常在应用启动时创建一次,并在应用的整个生命周期中共享
    c.常用 API
        openSession():创建一个新的 SqlSession 实例
        openSession(boolean autoCommit):创建一个新的 SqlSession 实例,并指定是否自动提交
    d.使用步骤
        a.步骤1
            配置 MyBatis 配置文件(mybatis-config.xml)
        b.步骤2
            使用 SqlSessionFactoryBuilder 读取配置文件并构建 SqlSessionFactory
    e.代码示例
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

02.SqlSession(SQL会话)
    a.定义
        SqlSession 是 MyBatis 的核心接口,用于执行 SQL 语句、获取映射器以及管理事务
    b.原理
        SqlSession 通过底层的数据库连接执行 SQL 语句,并返回结果。它不是线程安全的,每个线程都应该有自己的 SqlSession 实例
    c.常用 API
        selectOne(String statement, Object parameter):执行查询并返回单个结果
        selectList(String statement, Object parameter):执行查询并返回结果列表
        insert(String statement, Object parameter):执行插入操作
        update(String statement, Object parameter):执行更新操作
        delete(String statement, Object parameter):执行删除操作
        commit():提交事务
        rollback():回滚事务
        close():关闭 SqlSession
    d.使用步骤
        a.步骤1
            从 SqlSessionFactory 获取 SqlSession 实例
        b.步骤2
            使用 SqlSession 执行数据库操作
        c.步骤3
            提交或回滚事务
        d.步骤4
            关闭 SqlSession
    e.代码示例
        try (SqlSession session = sqlSessionFactory.openSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            User user = mapper.selectUser(1);
            session.commit();
        }

03.Mapper接口(映射器)
    a.定义
        Mapper接口 是 MyBatis 提供的用于定义数据库操作方法的接口。每个接口方法对应一个 SQL 语句
    b.原理
        MyBatis 通过动态代理实现 Mapper 接口的实例化,并将接口方法与 SQL 语句进行映射
    c.常用 API
        Mapper 接口本身没有 API,通常定义在接口中的方法会被 MyBatis 自动实现
    d.使用步骤
        a.步骤1
            定义 Mapper 接口
        b.步骤2
            在 MyBatis 配置文件中配置接口与 SQL 映射
        c.步骤3
            使用 SqlSession 获取 Mapper 接口的实例
    e.代码示例
        public interface UserMapper {
            User selectUser(int id);
        }

04.Configuration配置文件(Mapper XML)
    a.定义
        Mapper XML 是 MyBatis 的映射配置文件,包含了 SQL 语句与 Mapper 接口方法的映射规则
    b.原理
        Mapper XML 文件定义了 SQL 语句和接口方法的映射关系,MyBatis 在运行时根据这些配置执行相应的 SQL
    c.常用 API
        Mapper XML 文件本身没有 API,通常通过 XML 标签定义 SQL 语句
    d.使用步骤
        a.步骤1
            创建 Mapper XML 文件
        b.步骤2
            在 XML 文件中定义 SQL 语句
        c.步骤3
            在 MyBatis 配置文件中注册 Mapper XML
    e.代码示例
        <mapper namespace="com.example.UserMapper">
            <select id="selectUser" parameterType="int" resultType="User">
                SELECT * FROM users WHERE id = #{id}
            </select>
        </mapper>

2.2 [1]执行器:4个

00.四种执行器
    SimpleExecutor :每执行一次 update 或 select,就开启一个 Statement 对象,用完立刻关闭
    ReuseExecutor :执行 SQL 时会复用 Statement 对象,使用完后不关闭,供下一次使用
    BatchExecutor :执行器用于批处理,缓存多个 Statement 对象,然后批量执行
    CachingExecutor :在上述的三个执行器之上,增加二级缓存的功能

01.SimpleExecutor
    a.定义
        SimpleExecutor是MyBatis中最简单的执行器,每次执行SQL操作时都会创建一个新的Statement对象,并在操作完成后立即关闭
    b.原理
        每次执行SQL操作时,SimpleExecutor都会创建一个新的数据库连接和Statement对象,执行完SQL后立即关闭这些资源
    c.常用API
        a.doUpdate()
            执行更新操作
        b.doQuery()
            执行查询操作
    d.使用步骤
        创建SqlSession
        使用SqlSession执行SQL操作
        操作完成后关闭SqlSession
    e.代码示例
        try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE)) {
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            User user = mapper.selectUserById(1);
            System.out.println(user);
        }

02.ReuseExecutor
    a.定义
        ReuseExecutor是MyBatis中的执行器,能够复用Statement对象,避免频繁创建和关闭Statement
    b.原理
        ReuseExecutor会缓存Statement对象,在执行SQL操作时,如果缓存中存在可用的Statement对象,则直接复用,否则创建新的Statement
    c.常用API
        a.doUpdate()
            执行更新操作
        b.doQuery()
            执行查询操作
    d.使用步骤
        创建SqlSession
        使用SqlSession执行SQL操作
        操作完成后关闭SqlSession
    e.代码示例
        try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)) {
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            User user1 = mapper.selectUserById(1);
            User user2 = mapper.selectUserById(2);
            System.out.println(user1);
            System.out.println(user2);
        }

03.BatchExecutor
    a.定义
        BatchExecutor是MyBatis中用于批处理的执行器,能够缓存多个Statement对象并批量执行
    b.原理
        BatchExecutor会将多个SQL操作缓存起来,等到执行时一次性批量提交,减少数据库交互次数,提高性能
    c.常用API
        a.doUpdate()
            执行更新操作
        b.doQuery()
            执行查询操作
        c.flushStatements()
            批量执行缓存的SQL操作
    d.使用步骤
        创建SqlSession
        使用SqlSession执行多个SQL操作
        调用flushStatements()提交批处理
        操作完成后关闭SqlSession
    e.代码示例
        try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            mapper.updateUser(user1);
            mapper.updateUser(user2);
            sqlSession.flushStatements();
        }

04.CachingExecutor
    a.定义
        CachingExecutor是MyBatis中的执行器,增加了二级缓存功能,能够缓存查询结果以提高性能
    b.原理
        CachingExecutor在其他执行器之上增加了缓存层,查询时会先检查缓存中是否有结果,如果有则直接返回,否则执行查询并将结果存入缓存
    c.常用API
        a.doQuery()
            执行查询操作并缓存结果
        b.clearCache()
            清除缓存
    d.使用步骤
        配置MyBatis二级缓存
        创建SqlSession
        使用SqlSession执行查询操作
        操作完成后关闭SqlSession
    e.代码示例
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            User user = mapper.selectUserById(1);
            System.out.println(user);
            sqlSession.clearCache(); // 清除缓存
        }

2.3 [1]处理器:4个

00.四个处理器
    Executor(执行器):Executor负责管理和执行SQL语句
    StatementHandler(语句处理器):负责在 Java 类型与 JDBC 类型之间进行转换
    ParameterHandler(参数处理器):负责将参数转换为 JDBC Statement 所需的参数
    ResultSetHandler(结果集处理器):负责将 JDBC ResultSet 结果集转换为 List 集合或单个对象

01.Executor(执行器)
    a.定义
        Executor 是 MyBatis 中负责管理和执行 SQL 语句的组件。它负责与数据库交互,执行查询、更新等操作,并返回结果
    b.原理
        Executor 通过调用 JDBC API 执行 SQL 语句,并处理事务、缓存等功能。它是 MyBatis 的核心组件之一,支持简单执行、批处理执行和可重用执行等多种执行策略
    c.常用API
        update(): 执行更新操作
        query(): 执行查询操作
        commit(): 提交事务
        rollback(): 回滚事务
    d.使用步骤
        1.创建 SqlSession
        2.获取 Executor 实例
        3.执行 SQL 语句
        4.处理结果
        5.提交或回滚事务
    e.代码示例
        SqlSession sqlSession = sqlSessionFactory.openSession();
        Executor executor = sqlSession.getExecutor();
        try {
            List<Object> results = executor.query(mappedStatement, parameter, RowBounds.DEFAULT, resultHandler);
            sqlSession.commit();
        } catch (Exception e) {
            sqlSession.rollback();
        } finally {
            sqlSession.close();
        }

02.StatementHandler(语句处理器)
    a.定义
        StatementHandler 负责在 Java 类型与 JDBC 类型之间进行转换,并准备 SQL 语句
    b.原理
        StatementHandler 通过解析 SQL 语句,设置参数,并调用 JDBC API 执行 SQL。它是 MyBatis 中用于处理 SQL 语句的核心组件
    c.常用API
        prepare(): 准备 SQL 语句
        parameterize(): 设置 SQL 参数
        batch(): 执行批处理操作
        update(): 执行更新操作
        query(): 执行查询操作
    d.使用步骤
        1.创建 StatementHandler 实例
        2.准备 SQL 语句
        3.设置参数
        4.执行 SQL 语句
        5.处理结果
    e.代码示例
        StatementHandler handler = configuration.newStatementHandler(executor, mappedStatement, parameter, RowBounds.DEFAULT, resultHandler, boundSql);
        PreparedStatement stmt = handler.prepare(connection, transactionTimeout);
        handler.parameterize(stmt);
        ResultSet rs = handler.query(stmt, resultHandler);

03.ParameterHandler(参数处理器)
    a.定义
        ParameterHandler 负责将 Java 参数转换为 JDBC Statement 所需的参数
    b.原理
        ParameterHandler 通过解析传入的参数对象,将其转换为 SQL 语句中需要的格式,并设置到 PreparedStatement 中
    c.常用API
        setParameters(): 设置 SQL 语句的参数
    d.使用步骤
        1.创建 ParameterHandler 实例
        2.设置参数到 PreparedStatement
    e.代码示例
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
        parameterHandler.setParameters(preparedStatement);

04.ResultSetHandler(结果集处理器)
    a.定义
        ResultSetHandler 负责将 JDBC ResultSet 结果集转换为 Java 对象或集合
    b.原理
        ResultSetHandler 通过解析 ResultSet,将其转换为 Java 对象或集合,通常使用反射机制将结果映射到对象的属性中
    c.常用API
        handleResultSets(): 处理结果集并返回 Java 对象
    d.使用步骤
        1.创建 ResultSetHandler 实例
        2.处理 ResultSet
        3.返回 Java 对象或集合
    e.代码示例
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
        List<Object> results = resultSetHandler.handleResultSets(preparedStatement);

2.4 [1]转换器:6类

01.基本数据类型处理器
    BooleanTypeHandler           Boolean,boolean      任何兼容的布尔值
    ByteTypeHandler              Byte,byte            任何兼容的数字或字节类型
    ShortTypeHandler             Short,short          任何兼容的数字或短整型
    IntegerTypeHandler           Integer,int          任何兼容的数字和整型
    LongTypeHandler              Long,long            任何兼容的数字或长整型
    FloatTypeHandler             Float,float          任何兼容的数字或单精度浮点型
    DoubleTypeHandler            Double,double        任何兼容的数字或双精度浮点型

02.数字类型处理器
    BigDecimalTypeHandler        BigDecimal            任何兼容的数字或十进制小数类型

03.字符串类型处理器
    StringTypeHandler            String                CHAR和VARCHAR类型
    ClobTypeHandler              String                CLOB和LONGVARCHAR类型
    NStringTypeHandler           String                NVARCHAR和NCHAR类型
    NClobTypeHandler             String                NCLOB类型

04.字节数组类型处理器
    ByteArrayTypeHandler         byte[]                任何兼容的字节流类型
    BlobTypeHandler              byte[]                BLOB和LONGVARBINARY类型

05.日期和时间类型处理器
    DateTypeHandler              Date(java.util)     TIMESTAMP类型
    DateOnlyTypeHandler          Date(java.util)     DATE类型
    TimeOnlyTypeHandler          Date(java.util)     TIME类型
    SqlTimestampTypeHandler      Timestamp(java.sql) TIMESTAMP类型
    SqlDateTypeHandler           Date(java.sql)      DATE类型
    SqlTimeTypeHandler           Time(java.sql)      TIME类型

06.其他类型处理器
    ObjectTypeHandler            任意                  其他或未指定类型
    EnumTypeHandler              Enumeration类型       VARCHAR。任何兼容的字符串类型,作为代码存储(而不是索引)

2.5 [1]组件、执行器、处理器

01.组件
    SqlSessionFactory创建SqlSession,SqlSession执行SQL,Mapper定义数据库操作方法

02.执行器
    SimpleExecutor、ReuseExecutor、BatchExecutor,用于执行SQL语句

03.处理器
    ParameterHandler、ResultSetHandler、TypeHandler,用于参数处理和结果集转换

2.6 [2]工作原理:7步

01.工作原理
    1.创建SqlSessionFactory对象
    2.通过SqlSessionFactory获取SqlSession对象
    3.通过SqlSession获得Mapper代理对象
    4.通过Mapper代理对象,执行数据库操作
    5.执行成功,则使用SqlSession提交事务
    6.执行失败,则使用SqlSession回滚事务
    7.关闭会话

2.7 [2]使用步骤:4步

01.使用步骤
    1.获取SqlSessionFactory对象
    2.获取SqlSession对象
    3.获取XxxMapper对象(代理接口中的方法、mapper..xml中的<select:>等标签)
    4.执行<select>等标签中定义的SQL语句

2.8 [2]Mapper与XML对应关系:4种

01.XML与Mapper对应关系
    1.Mapper接口方法名和mapper.xml中定义的每个sql的id相同
    2.Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType的类型相同
    3.Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同
    4.Mapper.xml文件中的namespace即是mapper接口的类路径

2.9 [2]Mapper与接口绑定方式:3种

01.Mapper与接口绑定方式
    1.通过XML、Mapper里面写SQL来绑定。在这种情况下,要指定XML映射文件里面的"namespace"必须为接口的全路径名
    2.通过注解绑定,就是在接口的方法上面加上 @Select、@Update、@Insert、@Delete 注解,里面包含SQL语句来绑定
    3.是第二种的特例,也是通过注解绑定,在接口的方法上面加上 @SelectProvider、@UpdateProvider、@InsertProvider、@DeleteProvider 注解,通过Java代码,生成对应的动态SQL

2.10 [2]Mybatis允许映射Enum:自定义TypeHandler

01.说明
    在 MyBatis 中,枚举类的映射是通过自定义 TypeHandler 来实现的
    TypeHandler 是 MyBatis 提供的一个接口,用于处理 Java 类型和 JDBC 类型之间的转换
    通过实现 TypeHandler,你可以自定义如何将枚举类型映射到数据库字段,以及如何从数据库字段映射回枚举类型

02.自定义 TypeHandler 的步骤
    a.实现 TypeHandler 接口
        你需要实现 TypeHandler 接口的四个方法:setParameter、getResult(有三个重载版本)
        setParameter 方法用于将 Java 枚举类型转换为 JDBC 类型,并设置到 SQL 语句的参数中
        getResult 方法用于从结果集中获取数据,并将其转换为 Java 枚举类型
    b.示例代码
        import org.apache.ibatis.type.BaseTypeHandler;
        import org.apache.ibatis.type.JdbcType;
        import java.sql.CallableStatement;
        import java.sql.PreparedStatement;
        import java.sql.ResultSet;
        import java.sql.SQLException;

        public class MyEnumTypeHandler extends BaseTypeHandler<MyEnum> {

            @Override
            public void setParameter(PreparedStatement ps, int i, MyEnum parameter, JdbcType jdbcType) throws SQLException {
                if (parameter == null) {
                    ps.setNull(i, jdbcType.TYPE_CODE);
                } else {
                    ps.setString(i, parameter.name()); // 或者根据需要使用 parameter.ordinal()
                }
            }

            @Override
            public MyEnum getResult(ResultSet rs, String columnName) throws SQLException {
                String value = rs.getString(columnName);
                return value == null ? null : MyEnum.valueOf(value);
            }

            @Override
            public MyEnum getResult(ResultSet rs, int columnIndex) throws SQLException {
                String value = rs.getString(columnIndex);
                return value == null ? null : MyEnum.valueOf(value);
            }

            @Override
            public MyEnum getResult(CallableStatement cs, int columnIndex) throws SQLException {
                String value = cs.getString(columnIndex);
                return value == null ? null : MyEnum.valueOf(value);
            }
        }
    c.注册TypeHandler
        Configuration configuration = new Configuration();
        configuration.getTypeHandlerRegistry().register(MyEnum.class, MyEnumTypeHandler.class);

2.11 [3]传递参数:4种

01.方法1:顺序传参法
    a.代码
        public User selectUser(String name, int deptId);

        <select id="selectUser" resultMap="UserResultMap">
            select * from user
            where user_name = #{0} and dept_id = #{1}
        </select>
    b.说明
        #{}里面的数字代表传入参数的顺序
        这种方法不建议使用,sql层表达不直观,且一旦顺序调整容易出错

02.方法2:@Param注解传参法
    a.代码
        public User selectUser(@Param("userName") String name, int @Param("deptId") deptId);

        <select id="selectUser" resultMap="UserResultMap">
            select * from user
            where user_name = #{userName} and dept_id = #{deptId}
        </select>
    b.说明
        #{}里面的名称对应的是注解@Param括号里面修饰的名称
        这种方法在参数不多的情况还是比较直观的,推荐使用

03.方法3:Map传参法
    a.代码
        public User selectUser(Map<String, Object> params);

        <select id="selectUser" parameterType="java.util.Map" resultMap="UserResultMap">
            select * from user
            where user_name = #{userName} and dept_id = #{deptId}
        </select>
    b.说明
        #{}里面的名称对应的是Map里面的key名称
        这种方法适合传递多个参数,且参数易变能灵活传递的情况

04.方法4:Java Bean传参法
    a.代码1
        public User selectUser(User user);

        <select id="selectUser" parameterType="com.jourwon.pojo.User" resultMap="UserResultMap">
            select * from user
            where user_name = #{userName} and dept_id = #{deptId}
        </select>
    b.说明1
        #{}里面的名称对应的是User类里面的成员属性
        这种方法直观,需要建一个实体类,扩展不容易,需要加属性,但代码可读性强,业务逻辑处理方便,推荐使用
    c.代码2
        public User selectUser(User user);

        <select id="selectUser" resultMap="UserResultMap">
            select * from user
            where user_name = #{userName} and dept_id = #{deptId}
        </select>
    d.说明2
        parameterType配置参数:【在一个参数时可以忽略(可写可不写)】

2.12 [3]#{}与${}的不同点

00.汇总
    a.#{}
        #{}可以防止SQL注入,自动添加了''引号
        相当于“定义好SQL,去填写”
        相当于PreparedStatement,先进行预编译,然后再将参数中的内容替换进来
        #{}传入参数是以字符串传入,会将SQL中的#{}替换为?号,调用PreparedStatement的set方法去赋值
    b.${}
        ${}不可以防止SQL注入,就是字符串拼接,即${}原样输出
        相当于“自己写规则定义sql”
        相当于Statement
        ${}传入参数是原值传入,就是把{}替换成变量的值
    c.重要提示
        永远优先使用#{},除非你明确知道${}是安全的并且是必要的
        在使用${}时,务必对变量值进行严格的校验和过滤,防止SQL注入。 可以使用白名单机制,只允许特定的值通过
        避免将用户输入直接传递给${},这几乎肯定会导致SQL注入

01.#{}的使用场景
    a.传递参数值:根据用户ID查询用户信息
        // 假设 UserMapper 继承了 BaseMapper<User>
        @Mapper
        public interface UserMapper extends BaseMapper<User> {
            @Select("SELECT * FROM users WHERE id = #{id}")
            User getUserById(@Param("id") int id);
        }

        // 使用
        @Autowired
        private UserMapper userMapper;

        public void testGetUserById() {
            int userId = 123;
            User user = userMapper.getUserById(userId);
            System.out.println(user);
        }
        -----------------------------------------------------------------------------------------------------
        @Select 注解直接定义了 SQL 语句
        #{id} 仍然是参数占位符,MyBatis-Plus 会自动处理
        @Param("id") 指定了参数名称,与 #{id} 对应
    b.插入数据
        // UserMapper 接口 (继承 BaseMapper<User>)
        @Mapper
        public interface UserMapper extends BaseMapper<User> {
            // 不需要额外注解,BaseMapper 提供了 insert 方法
        }

        // 使用
        @Autowired
        private UserMapper userMapper;

        public void testInsertUser() {
            User newUser = new User();
            newUser.setUsername("testuser");
            newUser.setEmail("[email protected]");
            userMapper.insert(newUser); // MyBatis-Plus 提供的 insert 方法
        }
        -----------------------------------------------------------------------------------------------------
        MyBatis-Plus 的 BaseMapper 提供了通用的 insert 方法,无需手动编写 SQL
        #{} 在 User 对象的属性中自动匹配,将 username 和 email 的值插入到 SQL 语句中
    c.更新数据
        // UserMapper 接口 (继承 BaseMapper<User>)
        @Mapper
        public interface UserMapper extends BaseMapper<User> {
            // 不需要额外注解,BaseMapper 提供了 updateById 方法
        }

        // 使用
        @Autowired
        private UserMapper userMapper;

        public void testUpdateUser() {
            User updatedUser = new User();
            updatedUser.setId(123); // 必须设置 ID,否则无法更新
            updatedUser.setUsername("newusername");
            updatedUser.setEmail("[email protected]");
            userMapper.updateById(updatedUser); // MyBatis-Plus 提供的 updateById 方法
        }
        -----------------------------------------------------------------------------------------------------
        BaseMapper 提供了 updateById 方法,根据 ID 更新数据
        #{} 在 User 对象的属性中自动匹配,将 username 和 email 的值更新到 SQL 语句中

02.${}的使用场景
    a.动态表名:根据不同的条件查询不同的表
        // UserMapper 接口
        @Mapper
        public interface UserMapper extends BaseMapper<User> {
            @Select("SELECT * FROM ${tableName} WHERE id = #{id}")
            User getDataFromTable(@Param("tableName") String tableName, @Param("id") int id);
        }

        // 使用
        @Autowired
        private UserMapper userMapper;

        public void testGetDataFromTable() {
            String tableName = "users"; // 确保 tableName 是安全的
            int id = 123;
            User user = userMapper.getDataFromTable(tableName, id);
            System.out.println(user);
        }
        -----------------------------------------------------------------------------------------------------
        ${tableName} 直接将 tableName 的值拼接到 SQL 语句中
        务必确保 tableName 是安全的,防止 SQL 注入
    b.动态排序字段:根据不同的字段进行排序
        // UserMapper 接口
        @Mapper
        public interface UserMapper extends BaseMapper<User> {
            @Select("SELECT * FROM users ORDER BY ${sortField}")
            List<User> getAllUsers(@Param("sortField") String sortField);
        }

        // 使用
        @Autowired
        private UserMapper userMapper;

        public void testGetAllUsers() {
            String sortField = "username"; // 确保 sortField 是安全的
            List<User> users = userMapper.getAllUsers(sortField);
            System.out.println(users);
        }
        -----------------------------------------------------------------------------------------------------
        ${sortField} 直接将 sortField 的值拼接到 SQL 语句中
        务必确保 sortField 是安全的,防止 SQL 注入

2.13 [3]模糊查询like语句

00.汇总
    a.2024%
        匹配以“2024”开头的字符串
        2024-01-01
        2024年
        2024abc
        indicator_date LIKE CONCAT(#{year, jdbcType=VARCHAR}, '%')
    b.%2024%
        匹配包含“2024”的字符串
        abc2024def
        2024年
        2024
        indicator_date LIKE CONCAT('%', #{year, jdbcType=VARCHAR}, '%')
    c.%2024
        匹配以“2024”结尾的字符串
        abc2024
        2024
        indicator_date LIKE CONCAT('%', #{year, jdbcType=VARCHAR})
    d.连接任意
        AND indicator_date = CONCAT(SUBSTR(#{month, jdbcType=VARCHAR}, 1, 4), SUBSTR(#{month, jdbcType=VARCHAR}, 6, 7))

00.汇总
    a.'%${question}%'
        可能引起SQL注入,不推荐
    b."%"#{question}"%"
        因为#{…}解析成sql语句时候,会在变量外侧自动加单引号’ ',
        所以这里 % 需要使用双引号" ",不能使用单引号 ’ ',不然会查不到任何结果
    c.CONCAT('%',#{question},'%')
        使用CONCAT()函数,推荐
    d.使用bind标签
        <select id="listUserLikeUsername" resultType="com.jourwon.pojo.User">
          <bind name="pattern" value="'%' + username + '%'" />
          select id,sex,age,username,password from person where username LIKE #{pattern}
        </select>

01.模糊查询的四种方式
    a.说明
        安全性:使用 CONCAT() 或 <bind> 标签可以有效避免 SQL 注入问题
        可读性:CONCAT() 函数和 <bind> 标签都提供了清晰的语法结构,便于维护和理解
    b.使用 %${question}%
        a.描述
            这种方式直接将变量插入到 SQL 中,可能会引起 SQL 注入,因此不推荐使用
        b.示例
            <select id="listUserLikeUsername" resultType="com.jourwon.pojo.User">
                SELECT id, sex, age, username, password
                FROM person
                WHERE username LIKE '%${username}%'
            </select>
    c.使用 "%#{question}%"
        a.描述
            由于 #{} 会自动加上单引号,因此 % 需要使用双引号
        b.示例
            <select id="listUserLikeUsername" resultType="com.jourwon.pojo.User">
                SELECT id, sex, age, username, password
                FROM person
                WHERE username LIKE "%"#{username}"%"
            </select>
    d.使用 CONCAT('%', #{question}, '%')
        a.描述
            推荐使用 CONCAT() 函数来构建模糊查询,以避免 SQL 注入
        b.示例
            <select id="listUserLikeUsername" resultType="com.jourwon.pojo.User">
                SELECT id, sex, age, username, password
                FROM person
                WHERE username LIKE CONCAT('%', #{username}, '%')
            </select>
    e.使用 <bind> 标签
        a.描述
            使用 <bind> 标签可以提前处理变量,避免 SQL 注入
        b.示例
            <select id="listUserLikeUsername" resultType="com.jourwon.pojo.User">
                <bind name="pattern" value="'%' + username + '%'" />
                SELECT id, sex, age, username, password
                FROM person
                WHERE username LIKE #{pattern}
            </select>

02.MySQL中like的模糊查询如何优化
    a.索引
        a.无法使用索引
            1.当like值前后都有匹配符时%abc%,无法使用索引
            2.当like值前有匹配符时%abc,无法使用索引
        b.可以使用索引
            1.当like值后有匹配符时'abc%',可以使用索引
    b.优化方案
        a.反转列
            假设表中的name可能包含以abc结尾的字符串,可以将这一列反转过来插入到一个冗余列v_name中,并为这一列建立索引
            在查询的时候,可以使用v_name列进行模糊查询
        b.虚拟列功能(MySQL5.7.6之后)
            1.为一个列建立一个虚拟列,并为虚拟列建立索引,在查询时where中like条件改为虚拟列,就可以使用索引
            2.使用union查询like 'abc%'和like '%abc'
            3.虚拟列可以指定为VIRTUAL或STORED,VIRTUAL不会将虚拟列存储到磁盘中,STORED会存储到磁盘中
    c.虚拟列测试示例
        a.建表
            CREATE TABLE test (
              id INT AUTO_INCREMENT PRIMARY KEY,
              name VARCHAR(50),
              INDEX idx_name (name)
            ) CHARACTER SET utf8;
        b.创建存储过程,写入数据
            DELIMITER //

            CREATE PROCEDURE InsertTestData()
            BEGIN
              DECLARE i INT DEFAULT 1;

              WHILE i <= 2000000 DO
                IF i <= 200 THEN
                  SET @randomPrefix1 = CONCAT(CHAR(FLOOR(RAND() * 26) + 65), CHAR(FLOOR(RAND() * 26) + 97), CHAR(FLOOR(RAND() * 26) + 48));
                  SET @randomString1 = CONCAT(CHAR(FLOOR(RAND() * 26) + 65), CHAR(FLOOR(RAND() * 26) + 97), CHAR(FLOOR(RAND() * 26) + 48));
                  SET @randomName1 = CONCAT(@randomPrefix1, @randomString1, 'abc');
                  INSERT INTO test (name) VALUES (@randomName1);
                ELSEIF i <= 400 THEN
                  SET @randomString2 = CONCAT(CHAR(FLOOR(RAND() * 26) + 65), CHAR(FLOOR(RAND() * 26) + 97), CHAR(FLOOR(RAND() * 26) + 48));
                  SET @randomName2 = CONCAT('abc', @randomString2);
                  INSERT INTO test (name) VALUES (@randomName2);
                ELSE
                  SET @randomName3 = CONCAT(CHAR(FLOOR(RAND() * 26) + 65), CHAR(FLOOR(RAND() * 26) + 97), CHAR(FLOOR(RAND() * 26) + 48));
                  INSERT INTO test (name) VALUES (@randomName3);
                END IF;

                SET i = i + 1;
              END WHILE;
            END //

            DELIMITER ;
        c.调用存储过程
            call InsertTestData();
        d.建立虚拟列
            alter table test add column `v_name` varchar(50) generated always as (reverse(name));
        e.为虚拟列创建索引
            alter table test add index `idx_name_virt`(v_name);
        f.使用虚拟列模糊查询
            select * from test where v_name like 'cba%'
            union
            select * from test where name like 'abc%'
        g.不使用虚拟列模糊查询
            select * from test where name like 'abc%'
            union
            select * from test where name like '%abc'
    d.MySQL5.7.6虚拟列功能
        a.好处
            1.简化查询:虚拟列可以简化复杂的查询
            2.数据一致性:虚拟列的值自动计算,确保数据一致性
            3.减少冗余:避免存储冗余数据
            4.索引支持:虚拟列可以被索引,提高查询性能
            5.灵活性:虚拟列可以定义复杂的表达式
        b.坏处
            1.性能开销:VIRTUAL 虚拟列的值动态计算,增加查询开销
            2.存储空间:STORED 虚拟列增加表的存储空间
            3.复杂性增加:虚拟列定义增加表结构复杂性
            4.兼容性问题:虚拟列在较旧的 MySQL 版本中无法使用
            5.索引限制:某些复杂的表达式可能无法创建索引

2.14 [3]属性名与字段名不一致

01.使用AS别名
    <select id="selectOrder" parameterType="Integer" resultType="Order">
        SELECT order_id AS id, order_no AS orderno, order_price AS price
        FROM orders
        WHERE order_id = #{id}
    </select>

02.大多数场景下,数据库字段名和实体类中的属性名差,主要是前者为下划线风格,后者为驼峰风格
    <setting name="logImpl" value="LOG4J"/>
        <setting name="mapUnderscoreToCamelCase" value="true" />
    </settings>

03.通过<resultMap>来映射字段名和实体类属性名的一一对应的关系
    <resultMap type="me.gacl.domain.Order" id=”OrderResultMap”>
        <!–- 用 id 属性来映射主键字段 -–>
        <id property="id" column="order_id">
        <!–- 用 result 属性来映射非主键字段,property 为实体类属性名,column 为数据表中的属性 -–>
        <result property="orderNo" column ="order_no" />
        <result property="price" column="order_price" />
    </resultMap>
    <select id="getOrder" parameterType="Integer" resultMap="OrderResultMap">
        SELECT *
        FROM orders
        WHERE order_id = #{id}
    </select>

2.15 [4]动态SQL

00.汇总
    a.7类:9个
        if:根据条件判断
        choose、when、otherwise:组合使用,选择多个条件中的一个
        trim:定制类似where标签的功能
        where:where元素只会在子元素返回任何内容的情况下才插入“WHERE”子句,若子句的开头为“AND” 或 “OR”,where元素也会将它们去除
        set:用于动态包含需要更新的列,忽略其它不更新的列
        foreach:对集合进行遍历
        bind:允许在OGNL表达式以外创建一个变量,并将其绑定到当前的上下文
    b.映射
        一对一:association
        一对多:collection

01.if标签:在查询、删除、更新的时候很可能会使用到,必须结合test属性联合使用
    a.方式1
        在WHERE条件中使用if标签
    b.方式2
        在UPDATE更新列中使用if标签
    c.方式3
        在INSERT动态插入中使用if标签

02.choose标签
    a.choose when otherwise
        标签可以帮我们实现if else的逻辑
        一个choose标签至少有一个when,最多一个otherwise

03.trim、where、set
    a.trim来表示where
        <trim prefix="where" prefixOverrides="AND |OR"></trim>
        当 trim 中含有内容时,添加 where,且第一个为and或or时,会将其去掉。而如果没有内容,则不添加where
    b.trim来表示set
        <trim prefix="SET" suffixOverrides=","></trim>
        表示当trim中含有内容时,添加set,且最后的内容为,时,会将其去掉,而没有内容,不添加set
    c.trim的几个属性
        prefix:在trim标签中的内容的 前面添加某些内容
        prefixOverrides:在trim标签中的内容的 前面去掉某些内容
        suffix:在trim标签中的内容的 后面添加某些内容
        suffixOverrides:在trim标签中的内容的 后面去掉某些内容

04.foreach
    a.foreach迭代类型
        1.对象
        2.对象数组
        3.数组
        4.集合
    b.foreach属性
        collection: 表示对哪一个集合或数组做迭代;如果参数是数组类型,此时Map的key为array;如果参数是List类型,此时Map的key为list
        item: 变量名。即从迭代的对象中取出的每一个值
        index: 索引的属性名。当迭代的对象为Map时,该值为Map中的Key
        open: 循环开头的字符串
        close: 循环结束的字符串
        separator: 每次循环的分隔符
    c.collection中的值应该怎么设定呢?跟接口方法中的参数相关
        a.只有一个数组参数或集合参数
            默认情况:集合collection=list,数组是collection=array
            推荐:使用@Param来指定参数的名称,如我们在参数前@Param("ids"),则就填写 collection=ids
        b.多参数
            多参数请使用@Param来指定, 否则SQL中会很不方便
        c.参数是Map
            指定为 Map中的对应的Key即可,其实上面的@Param最后也是转化为Map的
        d.参数是对象
            使用属性. 属性即可
    d.在where中使用foreach
    e.foreach实现批量插入

05.sql、include、bind 标签
    a.sql标签
        使用<sql>片段来封装表的全部字段, 然后通过<include>来引入
    b.bind
        使用OGNL表达式创建一个变量,并将其绑定在上下文中.

2.16 [4]一对一、一对多

01.一对一:association
    a.介绍
        一对一映射:用于映射两个表之间一对一的关系,一个对象对应另一个对象
    b.使用
        使用 association 标签:在 resultMap 中使用 association 标签进行一对一映射
    c.示例
        假设有两个表:users 和 addresses,每个用户有一个地址
    d.表结构
        CREATE TABLE users (
            id INT PRIMARY KEY,
            username VARCHAR(50),
            address_id INT
        );
        CREATE TABLE addresses (
            id INT PRIMARY KEY,
            street VARCHAR(50),
            city VARCHAR(50)
        );
   e.Java类
        public class User {
            private int id;
            private String username;
            private Address address;
            // getters and setters
        }
        public class Address {
            private int id;
            private String street;
            private String city;
            // getters and setters
        }
    f.XML映射文件
        <resultMap id="userMap" type="User">
             <id property="id" column="id"/>
             <result property="username" column="username"/>
             <association property="address" javaType="Address">
                 <id property="id" column="address_id"/>
                 <result property="street" column="street"/>
                 <result property="city" column="city"/>
             </association>
         </resultMap>
         <select id="getUserById" resultMap="userMap">
             SELECT u.id, u.username, a.id AS address_id, a.street, a.city
             FROM users u
             JOIN addresses a ON u.address_id = a.id
             WHERE u.id = #{id}
         </select>

02.一对多:collection
    a.介绍
        一对多映射:用于映射两个表之间一对多的关系,一个对象对应多个对象
    b.使用
        使用 collection 标签:在 resultMap 中使用 collection 标签进行一对多映射
    c.示例
        假设有两个表:departments 和 employees,每个部门有多个员工
    d.表结构
        CREATE TABLE departments (
            id INT PRIMARY KEY,
            name VARCHAR(50)
        );
        CREATE TABLE employees (
            id INT PRIMARY KEY,
            name VARCHAR(50),
            department_id INT
        );
    e.Java类
        public class Department {
            private int id;
            private String name;
            private List<Employee> employees;
            // getters and setters
        }
        public class Employee {
            private int id;
            private String name;
            // getters and setters
        }
    f.XML映射文件
        <resultMap id="departmentMap" type="Department">
             <id property="id" column="id"/>
             <result property="name" column="name"/>
             <collection property="employees" ofType="Employee">
                 <id property="id" column="employee_id"/>
                 <result property="name" column="employee_name"/>
             </collection>
         </resultMap>
         <select id="getDepartmentById" resultMap="departmentMap">
             SELECT d.id, d.name, e.id AS employee_id, e.name AS employee_name
             FROM departments d
             LEFT JOIN employees e ON d.id = e.department_id
             WHERE d.id = #{id}
         </select>

2.17 [4]批量插入、批量更新

01.<insert>/<update> + <foreach>
    a.批量插入
        -- mysql
        <insert id="batchInsertUsers">
            INSERT INTO users (username, password) VALUES
            <foreach collection="list" item="user" separator=",">
                (#{user.username}, #{user.password})
            </foreach>
        </insert>
        -----------------------------------------------------------------------------------------------------
        -- oracle第一种
        <insert id="saveUsers">
            INSERT ALL
            <foreach collection="list" item="user" separator=" " close="SELECT * FROM dual" index="index">
                INTO USER (id, username, password)
                VALUES (#{user.id}, #{user.username}, #{user.password})
            </foreach>
        </insert>
        -----------------------------------------------------------------------------------------------------
        -- oracle第二种
        <insert id="saveUsers" parameterType="list">
            <foreach collection ="list" item="user" separator =";" open="begin" close = ";end;">
                INSERT INTO USER (id, username, password ) VALUES (#{user.id}, #{user.username}, #{user.password})
            </foreach>
        </insert>
    b.批量更新
        <update id="batchUpdateUsers">
            <foreach collection="list" item="user" separator=";">
                UPDATE users
                SET username = #{user.username}, password = #{user.password}
                WHERE id = #{user.id}
            </foreach>
        </update>

02.MyBatis的ExecutorType.BATCH 模式
    适用于MyBatis框架内部的批处理

03.数据库原生批处理功能
    直接使用JDBC提供的批处理方法

04.第三方库:MyBatisPlus
    使用第三方工具提供的批量操作方法

2.18 [4]parameterType、resultType、resultMap

00.总结
    parameterType:指定传递给 SQL 的参数类型
    resultType:指定 SQL 返回的结果类型
    resultMap:用于复杂的结果映射,自定义 SQL 结果与 Java 对象之间的关系

01.parameterType:输入参数
    a.介绍
        用于指定传递给 SQL 语句的参数类型
    b.使用
        在映射文件的 <select>, <insert>, <update>, <delete> 标签中使用
    c.示例
        <select id="getUser" parameterType="int" resultType="User">
            SELECT * FROM users WHERE id = #{id}
        </select>
        这里 parameterType="int" 指定参数类型为 int
    d.两种类型
        a.简单类型(8个基本类型+引用类型String)
            #{任意值}
            ${任意值},只能是value
        b.对象类型
            #{属性名}
            ${属性名}
    e.推荐使用parameterType而非parameterMap
        ParameterMap和resultMap类似,表示将查询结果集中列值的类型一一映射到java对象属性的类型上,在开发过程中不推荐这种方式。
        一般使用parameterType直接将查询结果列值类型自动对应到java对象属性类型上,不再配置映射关系一一对应,
        例如上述代码中下划线部分表示将查询结果类型自动对应到hdu.terence.bean.Message的Bean对象属性类型。

02.resultType:输出参数
    a.介绍
        用于指定 SQL 语句返回的结果类型,一般是 Java 类的全限定名或基本数据类型
    b.使用
        在映射文件的 <select> 标签中使用
    c.示例
        <select id="getUser" parameterType="int" resultType="User">
            SELECT * FROM users WHERE id = #{id}
        </select>
        这里 resultType="User" 指定返回类型为 User 类
    d.说明
        resultType 表示的是bean中的对象类,此时可以省略掉resultMap标签的映射,
        但是必须保证查询结果集中的属性和bean对象类中的属性是【一一对应】,此时大小写不敏感,但是有限制。
        1.简单类型(8个基本+String)
        2.输出类型为实体对象类型(resultType="Student")
        3.输出参数为实体对象类型的集合:虽然输出类型为集合,但是resultType依然写集合的元素类型(resultType:="Student")
        4.输出参数类型为HashMap,HashMap本身是一个集合,可以存放多个元素,但是根据提示发现。返回值为HashMap时。查询结果只能是一个学生(no,name)
    e.结论
        一个HashMap对应一个学生的多个元素(多个属性)[一个Map,一个学生],
        类似“二维数组”
        [
            {no=1, name=zs},       一个HashMap对象
            {no=2, name=1s},       一个HashMap对象
            {no=3, name=ww},       一个HashMap对象
            {no=4, name=zl}        一个HashMap对象
        ]

03.resultMap:结果映射
    a.介绍
        用于自定义 SQL 结果与 Java 对象之间的映射关系,适用于复杂的映射需求
    b.使用
        在映射文件中定义 <resultMap> 元素,并在 <select> 标签中引用
    c.示例
        <resultMap id="userMap" type="User">
            <id property="id" column="id"/>
            <result property="username" column="username"/>
            <result property="password" column="password"/>
        </resultMap>
        <select id="getUser" parameterType="int" resultMap="userMap">
            SELECT * FROM users WHERE id = #{id}
        </select>
        这里定义了一个 resultMap,并在 select 查询中引用
    d.说明
        resultMap表示将查询结果集中的列一一映射到bean对象的各个属性
        映射的查询结果集中的列标签可以根据需要灵活变化
        并且在映射关系中,还可以通过typeHandler设置实现查询结果值的类型转换,比如布尔型与0/1的类型转换
    e.情况
        实体类的属性、数据表的字段,二者类型、名相同(stuno,stunos):resultType
        实体类的属性、数据表的字段,二者类型、名不同(stuno,id):1.resultMap="queryStudentById"
                                                             2.resultType="student" + id "stuno", name "stuname"

2.19 [5]存储过程

01.思路
    a.创建
        在数据库中定义存储过程
    b.使用
        a.方式1:配置映射文件
            在MyBatis映射文件中配置存储过程调用
        b.方式2:定义Mapper接口
            在接口中定义方法并使用@Select注解
        c.方式3:调用存储过程
            通过SqlSession获取Mapper并调用方法

02.调用步骤
    a.创建存储过程
        CREATE PROCEDURE getUserById(IN userId INT, OUT userName VARCHAR(50))
        BEGIN
            SELECT username INTO userName FROM users WHERE id = userId;
        END;
    b.配置
        a.第一种:配置映射文件
            <mapper namespace="com.example.UserMapper">
                <select id="callGetUserById" statementType="CALLABLE">
                    {CALL getUserById(#{userId, mode=IN, jdbcType=INTEGER}, #{userName, mode=OUT, jdbcType=VARCHAR})}
                </select>
            </mapper>
        b.第二种:定义 Mapper 接口
            public interface UserMapper {
                @Select("{CALL getUserById(#{userId, mode=IN, jdbcType=INTEGER}, #{userName, mode=OUT, jdbcType=VARCHAR})}")
                @Options(statementType = StatementType.CALLABLE)
                void callGetUserById(Map<String, Object> params);
            }
        c.第三种:通过SqlSession获取Mapper并调用方法
            try (SqlSession session = sqlSessionFactory.openSession()) {
                UserMapper mapper = session.getMapper(UserMapper.class);
                Map<String, Object> params = new HashMap<>();
                params.put("userId", 1);
                params.put("userName", "");
                mapper.callGetUserById(params);
                String userName = (String) params.get("userName");
                System.out.println(userName);  // 输出存储过程返回的 userName
            }

2.20 [5]延迟加载:lazy=“true”

01.延迟加载
    在真正使用数据时才进行加载,而不是在对象创建时立即加载

02.使用
    在MyBatis配置文件中启用延迟加载,使用lazy属性配置具体的关联映射是否延迟加载

03.配置步骤
    a.在MyBatis配置文件中启用延迟加载
        <settings>
            <setting name="lazyLoadingEnabled" value="true"/>
            <setting name="aggressiveLazyLoading" value="false"/>
        </settings>
    b.配置一对多或一对一映射的延迟加载
        a.示例:一对一映射
            <resultMap id="userMap" type="User">
                <id property="id" column="id"/>
                <result property="username" column="username"/>
                <association property="address" javaType="Address" select="getAddressById" lazy="true"/>
            </resultMap>
            <select id="getAddressById" resultType="Address">
                SELECT id, street, city FROM addresses WHERE id = #{id}
            </select>
            <select id="getUserById" resultMap="userMap">
                SELECT id, username, address_id FROM users WHERE id = #{id}
            </select>
        b.示例:一对多映射
            <resultMap id="departmentMap" type="Department">
                <id property="id" column="id"/>
                <result property="name" column="name"/>
                <collection property="employees" ofType="Employee" select="getEmployeesByDepartmentId" lazy="true"/>
            </resultMap>
            <select id="getEmployeesByDepartmentId" resultType="Employee">
                SELECT id, name FROM employees WHERE department_id = #{departmentId}
            </select>
            <select id="getDepartmentById" resultMap="departmentMap">
                SELECT id, name FROM departments WHERE id = #{id}
            </select>

2.21 [5]分页原理:RowBounds对象

00.分页插件
    MyBatis-Plus
    Mybatis-PageHelper

01.pagehelper分页
    a.原理
        【RowBounds】对象进行分页,使用RowBounds(m,n),其中m表示开始位置,n表示间隔
    b.使用
        public Object getUsers(int pageNum, int pageSize) {
            PageHelper.startPage(pageNum, pageSize);
            // 不带分页的查询
            List<UserEntity> list = userMapper.selectAllWithPage(null);
            // 可以将结果转换为Page,然后获取count和其他结果值
            com.github.pagehelper.Page listWithPage = (com.github.pagehelper.Page) list;
            System.out.println("listCnt:" + listWithPage.getTotal());
            return list;
        }
    c.说明
        使用的时候,只需在查询list前,调用 startPage 设置分页信息,即可使用分页功能
        即使用时,只需提前声明要分页的信息,得到的结果就是有分页信息的了
        如果不想进行count,只要查分页数据,则调用:PageHelper.startPage(pageNum, pageSize, false);

02.limit分页
    a.公式
        limit (currentPage-1)*pageSize,pageSize
    b.说明
        //表示查询第一页的10条数据,也就是第1 -10条数据
        select * from table limit 0,10;
        //表示查询第二页的10条数据,也就是第11-20条数据
        select * from table limit 10,10;
         //表示查询第三页的10条数据,也就是第21-30条数据
        select * from table limit 20,10;

03.PageHelper实现分页原理
    a.示例
        a.依赖
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>1.3.0</version>
            </dependency>
        b.使用
            @Test
            public void select() {
                PageHelper.startPage(1, 10);
                List<Order> orders = orderMapper.queryAll();
                log.info("记录条数为:{}", orders.size());
            }
    b.实现原理
        a.使用 ThreadLocal 记录分页参数
            a.说明
                在调用 startPage 方法时,会通过 ThreadLocal 存储当前分页参数
            b.代码
                public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
                    Page<E> page = new Page<E>(pageNum, pageSize, count);
                    page.setReasonable(reasonable);
                    page.setPageSizeZero(pageSizeZero);
                    //当已经执行过orderBy的时候
                    Page<E> oldPage = getLocalPage();
                    if (oldPage != null && oldPage.isOrderByOnly()) {
                        page.setOrderBy(oldPage.getOrderBy());
                    }
                    //设置ThreadLocal
                    setLocalPage(page);
                    return page;
                }
        b.使用 Mybatis 拦截器机制
            a.说明
                Mybatis 的拦截器机制是指用户可以自定义拦截器,通过代理的方式,增强 Mybatis 中的核心组件
                目前只支持 4 个核心组件:ParameterHandler、ResultSetHandler、StatementHandler 和 Executor
            b.通过 Interceptor 接口可以实现自定义拦截器
                public interface Interceptor {

                    Object intercept(Invocation invocation) throws Throwable;
                    // 默认方法
                    default Object plugin(Object target) {
                    return Plugin.wrap(target, this);
                    }

                    default void setProperties(Properties properties) {
                    // NOP
                    }
                }
            c.在 Configuration 初始化配置时,XMLConfigBuilder 会在配置文件中读取 对应的 Interceptor
                private void pluginElement(XNode parent) throws Exception {
                    if (parent != null) {
                      for (XNode child : parent.getChildren()) {
                        //解析Interceptor
                        String interceptor = child.getStringAttribute("interceptor");
                        Properties properties = child.getChildrenAsProperties();
                        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
                        interceptorInstance.setProperties(properties);

                        configuration.addInterceptor(interceptorInstance);
                      }
                    }
                  }
            d.在创建这4个核心对象后,InterceptorChain 都会尝试执行 pluginAll 方法 是否需要创建代理对象增强该类
                //创建ParameterHandler
                public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
                    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
                    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
                    return parameterHandler;
                }
                // 创建ResultSetHandler
                public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
                  ResultHandler resultHandler, BoundSql boundSql) {
                    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
                    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
                    return resultSetHandler;
                }
                //创建StatementHandler
                public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
                    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
                    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
                    return statementHandler;
                }
                //创建Executor
                public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
                    executorType = executorType == null ? defaultExecutorType : executorType;
                    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
                    Executor executor;
                    if (ExecutorType.BATCH == executorType) {
                      executor = new BatchExecutor(this, transaction);
                    } else if (ExecutorType.REUSE == executorType) {
                      executor = new ReuseExecutor(this, transaction);
                    } else {
                      executor = new SimpleExecutor(this, transaction);
                    }
                    if (cacheEnabled) {
                      executor = new CachingExecutor(executor);
                    }
                    executor = (Executor) interceptorChain.pluginAll(executor);
                    return executor;
                }
            e.在 InterceptorChain 的pluginAll 方法,中会调用 Interceptor 接口的默认方法 plugin 方法进行判断
                public Object pluginAll(Object target) {
                    for (Interceptor interceptor : interceptors) {
                      target = interceptor.plugin(target);
                    }
                    return target;
                }
            f.再看看 plugin 方法,实则是调用 Plugin 的 wrap 方法
                default Object plugin(Object target) {
                    return Plugin.wrap(target, this);
                }
            g.Plugin 的 wrap 方法如下
                public static Object wrap(Object target, Interceptor interceptor) {

                    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
                    Class<?> type = target.getClass();
                    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
                    if (interfaces.length > 0) {
                      return Proxy.newProxyInstance(
                          type.getClassLoader(),
                          interfaces,
                          new Plugin(target, interceptor, signatureMap));
                    }
                    return target;
                  }
                ---------------------------------------------------------------------------------------------
                先获取 Interceptor 注解上的参数,比如 class 类,方法名称,方法参数,这三个参数可以准确获取某个类的某个方法
                接着判断当前的 Interceptor  是否符合当前类,如果符合就去创建代理对象
                signatureMap 的 key 是类的对象,value 是该类对象中的方法 set 集合
            h.在 Plugin 的 Invoke 方法中
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    try {
                      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
                      if (methods != null && methods.contains(method)) {
                        return interceptor.intercept(new Invocation(target, method, args));
                      }
                      return method.invoke(target, args);
                    } catch (Exception e) {
                      throw ExceptionUtil.unwrapThrowable(e);
                    }
                }
                ---------------------------------------------------------------------------------------------
                先判断当前方法是不是存在于 Set 集合中,如果存在就执行 interceptor 的 intercept 方法,如果不存在就执行原方法
        c.PageHelper 自定义 Mybatis 拦截器类
            a.PageHelper 自定义的拦截器 PageInterceptor 类中,会调用 ExecutorUtil 类的 pageQuery 方法去执行分页操作
                @Intercepts(
                        {
                                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
                        }
                )
                public class PageInterceptor implements Interceptor {
                    @Override
                    public Object intercept(Invocation invocation) throws Throwable {
                        try {
                            //...
                            List resultList;
                            //调用方法判断是否需要进行分页,如果不需要,直接返回结果
                            if (!dialect.skip(ms, parameter, rowBounds)) {
                                //判断是否需要进行 count 查询
                                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                                    //查询总数
                                    Long count = count(executor, ms, parameter, rowBounds, null, boundSql);
                                    //处理查询总数,返回 true 时继续分页查询,false 时直接返回
                                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                                        //当查询总数为 0 时,直接返回空的结果
                                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                                    }
                                }
                                // 进行分页处理
                                resultList = ExecutorUtil.pageQuery(dialect, executor,
                                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
                            } else {
                                //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
                                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
                            }
                            return dialect.afterPage(resultList, parameter, rowBounds);
                        } finally {
                            if(dialect != null){
                                dialect.afterAll();
                            }
                        }
                    }
                }
                ---------------------------------------------------------------------------------------------
                可见,它是针对 Executor 的 query 方法进行增强,目的是获取 BoundSql 类中的原 SQL
            b.ExecutorUtil.pageQuery 里面会调用抽象类 AbstractHelperDialect 类的 getPageSql 方法来实现对 BoundSql 中 sql 改写
                public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
                    String sql = boundSql.getSql();
                    //从ThreadLoacl中拿出分页参数
                    Page page = getLocalPage();
                    String orderBy = page.getOrderBy();
                    if (StringUtil.isNotEmpty(orderBy)) {
                        pageKey.update(orderBy);
                        sql = OrderByParser.converToOrderBySql(sql, orderBy);
                    }
                    if (page.isOrderByOnly()) {
                        return sql;
                    }
                    //调用子类的getPageSql方法
                    return getPageSql(sql, page, pageKey);
                }
            c.但实际上进行改写的是实现类 MySqlDialect
                @Override
                public String getPageSql(String sql, Page page, CacheKey pageKey) {
                    StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
                    sqlBuilder.append(sql);
                    if (page.getStartRow() == 0) {
                        sqlBuilder.append("\n LIMIT ? ");
                    } else {
                        sqlBuilder.append("\n LIMIT ?, ? ");
                    }
                    return sqlBuilder.toString();
                }
            d.可见上面的代码就是在原 SQL 中拼接分页参数

2.22 [5]一级缓存、二级缓存

00.总结
    a.回答
        一级缓存:SqlSession级别,默认开启,同一SqlSession内
        二级缓存:Mapper级别,需要手动配置,不同SqlSession共享
    b.图示
        特性       一级缓存(默认)      二级缓存
        作用范围   SqlSession           Mapper级,跨SqlSession共享
        默认开启   是                   否
        适用场景   单次会话中频繁查询    多线程访问相同数据,且读多写少
        性能影响   低                   频繁修改时影响较高

01.一级缓存
    a.介绍
        默认开启,基于SqlSession级别的缓存
        即在同一个SqlSession中,查询相同的数据只会从数据库中读取一次,后续查询会直接从缓存中获取
    b.特点
        作用范围:SqlSession范围内
        默认开启:无需配置,MyBatis自动管理
        失效条件:增删改操作后,缓存会失效
    c.使用
        无需特别配置,使用默认设置即可
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            User user1 = mapper.getUserById(1);
            User user2 = mapper.getUserById(1);
            // user1 和 user2 是同一个对象,来自一级缓存
        }

02.二级缓存
    a.介绍
        基于Mapper映射级别的缓存,不同SqlSession共享该缓存
    b.特点
        作用范围:同一个Mapper范围内,不同SqlSession共享
        需要手动配置:需要在MyBatis配置文件和 Mapper映射文件中启用
        配置灵活:可以定制缓存策略,如使用第三方缓存框架(如 Ehcache)
    c.使用
        a.在MyBatis配置文件中启用二级缓存
            <settings>
                <setting name="cacheEnabled" value="true"/>
            </settings>
        b.在Mapper映射文件中配置二级缓存
            <cache />
        c.可选:配置缓存策略:
            <cache
                eviction="LRU"
                flushInterval="60000"
                size="512"
                readOnly="true"/>
            -------------------------------------------------------------------------------------------------
            eviction:缓存回收策略,常用的有 LRU(最近最少使用)。
            flushInterval:刷新间隔,单位毫秒。
            size:缓存大小。
            readOnly:只读缓存,提高性能但不允许修改缓存对象。
    d.示例
        <mapper namespace="com.example.UserMapper">
            <cache />
            <select id="getUserById" resultType="User">
                SELECT * FROM users WHERE id = #{id}
            </select>
        </mapper>

2.23 [5]自定义插件:拦截器,Interceptor

01.思路
    1.编写拦截器
    2.编写签名注解
    3.配置

02.编写插件
    1.实现 Mybatis 的 Interceptor 接口,并实现 #intercept(...) 方法
    2.在给插件编写注解,指定要拦截哪一个接口的哪些方法即可
    3.在配置文件中配置你编写的插件。

03.插件运行原理
    a.拦截目标接口
        ParameterHandler:负责处理 SQL 语句中的参数
        ResultSetHandler:负责将 JDBC 返回的结果集转换为 List 集合
        StatementHandler:负责执行 SQL 语句
        Executor:负责执行增删改查等操作
    b.使用动态代理
        MyBatis 使用 JDK 动态代理机制为需要拦截的接口生成代理对象
        当这些接口的方法被调用时,代理对象会拦截调用,并将其转发到插件的拦截方法中
    c.实现 Interceptor 接口
        开发者需要实现 MyBatis 提供的 Interceptor 接口,并重写 intercept() 方法
        intercept() 方法会在目标方法被调用时执行,开发者可以在此方法中添加自定义逻辑
    d.指定拦截方法
        在实现 Interceptor 接口时,可以通过注解或配置文件指定要拦截的接口和方法
        这通常通过 @Intercepts 和 @Signature 注解来实现,指定拦截的类型、方法和参数
    e.配置插件
        在 MyBatis 配置文件中,需要注册自定义的插件
        这可以通过 XML 配置或 Java 配置来完成

3 MyBatisPlus

3.1 [1]基础:代码生成器

01.MyBatis
    模板配置文件
    student表 -> Student类、Mapper接口、mapper.xml

02.MyBatisPlus
    类
    student表 -> Student类、Mapper接口、mapper.xml、Service、Controller

3.2 [1]基础:ActiveRecord模式

01.定义
    MyBatis Plus提供的一种ORM风格,使实体类具备直接执行数据库操作的能力
    通过继承Model类,实体类可以使用 insert、deleteById、updateById、selectById等方法

02.主要特点
    面向对象:实体类自身包含数据操作方法
    简化开发:减少编写 Mapper 接口和 XML 配置文件的需求
    易用性:实体类继承 Model 类,即可使用 ActiveRecord 功能

03.使用方法
    a.继承Model类
        public class User extends Model<User> {
            private Long id;
            private String name;
        }
    b.常用方法
        insert(): 插入记录
        deleteById(Serializable id): 根据 ID 删除记录
        updateById(): 根据 ID 更新记录
        selectById(Serializable id): 根据 ID 查询记录
        selectList(Wrapper<T> queryWrapper): 查询符合条件的记录列表

3.3 [1]基础:BaseMapper接口/IService接口

00.BaseMapper接口、IService接口
    IService接口:服务层接口,save、removeById、updateById、getById、list                      this.listByIds()
    BaseMapper接口:基础Mapper接口,insert、deleteById、updateById、selectById、selectList     this.baseMapper.insert() / deleteByApplicantId()

01.BaseMapper接口
    a.定义
        BaseMapper 接口的全限定名称为 com.baomidou.mybatisplus.core.mapper.BaseMapper<T>,该接口提供了插入、修改、删除和查询接口
        MyBatis Plus 提供了通用的 Mapper 接口(即 BaseMapper 接口),该接口对应我们的 DAO 层
    b.方法
        插入:1个      insert
        删除:4个      delete
        更新:2个      update
        查询:10个     select

02.IService接口
    a.定义
        Service CRUD 实现了 IService 接口,进一步封装 CRUD
    b.方法
        保存数据:3个              Save
        保存或更新数据:4个        SaveOrUpdate
        删除数据:4个              Remove
        更新数据:5个              Update
        获取单条数据:5个          Get
        获取数据列表:10个         List
        数据分页查询:8个          Page
        统计数据条数:2个          Count
        链式查询:3个              Chain

03.BaseMapper接口
    a.插入:1个 (insert)
        insert
    b.删除:4个 (delete)
        deleteById
        deleteByMap
        delete
        deleteBatchIds
    c.更新:2个 (update)
        updateById
        update
    d.查询:10个 (select)
        selectById
        selectBatchIds
        selectByMap
        selectOne
        selectCount
        selectList
        selectMaps
        selectObjs
        selectPage
        selectMapsPage

04.IService接口
    a.保存数据:3个 (Save)
        1.save
        2.saveBatch
        3.saveOrUpdateBatch
    b.保存或更新数据:4个 (SaveOrUpdate)
        1.saveOrUpdate
        2.saveOrUpdateBatch(注意: 此方法同时归类于“保存数据”和“保存或更新数据”中)
        3.lambdaQuery
        4.lambdaUpdate
    c.删除数据:4个 (Remove)
        1.remove
        2.removeById
        3.removeByIds
        4.removeByMap
    d.更新数据:5个 (Update)
        1.update(T entity)
        2.update(T entity, Wrapper<T> updateWrapper): 根据条件更新指定实体
        3.updateById: 根据ID更新实体
        4.updateBatchById: 根据ID批量更新实体
        5.lambdaUpdate: 获取一个 LambdaUpdateWrapper,用于链式更新
    e.获取单条数据:5个 (Get)
        1.getById
        2.getOne
        3.getMap
        4.getObj
        5.lambdaQuery
    f.获取数据列表:10个 (List)
        1.list
        2.listByIds
        3.listByMap
        4.listMaps
        5.listObjs
        6.page
        7.pageMaps
        8.query
        9.lambdaQuery
        10. lambdaUpdate (虽然主要用于更新,但其 list 方法可用于查询)
    g.数据分页查询:8个 (Page)
        1.page
        2.pageMaps
        3.list (其重载方法可以接受分页参数)
        4.listMaps (其重载方法可以接受分页参数)
        5.listObjs (其重载方法可以接受分页参数)
        6.query
        7.lambdaQuery
        8.chainQuery (已不推荐,但历史版本存在)
    h.统计数据条数:2个 (Count)
        1.count
        2.lambdaQuery().count()
    i.链式查询:3个 (Chain)
        1.query
        2.lambdaQuery
        3.update (严格来说是链式更新,但常与链式查询一起提及) 或 lambdaUpdate

3.4 [1]核心:动态SQL1

00.汇总
    a.分类
        01.if标签:条件判断
        02.where+if标签
        03.set标签
        04.choose(when,otherwise) 语句
        05.trim
        06.foreach
        07.sql
        08.include
    b.代码
        <?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.yzx.mapper.StuMapper">
            <sql id="selectvp">
                select  *  from  student
            </sql>
            
            <select id="find" resultType="Student">
                <include refid="selectvp"/>
            </select>

            <select id="findbyid"  resultType="student">
                <include refid="selectvp"/>
                WHERE 1=1
                <if test="sid != null">
                    AND sid like #{sid}
                </if>
            </select>

                <select id="findQuery" resultType="Student">
                    <include refid="selectvp"/>
                    <where>
                        <if test="sacc != null">
                            sacc like concat('%' #{sacc} '%')
                        </if>
                        <if test="sname != null">
                            AND sname like concat('%' #{sname} '%')
                        </if>
                        <if test="sex != null">
                            AND sex=#{sex}
                        </if>
                        <if test="phone != null">
                            AND phone=#{phone}
                        </if>
                    </where>
                </select>

            <update id="upd">
                update student
                <set>
                    <if test="sname != null">sname=#{sname},</if>
                    <if test="spwd != null">spwd=#{spwd},</if>
                    <if test="sex != null">sex=#{sex},</if>
                    <if test="phone != null">phone=#{phone}</if>
                sid=#{sid}
                </set>
                where sid=#{sid}
            </update>

            <insert id="add">
                insert  into student
                <trim prefix="(" suffix=")" suffixOverrides=",">
                    <if test="sname != null">sname,</if>
                    <if test="spwd != null">spwd,</if>
                    <if test="sex != null">sex,</if>
                    <if test="phone != null">phone,</if>
                </trim>

                <trim prefix="values (" suffix=")"  suffixOverrides=",">
                    <if test="sname != null">#{sname},</if>
                    <if test="spwd != null">#{spwd},</if>
                    <if test="sex != null">#{sex},</if>
                    <if test="phone != null">#{phone}</if>
                </trim>

            </insert>
            <select id="findAll" resultType="Student" parameterType="Integer">
                <include refid="selectvp"/> WHERE sid in
                <foreach item="ids" collection="array"  open="(" separator="," close=")">
                    #{ids}
                </foreach>
            </select>

            <delete id="del"  parameterType="Integer">
                delete  from  student  where  sid in
                <foreach item="ids" collection="array"  open="(" separator="," close=")">
                    #{ids}
                </foreach>
            </delete>
        </mapper>

01.if标签:条件判断
    a.不使用动态sql
        <if test="判断条件">
            SQL语句
        </if>
    b.使用动态sql
        <select id="selectAllWebsite" resultMap="myResult">  
            select id,name,url from website 
            where 1=1    
           <if test="name != null">        
               AND name like #{name}   
           </if>    
           <if test="url!= null">        
               AND url like #{url}    
           </if>
        </select>

02.where+if标签
    a.说明
        <if>失败后,<where> 关键字只会去掉库表字段赋值前面的and,不会去掉语句后面的and关键字
        即注意,<where> 只会去掉<if> 语句中的最开始的and关键字
    b.在每个条件前面加上 AND,并确保第一个条件没有连接符
        <select id="findQuery" resultType="Student">
            <include refid="selectvp"/>
            <where>
                <if test="sacc != null">
                    sacc LIKE CONCAT('%', #{sacc}, '%')
                </if>
                <if test="sname != null">
                    AND sname LIKE CONCAT('%', #{sname}, '%')
                </if>
                <if test="sex != null">
                    AND sex = #{sex}
                </if>
                <if test="phone != null">
                    AND phone = #{phone}
                </if>
            </where>
        </select>
    c.说明
        自动处理开头的 AND:<where> 标签会自动去掉第一个条件前面的 AND 或 OR,因此第一个条件不需要手动加连接符
        确保条件连接正确:从第二个条件开始,使用 AND 或 OR 连接条件,以确保生成的 SQL 语句是正确的
        动态 SQL:通过 <if> 标签实现动态 SQL,根据传入的参数动态生成查询条件

03.set标签
    a.代码
        <update id="upd">
            update student
            <set>
                <if test="sname != null">sname=#{sname},</if>
                <if test="spwd != null">spwd=#{spwd},</if>
                <if test="sex != null">sex=#{sex},</if>
                <if test="phone != null">phone=#{phone}</if>
            sid=#{sid}
            </set>
            where sid=#{sid}
        </update>

04.choose(when,otherwise) 语句
    a.说明
        类似Java的switch语句
    b.代码
        <select id="selectUserByChoose" resultType="com.ys.po.User" parameterType="com.ys.po.User">
              select * from user
              <where>
                  <choose>
                      <when test="id !='' and id != null">
                          id=#{id}
                      </when>
                      <when test="username !='' and username != null">
                          and username=#{username}
                      </when>
                      <otherwise>
                          and sex=#{sex}
                      </otherwise>
                  </choose>
              </where>
          </select>
    c.说明
        这里我们有三个条件,id、username、sex,只能选择一个作为查询条件
        如果 id 不为空,那么查询语句为:select * from user where id=?
        如果 id 为空,那么看username 是否为空,如果不为空,那么语句为 select * from user where username=?;
        如果 username 为空,那么查询语句为 select * from user where sex=?

05.trim
    a.说明
        trim标记是一个格式化的标记,可以完成set或者是where标记的功能
    b.用 trim 改写 if+where 语句
        <select id="selectUserByUsernameAndSex" resultType="user" parameterType="com.ys.po.User">
            select * from user
            <!-- <where>
                <if test="username != null">
                   username=#{username}
                </if>
                 
                <if test="username != null">
                   and sex=#{sex}
                </if>
            </where>  -->
            <trim prefix="where" prefixOverrides="and | or">
                <if test="username != null">
                   and username=#{username}
                </if>
                <if test="sex != null">
                   and sex=#{sex}
                </if>
            </trim>
        </select>
        -----------------------------------------------------------------------------------------------------
        prefix:前缀
        prefixoverride:去掉第一个and或者是or
    c.用 trim 改写 if+set 语句
        <!-- 根据 id 更新 user 表的数据 -->
        <update id="updateUserById" parameterType="com.ys.po.User">
            update user u
                <!-- <set>
                    <if test="username != null and username != ''">
                        u.username = #{username},
                    </if>
                    <if test="sex != null and sex != ''">
                        u.sex = #{sex}
                    </if>
                </set> -->
                <trim prefix="set" suffixOverrides=",">
                    <if test="username != null and username != ''">
                        u.username = #{username},
                    </if>
                    <if test="sex != null and sex != ''">
                        u.sex = #{sex},
                    </if>
                </trim>
             
             where id=#{id}
        </update>
        -----------------------------------------------------------------------------------------------------
        suffix:后缀
        suffixoverride:去掉最后一个逗号
    d.trim+if同时使用可以添加
        <insert id="add">
            insert  into student
            <trim prefix="(" suffix=")" suffixOverrides=",">
                <if test="sname != null">sname,</if>
                <if test="spwd != null">spwd,</if>
                <if test="sex != null">sex,</if>
                <if test="phone != null">phone,</if>
            </trim>

            <trim prefix="values (" suffix=")"  suffixOverrides=",">
                <if test="sname != null">#{sname},</if>
                <if test="spwd != null">#{spwd},</if>
                <if test="sex != null">#{sex},</if>
                <if test="phone != null">#{phone}</if>
            </trim>

        </insert>

06.foreach
    a.说明
        你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach
        当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素
        当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值
    b.代码
        //批量查询
        <select id="findAll" resultType="Student" parameterType="Integer">
            <include refid="selectvp"/> WHERE sid in
            <foreach item="ids" collection="array"  open="(" separator="," close=")">
                #{ids}
            </foreach>
        </select>
        //批量删除
        <delete id="del"  parameterType="Integer">
            delete  from  student  where  sid in
            <foreach item="ids" collection="array"  open="(" separator="," close=")">
                #{ids}
            </foreach>
        </delete>

07.sql
    a.说明
        当多种类型的查询语句的查询字段或者查询条件相同时,可以将其定义为常量,方便调用
    b.代码
        <sql id="selectvp">
            select * from student
        </sql>

08.include
    a.说明
        这个标签和<sql>是天仙配,是共生的,include用于引用sql标签定义的常量。比如引用上面sql标签定义的常量
        refid这个属性就是指定<sql>标签中的id值(唯一标识)
    b.代码
        <select id="findbyid"  resultType="student">
            <include refid="selectvp"/>
            WHERE 1=1
            <if test="sid != null">
                AND sid like #{sid}
            </if>
        </select>

3.5 [1]核心:动态SQL2

00.汇总
    a.基础标签
        select标签
        insert标签
        update标签
        delete标签
    b.动态SQL标签
        if标签
        choose-when-otherwise标签
        where标签
        set 标签
        trim标签
        foreach标签
    c.高级映射
        resultMap标签
        sql标签
    d.特殊功能
        bind 标签
        特殊字符:CDATA 区段(关联 XML 实体)
        特殊字符:XML 实体(关联 CDATA 区段)
    e.实用场景示例
        动态表关联
        批量操作优化

01.基础标签
    a.select 标签
        a.示例代码
            <select id="getById" resultType="User" parameterType="long">
                SELECT * FROM user WHERE id = #{id}
            </select>
        b.用于查询操作
            id: 对应 Mapper 接口中的方法名
            resultType: 返回的结果类型
            parameterType: 参数类型(可选)
    b.insert 标签
        a.示例代码
            <insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
                INSERT INTO user (username, age) VALUES (#{username}, #{age})
            </insert>
        b.用于插入操作
            useGeneratedKeys: 是否使用自增主键
            keyProperty: 自增主键对应的属性名
    c.update 标签
        a.示例代码
            <update id="update" parameterType="User">
                UPDATE user SET username = #{username} WHERE id = #{id}
            </update>
        b.用于更新操作
            parameterType: 参数类型
    d.delete 标签
        a.示例代码
            <delete id="deleteById" parameterType="long">
                DELETE FROM user WHERE id = #{id}
            </delete>
        b.用于删除操作
            parameterType: 参数类型

02.动态 SQL 标签
    a.if 标签
        a.示例代码
            <if test="username != null and username != ''">
                AND username LIKE CONCAT('%', #{username}, '%')
            </if>
        b.说明
            条件判断,当 test 属性中的条件成立时,才会将其包含的 SQL 片段拼接到最终的 SQL 中
            test: 判断条件,支持 OGNL 表达式
    b.choose-when-otherwise 标签
        a.示例代码
            <choose>
                <when test="status != null">
                    AND status = #{status}
                </when>
                <when test="type != null">
                    AND type = #{type}
                </when>
                <otherwise>
                    AND create_time > SYSDATE
                </otherwise>
            </choose>
        b.说明
            类似 Java 中的 switch 语句,只会执行一个条件
    c.where 标签
        a.示例代码
            <where>
                <if test="username != null">
                    AND username = #{username}
                </if>
                <if test="age != null">
                    AND age >= #{age}
                </if>
            </where>
        b.说明
            自动处理 WHERE 子句,会自动去除多余的 AND 或 OR
    d.set 标签
        a.示例代码
            <update id="updateUser">
                UPDATE user
                <set>
                    <if test="username != null">username = #{username},</if>
                    <if test="age != null">age = #{age},</if>
                </set>
                WHERE id = #{id}
            </update>
        b.说明
            用在 UPDATE 语句中,自动处理 SET 关键字和逗号
    e.trim 标签
        a.示例代码
            <trim prefix="WHERE" prefixOverrides="AND|OR" suffix=")" suffixOverrides=",">
                <if test="username != null">
                    AND username = #{username},
                </if>
            </trim>
        b.更灵活的前缀/后缀处理
            prefix: 给整个语句增加前缀
            prefixOverrides: 去除整个语句前面的关键字或字符
            suffix: 给整个语句增加后缀
            suffixOverrides: 去除整个语句后面的关键字或字符
            运行结果 : WHERE username = #{username}
    f.foreach 标签
        a.示例代码
            <foreach collection="list" item="item" index="index" open="(" separator="," close=")">
                #{item}
            </foreach>
        b.循环遍历集合或数组
            collection: 要遍历的集合或数组(list、array、map等)
            item: 当前遍历的元素
            index: 当前遍历的索引
            open: 开始字符
            separator: 分隔符
            close: 结束字符

03.高级映射
    a.resultMap 标签
        a.示例代码
            <resultMap id="UserResultMap" type="User">
                <!-- 主键映射 -->
                <id property="id" column="user_id"/>

                <!-- 普通属性映射 -->
                <result property="username" column="user_name"/>

                <!-- 一对一关系 -->
                <association property="profile" javaType="UserProfile">
                    <id property="id" column="profile_id"/>
                    <result property="address" column="address"/>
                </association>

                <!-- 一对多关系 -->
                <collection property="roles" ofType="Role">
                    <id property="id" column="role_id"/>
                    <result property="roleName" column="role_name"/>
                </collection>
            </resultMap>
        b.自定义结果映射关系
            a.<resultMap>标签
                id="UserResultMap": 定义了这个 <resultMap> 的唯一标识符,以便在其他地方引用它
                type="User": 指定了这个 <resultMap> 所映射的目标类型,这里是 User 类
            b.主键映射
                <id property="id" column="user_id"/>:映射数据库表中的 user_id 列到 User 对象的 id 属性上,<id> 标签通常用于主键映射,有助于提高 MyBatis 在处理关联关系时的性能
            c.普通属性映射
                <result property="username" column="user_name"/>:将数据库表中的 user_name 列映射到 User 对象的 username 属性上,<result> 标签用于普通属性的映射
            d.一对一关系 (<association>)
                property="profile" javaType="UserProfile":表示 User 对象与 UserProfile 对象之间存在一对一的关系,并且将查询结果映射到 User 对象的 profile 属性上
                <id property="id" column="profile_id"/>:映射 UserProfile 对象的主键
                <result property="address" column="address"/>:映射 UserProfile 对象的 address 属性
            e.一对多关系 (<collection>)
                property="roles" ofType="Role":表示 User 对象与多个 Role 对象之间存在一对多的关系,并且将查询结果映射到 User 对象的 roles 属性上
                <id property="id" column="role_id"/>:映射每个 Role 对象的主键
                <result property="roleName" column="role_name"/>:映射每个 Role 对象的 roleName 属性
    b.sql 标签
        a.示例代码
            <!-- 定义 SQL 片段 -->
            <sql id="Base_Column_List">
                id, username, age, email, create_time
            </sql>

            <!-- 使用 SQL 片段 -->
            <select id="selectById">
                SELECT <include refid="Base_Column_List"/> FROM user
            </select>
        b.说明
            定义可重用的 SQL 片段

04.特殊功能
    a.bind 标签
        a.示例代码
            <select id="selectByName">
                <bind name="pattern" value="'%' + name + '%'"/>
                SELECT * FROM user WHERE username LIKE #{pattern}
            </select>
        b.说明
            创建一个变量并绑定到上下文中
    b.特殊字符:CDATA 区段(关联 XML 实体)
        a.示例代码
            <select id="getUsers">
                SELECT * FROM user
                WHERE age <![CDATA[ >= ]]> #{minAge}
            </select>
        b.说明
            处理特殊字符
    c.特殊字符:XML 实体(关联 CDATA 区段)
        a.预定义的 XML 实体
            <select id="getUsers">
                SELECT * FROM user
                WHERE age &gt;= #{minAge}
            </select>
            -------------------------------------------------------------------------------------------------
            < 对应 &lt;
            > 对应 &gt;
            & 对应 &amp;
            " 对应 &quot;
            ' 对应 &apos;
        b.自定义实体
            <!-- DOCTYPE声明 -->
            <!DOCTYPE example [
                <!ENTITY myEntity "This is a custom entity">
            ]>

            <!-- 使用 -->
            <example>
                Here is the custom entity: &myEntity;
            </example>

            <!-- 结果 -->
            <example>
                Here is the custom entity: This is a custom entity
            </example>
            -------------------------------------------------------------------------------------------------
            自定义实体允许你在XML文档中定义一些简短的替代符号,这些符号可以在文档的不同部分引用
            -------------------------------------------------------------------------------------------------
            代码解释
            <!DOCTYPE example [...] >: 这是一个DOCTYPE声明,用于定义当前文档的类型和结构。在这个例子中,example是根元素名称,方括号[]内包含了自定义实体的定义
            <!ENTITY myEntity "This is a custom entity">: 定义了一个名为myEntity的自定义实体。myEntity是一个标识符,"This is a custom entity"是这个实体的实际值。在文档的其他地方,你可以通过&myEntity;来引用这个实体
            <example>: 根元素,包含文档的主要内容
            Here is the custom entity: &myEntity;: 在这个元素的内容中,使用了之前定义的自定义实体&myEntity;。当解析器读取到这个标记时,会自动将其替换为实体的实际值"This is a custom entity"
            -------------------------------------------------------------------------------------------------
            渲染结果
            当你解析这个XML文档时,解析器会将&myEntity;替换为其定义的值。最终的结果将是:
            <example>
                Here is the custom entity: This is a custom entity
            </example>

05.实用场景示例
    a.动态表关联
        a.示例代码
            <select id="getUserDetails" resultMap="UserDetailMap">
                SELECT u.*
                FROM user u
                <!-- 动态关联用户资料表 -->
                <if test="includeProfile">
                    LEFT JOIN user_profile up ON u.id = up.user_id
                </if>
                <!-- 动态关联角色表 -->
                <if test="includeRoles">
                    LEFT JOIN user_role ur ON u.id = ur.user_id
                    LEFT JOIN role r ON ur.role_id = r.id
                </if>
                WHERE u.id = #{userId}
            </select>
        b.说明
            a.说明
                根据条件决定是否关联其他表
                动态地从多个表中获取用户详细信息
            b.代码解析
                a.<select id="getUserDetails" resultMap="UserDetailMap">
                    定义了这个查询的唯一标识符,可以在Java代码中通过这个ID来调用该查询
                    并指定了结果映射(resultMap)
                b.SELECT u.* FROM user u
                    选择了user表中的所有列,并给这个表起了一个别名u
                c.动态关联用户资料表
                    <if test="includeProfile">
                        LEFT JOIN user_profile up ON u.id = up.user_id
                    </if>
                    -----------------------------------------------------------------------------------------
                    检查传入的参数includeProfile是否为true。如果为true,则执行内部的SQL片段
                d.动态关联角色表
                    <if test="includeRoles">
                        LEFT JOIN user_role ur ON u.id = ur.user_id
                        LEFT JOIN role r ON ur.role_id = r.id
                    </if>
                    -----------------------------------------------------------------------------------------
                    检查传入的参数includeRoles是否为true。如果为true,则执行内部的SQL片段
                e.WHERE u.id = #{userId}
                    这条语句用于过滤结果,只返回指定用户ID的数据
            c.调用
                a.传参
                    includeProfile = true
                    includeRoles = true
                    userId = 1
                b.生成的SQL
                    SELECT u.*
                    FROM user u
                    LEFT JOIN user_profile up ON u.id = up.user_id
                    LEFT JOIN user_role ur ON u.id = ur.user_id
                    LEFT JOIN role r ON ur.role_id = r.id
                    WHERE u.id
    b.批量操作优化
        a.示例代码
            <!-- MySQL批量更新 -->
            <update id="batchUpdate">
                UPDATE user
                <trim prefix="SET" suffixOverrides=",">
                    <trim prefix="username = CASE" suffix="END,">
                        <foreach collection="list" item="item">
                            WHEN id = #{item.id} THEN #{item.username}
                        </foreach>
                    </trim>
                </trim>
                WHERE id IN
                <foreach collection="list" item="item" open="(" separator="," close=")">
                    #{item.id}
                </foreach>
            </update>
        b.说明
            a.说明
                适用于大量数据的批量操作
                批量更新用户表中的数据
            b.代码解析
                a.<update id="batchUpdate">
                    定义了这个更新操作的唯一标识符,可以在Java代码中通过这个ID来调用该更新操作
                b.UPDATE user
                    指定了要更新的表名,这里是user表
                c.第一个 <trim> 标签
                    <trim prefix="SET" suffixOverrides=",">
                        ...
                    </trim>
                    -----------------------------------------------------------------------------------------
                    prefix="SET": 在生成的SQL语句前添加SET关键字
                    suffixOverrides=",": 自动移除生成的SQL语句末尾多余的逗号
                d.第二个 <trim> 标签
                    <trim prefix="username = CASE" suffix="END,">
                        ...
                    </trim>
                    -----------------------------------------------------------------------------------------
                    prefix="username = CASE": 在生成的SQL语句前添加username = CASE
                    suffix="END,": 在生成的SQL语句后添加END,
                e.嵌套的 <foreach> 标签
                    <foreach collection="list" item="item">
                        WHEN id = #{item.id} THEN #{item.username}
                    </foreach>
                    -----------------------------------------------------------------------------------------
                    collection="list": 指定要遍历的集合,这里假设传入的参数是一个包含多个用户的列表
                    item="item": 定义当前遍历项的变量名,这里是item
                f.WHERE id IN ...:
                    WHERE id IN
                    <foreach collection="list" item="item" open="(" separator="," close=")">
                        #{item.id}
                    </foreach>
                    -----------------------------------------------------------------------------------------
                    这部分使用另一个<foreach>标签来生成WHERE id IN (...)子句
            c.整体效果
                a.说明
                    假设你传入的参数是一个包含两个用户的列表,每个用户都有一个id和一个新的username值
                b.代码
                    List<User> userList = new ArrayList<>();
                    userList.add(new User(1, "newUsername1"));
                    userList.add(new User(2, "newUsername2"));
                c.说明
                    生成的最终SQL语句将会是
                d.代码
                    UPDATE user
                    SET username = CASE
                        WHEN id = 1 THEN 'newUsername1'
                        WHEN id = 2 THEN 'newUsername2'
                    END
                    WHERE id IN (1, 2);

3.6 [1]核心:关联查询

00.汇总
    一对多
    多对一
    多对多

01.一对多
    <!--一对多-->
    <resultMap id="myStudent1" type="student1">
        <id property="sid" column="sid"/>
        <result property="sname" column="sname"/>
        <result property="sex" column="sex"/>
        <result property="sage" column="sage"/>
        <collection property="list" ofType="teacher">
            <id property="tid" column="tid"/>
            <result property="tname" column="tname"/>
            <result property="tage" column="tage"/>
        </collection>
    </resultMap>

    <!--一对多-->
    <select id="find1" resultMap="myStudent1">
        select * from student1 s left join teacher t on s.sid=t.sid
    </select>

02.多对一
    <!--多对一-->
    <resultMap id="myTeacher" type="teacher">
        <id property="tid" column="tid"/>
        <result property="tname" column="tname"/>
        <result property="tage" column="tage"/>
        <association property="student1" javaType="Student1">
            <id property="sid" column="sid"/>
            <result property="sname" column="sname"/>
            <result property="sex" column="sex"/>
            <result property="sage" column="sage"/>
        </association>
    </resultMap>

    <!--多对一-->
    <select id="find2" resultMap="myTeacher">
    select * from teacher t right join student1 s on t.sid=s.sid
    </select>


03.多对多
    <!--多对多:以谁为主表查询的时候,主表约等于1的一方,另一方相当于N的一方-->
    <select id="find3" resultMap="myStudent1">
        select * from student1 s left join relevance r on s.sid=r.sid left join teacher t on r.tid=t.tid
    </select>

3.7 [1]核心:apply方法

00.汇总
    避免直接拼接 SQL,始终对用户输入进行验证和过滤
    apply 方法可以安全使用,但必须通过占位符和参数化查询来避免 SQL 注入
    使用 MyBatis-Plus 提供的工具类(如 SqlInjectionUtils)来检查输入的安全性

01.apply 方法的作用
    a.内容
        apply 方法用于在查询条件中拼接自定义的 SQL 片段。例如:
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.apply("date_format(create_time, '%Y-%m-%d') = {0}", "2023-10-01");
    b.说明
        这里的 {0} 是占位符,MyBatis-Plus 会自动将其替换为参数值

02.SQL 注入风险
    a.内容
        如果直接将用户输入拼接到 SQL 片段中,可能会导致 SQL 注入。例如:
        String userInput = "2023-10-01'; DROP TABLE user; --";
        queryWrapper.apply("date_format(create_time, '%Y-%m-%d') = '" + userInput + "'");
    b.生成的 SQL 可能会变成
        date_format(create_time, '%Y-%m-%d') = '2023-10-01'; DROP TABLE user; --'
        这将导致严重的 SQL 注入问题

03.防止 SQL 注入的方法
    a.方法 1:使用占位符和参数化查询
        MyBatis-Plus 的 apply 方法支持占位符(如 {0}、{1}),并会自动对参数进行转义,从而防止 SQL 注入
        String safeDate = "2023-10-01";
        queryWrapper.apply("date_format(create_time, '%Y-%m-%d') = {0}", safeDate);
        -----------------------------------------------------------------------------------------------------
        生成的 SQL 是安全的:
        date_format(create_time, '%Y-%m-%d') = '2023-10-01'
    b.方法 2:使用 SqlInjectionUtils 检查输入
        MyBatis-Plus 提供了 SqlInjectionUtils 工具类,可以用于检查输入是否包含 SQL 注入风险
        -----------------------------------------------------------------------------------------------------
        import com.baomidou.mybatisplus.core.toolkit.sql.SqlInjectionUtils;

        String userInput = "2023-10-01'; DROP TABLE user; --";
        if (SqlInjectionUtils.check(userInput)) {
            throw new IllegalArgumentException("输入包含非法字符");
        }
        queryWrapper.apply("date_format(create_time, '%Y-%m-%d') = {0}", userInput);
    c.方法 3:避免直接拼接 SQL
        尽量避免直接拼接 SQL 片段,而是使用 MyBatis-Plus 提供的安全方法。例如,使用 eq、like 等方法:
        -----------------------------------------------------------------------------------------------------
        queryWrapper.eq("date_format(create_time, '%Y-%m-%d')", "2023-10-01");

04.最佳实践
    a.始终使用占位符
        使用 {0}、{1} 等占位符,而不是直接拼接字符串
    b.验证用户输入
        对用户输入进行严格的验证和过滤,确保其符合预期格式
    c.使用安全的 API
        尽量使用 MyBatis-Plus 提供的安全方法(如 eq、like 等),而不是直接拼接 SQL
    d.定期检查代码
        定期检查代码中是否存在直接拼接 SQL 的情况,及时修复潜在的安全问题

05.示例代码
    a.安全使用 apply 方法
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        String safeDate = "2023-10-01";
        queryWrapper.apply("date_format(create_time, '%Y-%m-%d') = {0}", safeDate);
        List<User> userList = userMapper.selectList(queryWrapper);
    b.不安全使用 apply 方法(不推荐)
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        String unsafeDate = "2023-10-01'; DROP TABLE user; --";
        queryWrapper.apply("date_format(create_time, '%Y-%m-%d') = '" + unsafeDate + "'");
        List<User> userList = userMapper.selectList(queryWrapper);

3.8 [1]核心:QueryWrapper

00.汇总
    a.分类
        a.字段查询
            select          字段查询
        b.等值查询
            eq              条件筛选属性 = ?
            allEq           满足多个条件字段的值的筛选
            ne              不等于
        c.范围查询
            gt              >
            ge              >=
            It              <
            le              <=
            between
            notBetween
        d.模糊查询
            like            %值%
            notLike         不满足%值%
            likeLeft        %值
            likeRight       值%
        e.判空查询
            isNUI           判断是否为Null
            isNotNull
        f.包含查询
            in              包含该内容的字段
            notIn           不包含该内容的字段
            inSql           包含该内容的字段
            notInSql        不包含该内容的字段
        g.存在查询
            exists          存在查询
            notExists       不存在查询
        h.分组查询
            groupBy         分组的字段
        i.聚合查询
            having          聚合的字段
        j.排序查询
            orderByAsc      升序
            orderByDesc     降序
            orderBy         多字段排序定义
        k.func查询
            略
        l.逻辑查询
            and             与
            or              或
            nested          非
        m.自定义条件查询
            apply           自定义查询条件
        n.last查询
            在sql语句的最末尾拼接“字符串”
    b.综合示例
        import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
        import com.example.demo.entity.User;

        public class QueryWrapperDemo {

            public void demo() {
                QueryWrapper<User> queryWrapper = new QueryWrapper<>();

                // 字段查询
                queryWrapper.select("name", "age");

                // 等值查询
                queryWrapper.eq("status", 1);
                queryWrapper.allEq(Map.of("name", "Alice", "age", 25));
                queryWrapper.ne("status", 0);

                // 范围查询
                queryWrapper.gt("age", 18);
                queryWrapper.ge("age", 18);
                queryWrapper.lt("age", 30);
                queryWrapper.le("age", 30);
                queryWrapper.between("age", 18, 30);
                queryWrapper.notBetween("age", 10, 15);

                // 模糊查询
                queryWrapper.like("name", "Alice");
                queryWrapper.notLike("name", "Bob");
                queryWrapper.likeLeft("name", "son");
                queryWrapper.likeRight("name", "Ali");

                // 判空查询
                queryWrapper.isNull("email");
                queryWrapper.isNotNull("phone");

                // 包含查询
                queryWrapper.in("id", List.of(1, 2, 3));
                queryWrapper.notIn("id", List.of(4, 5));
                queryWrapper.inSql("id", "SELECT id FROM user WHERE status = 1");
                queryWrapper.notInSql("id", "SELECT id FROM user WHERE status = 0");

                // 存在查询
                queryWrapper.exists("SELECT 1 FROM user WHERE status = 1");
                queryWrapper.notExists("SELECT 1 FROM user WHERE status = 0");

                // 分组查询
                queryWrapper.groupBy("department");

                // 聚合查询
                queryWrapper.having("COUNT(id) > 1");

                // 排序查询
                queryWrapper.orderByAsc("age");
                queryWrapper.orderByDesc("salary");
                queryWrapper.orderBy(true, true, "age", "salary");

                // 逻辑查询
                queryWrapper.and(wrapper -> wrapper.eq("status", 1).ne("age", 30));
                queryWrapper.or(wrapper -> wrapper.eq("status", 0).gt("age", 25));
                queryWrapper.nested(wrapper -> wrapper.eq("status", 1).ne("age", 30));

                // 自定义条件查询
                queryWrapper.apply("DATE_FORMAT(date_column, '%Y-%m-%d') = {0}", "2023-10-01");

                // last查询
                queryWrapper.last("LIMIT 1");

                // 使用 queryWrapper 执行查询
                List<User> users = userMapper.selectList(queryWrapper);
            }
        }

01.字段查询
    a.select:字段查询
        @SpringBootTest
        public class QueryTest03 {

            @Autowired
            private UserMapper userMapper;

            @Test
            void select() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.select(User::getId,User::getName);
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }

02.等值查询
    a.eq:条件筛选属性 = ?
        @SpringBootTest
        public class QueryTest {
            @Autowired
            private UserMapper userMapper;

            @Test
            void eq() {
                // 1.创建条件查询对象
                QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
                // 2. 设置查询条件,指定查询的字段和匹配的值
                QueryWrapper<User> eq = userQueryWrapper.eq("name", "Jone");

                // 3. 进行条件查询
                User user = userMapper.selectOne(eq);
                System.out.println(user);
            }
        }
        -----------------------------------------------------------------------------------------------------
        @SpringBootTest
        public class QueryTest {
            @Autowired
            private UserMapper userMapper;

            // 使用: LambdaQueryWrapper 进行引用查询,防止报错
            @Test
            void eq2() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                LambdaQueryWrapper<User> queryWrapper = lambdaQueryWrapper.eq(User::getName, "Jone");

                User user = userMapper.selectOne(queryWrapper);
                System.out.println(user);
            }
        }
        -----------------------------------------------------------------------------------------------------
        @SpringBootTest
        public class QueryTest {
            @Autowired
            private UserMapper userMapper;

            // 字段名为 不null的
            @Test
            void isNull2() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                String name = null;
                //String name = "Jone";

                //  public Children eq(boolean condition, R column, Object val) {
                lambdaQueryWrapper.eq(name != null, User::getName, name);
                //User user = userMapper.selectOne(lambdaQueryWrapper);
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);

            }
        }
    b.allEq:满足多个条件字段的值的筛选
        @SpringBootTest
        public class QueryTest {

            @Autowired
            private UserMapper userMapper;

            // and 满足多个条件
            @Test
            void allEql() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.eq(User::getName, "Tom");
                lambdaQueryWrapper.eq(User::getAge, 18);
                User user = userMapper.selectOne(lambdaQueryWrapper);
                System.out.println(user);
            }
        }
        -----------------------------------------------------------------------------------------------------
        @SpringBootTest
        public class QueryTest {

            @Autowired
            private UserMapper userMapper;

            // 属性值为 null 的查询,属性值不为 Null的不查询该字段
            @Test
            void allEq2() {

                HashMap<String, Object> hashMap = new HashMap<>();

                hashMap.put("name", "Tom");
                hashMap.put("age",18);
                //hashMap.put("age", null);  // 为null 的属性值,则会不查询

                QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
                //userQueryWrapper.allEq(hashMap,false); //  // 为null 的属性值,则会不查询
                userQueryWrapper.allEq(hashMap, true);  // 为null,(name = ? AND age IS NULL) 查询

                User user = userMapper.selectOne(userQueryWrapper);
                System.out.println(user);
            }
        }
    c.ne:不等于
        @SpringBootTest
        public class QueryTest {

            @Autowired
            private UserMapper userMapper;

            // 不等于数值的
            @Test
            void ne() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.ne(User::getName, "Tom");
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);

            }
        }

03.范围查询
    a.gt:>
        @SpringBootTest
        public class QueryTest {

            @Autowired
            private UserMapper userMapper;

            // > 查询
            @Test
            void gt() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                Integer age = 18;
                lambdaQueryWrapper.gt(User::getAge, age);
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }
    b.ge:>=
        @SpringBootTest
        public class QueryTest {

            @Autowired
            private UserMapper userMapper;

            // >=查询
            @Test
            void ge() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                Integer age = 18;
                lambdaQueryWrapper.ge(User::getAge, age);
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }
    c.It:<
        @SpringBootTest
        public class QueryTest {

            @Autowired
            private UserMapper userMapper;


            // <
            @Test
            void lt() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                Integer age = 20;
                lambdaQueryWrapper.lt(User::getAge, age);
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }
    d.le:<=
        @SpringBootTest
        public class QueryTest {

            @Autowired
            private UserMapper userMapper;

            // <=
            @Test
            void le() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                Integer age = 20;
                LambdaQueryWrapper<User> lambdaQueryWrapper1 = lambdaQueryWrapper.le(User::getAge, age);

                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }
    e.between
        @SpringBootTest
        public class QueryTest {

            @Autowired
            private UserMapper userMapper;

            // 范围之间
            @Test
            void between() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.between(User::getAge, 10, 20);
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);

            }
        }
    f.notBetween
        @SpringBootTest
        public class QueryTest {

            @Autowired
            private UserMapper userMapper;

            // 不在范围的:  (age NOT BETWEEN ? AND ?)
            @Test
            void notBetween() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.notBetween(User::getAge,10,18);
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }

04.模糊查询
    a.like:%值%
        @SpringBootTest
        public class QueryTest {

            @Autowired
            private UserMapper userMapper;

            // 模糊查询: %J%(String)
            @Test
            void like() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.like(User::getName,"J");
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }
    b.notLike:不满足%值%
        @SpringBootTest
        public class QueryTest {

            @Autowired
            private UserMapper userMapper;

            // 模糊查询: %J%(String)
            @Test
            void like() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.notLike(User::getName,"J");
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }
    c.likeLeft:%值
        @SpringBootTest
        public class QueryTest {

            @Autowired
            private UserMapper userMapper;

            // 模糊查询:  %e(String) 左边
            @Test
            void likeft() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.likeLeft(User::getName,"e");
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);

            }
        }
    d.likeRight:值%
        @SpringBootTest
        public class QueryTest {

            @Autowired
            private UserMapper userMapper;

            // 模糊查询:  %e(String) 左边
            @Test
            void likeft() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.likeRight(User::getName,"e");
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);

            }
        }

05.判空查询
    a.isNUI:判断是否为Null
        @SpringBootTest
        public class QueryTest02 {
            @Resource
           private UserMapper userMapper;

            // 判断是否为 null  WHERE (name IS NULL)
            @Test
            void isNUll3() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.isNull(User::getName);
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }
    b.isNotNull
        @SpringBootTest
        public class QueryTest02 {
            @Resource
            private UserMapper userMapper;

            //  WHERE (name IS NULL)
            @Test
            void isNotNull() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.isNotNull(User::getName);
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }

06.包含查询
    a.in:包含该内容的字段
        @SpringBootTest
        public class QueryTest02 {
            @Resource
            private UserMapper userMapper;

            // 字段 = 值 or 字段 = 值 ->in
            // r WHERE (age IN (?,?,?))
            @Test
            void in() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                ArrayList<Object> arrayList = new ArrayList<>();
                Collections.addAll(arrayList, 18, 20, 22);
                lambdaQueryWrapper.in(User::getAge, arrayList);
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);

            }
        }
    b.notIn:不包含该内容的字段
        @SpringBootTest
        public class QueryTest02 {
            @Resource
            private UserMapper userMapper;

            // 字段!=值 and 字段!=值 ->not in
            //  WHERE (age NOT IN (?,?,?))
            @Test
            void notIn() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                ArrayList<Integer> arrayList = new ArrayList<>();
                Collections.addAll(arrayList, 18, 20, 22);
                lambdaQueryWrapper.notIn(User::getAge, arrayList);
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }
    c.inSql:包含该内容的字段
        @SpringBootTest
        public class QueryTest02 {
            @Resource
            private UserMapper userMapper;

            // er WHERE (age IN (18,20,22))
            @Test
            void inSql() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.inSql(User::getAge, "18,20,22");
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }
    d.notInSql:不包含该内容的字段
        @SpringBootTest
        public class QueryTest02 {
            @Resource
            private UserMapper userMapper;

            // age NOT IN (18,20,22))
            @Test
            void notInsql() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.notInSql(User::getAge, "18,20,22");
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }

07.存在查询
    a.exists:存在查询
        @SpringBootTest
        public class QueryTest03 {

            @Autowired
            private UserMapper userMapper;

            // SELECT * from rainbowsea_user WHERE EXISTS (select id FROM rainbowsea_user WHERE age = 18);
            // SELECT * from rainbowsea_user WHERE EXISTS (select id FROM rainbowsea_user WHERE age = 10);

            @Test
            void exists() {
                // 1. 创建QueryWrapper对象
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                // 2. 构建查询条件
                //lambdaQueryWrapper.exists("select id FROM rainbowsea_user WHERE age = 18");
                lambdaQueryWrapper.exists("select id FROM rainbowsea_user WHERE age = 10");
                // 3.查询
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }
    b.notExists:不存在查询
        @SpringBootTest
        public class QueryTest03 {

            @Autowired
            private UserMapper userMapper;


            //  SELECT * from rainbowsea_user WHERE not EXISTS (select id FROM rainbowsea_user WHERE age = 10);
            @Test
            void notExists() {
                // 1. 创建QueryWrapper对象
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                // 2. 构建查询条件
                //lambdaQueryWrapper.exists("select id FROM rainbowsea_user WHERE age = 18");
                lambdaQueryWrapper.notExists("select id FROM rainbowsea_user WHERE age = 10");
                // 3.查询
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }

08.分组查询
    a.groupBy:分组的字段
        @SpringBootTest
        public class QueryTest03 {

            @Autowired
            private UserMapper userMapper;

            // select age,count(*) as field_count from rainbowsea_user group by age;
            // 分组查询
            @Test
            void groupBy() {
                QueryWrapper<User> queryWrapper = new QueryWrapper<>();
                // 分组字段
                queryWrapper.groupBy("age");
                // 查询字段
                queryWrapper.select("age,count(*) as field_count");
                List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
                System.out.println(maps);
            }
        }

09.聚合查询
    a.having:聚合的字段
        @SpringBootTest
        public class QueryTest03 {

            @Autowired
            private UserMapper userMapper;

            // select age,count(*) as field_count from rainbowsea_user group by age HAVING field_count >=2;
            // 聚合查询
            @Test
            void having() {
                QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
                // 查询字段
                userQueryWrapper.select("age,count(*) as field_count");
                // 聚合条件筛选
                userQueryWrapper.having("field_count = 1");
                List<Map<String, Object>> maps = userMapper.selectMaps(userQueryWrapper);
                System.out.println(maps);
            }
        }

10.排序查询
    a.orderByAsc:升序
        @SpringBootTest
        public class QueryTest03 {

            @Autowired
            private UserMapper userMapper;

            // 降序
            @Test
            void orderByAsc() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.orderByAsc(User::getAge, User::getId);
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }
    b.orderByDesc:降序
        @SpringBootTest
        public class QueryTest03 {

            @Autowired
            private UserMapper userMapper;

            // 升序
            @Test
            void orderByDesc() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.orderByDesc(User::getAge, User::getId);
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }
    c.orderBy:多字段排序定义
        @SpringBootTest
        public class QueryTest03 {

            @Autowired
            private UserMapper userMapper;

            @Test
            void orderBy() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                // 设置排序字段和排序的方式
                // 参数1:如果排序字段的值为null的时候,是否还要作为排序字段参与排序
                // 参数2:是否升序排序,
                // 参数3: 排序字段
                //lambdaQueryWrapper.orderBy(true, true, User::getAge);
                lambdaQueryWrapper.orderBy(false, true, User::getAge);
                lambdaQueryWrapper.orderBy(true, false, User::getId);
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }

11.func查询
    @SpringBootTest
    public class QueryTest03 {

        @Autowired
        private UserMapper userMapper;

        //   // 可能会根据不同的请情况选择拼接不同的查询条件
        @Test
        void func() {
            LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();

            // 可能会根据不同的请情况选择拼接不同的查询条件
          /*  lambdaQueryWrapper.func(new Consumer<LambdaQueryWrapper<User>>() {
                @Override
                public void accept(LambdaQueryWrapper<User> userLambdaQueryWrapper) {
                    //if (true) {
                    if (false) {
                        userLambdaQueryWrapper.eq(User::getId,1);
                    } else {
                        userLambdaQueryWrapper.ne(User::getId,1);
                    }
                }
            });*/

            // 使用lambad表达式
            lambdaQueryWrapper.func(userLambdaQueryWrapper -> {
                if (false) {
                    userLambdaQueryWrapper.eq(User::getId, 1);
                } else {
                    userLambdaQueryWrapper.ne(User::getId, 1);
                }
            });
            List<User> users = userMapper.selectList(lambdaQueryWrapper);
            System.out.println(users);
        }
    }

12.逻辑查询
    a.and:与
        @SpringBootTest
        public class QueryTest03 {

            @Autowired
            private UserMapper userMapper;

            //  WHERE (age > ? AND age < ?)
            @Test
            void and() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.gt(User::getAge, 22).lt(User::getAge, 30);
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }
        -----------------------------------------------------------------------------------------------------
        @SpringBootTest
        public class QueryTest03 {

            @Autowired
            private UserMapper userMapper;


            // WHERE (name = ? AND (age > ? OR age < ?))
            @Test
            void add2() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.eq(User::getName, "wang").and(i -> i.gt(User::getAge, 26).or().lt(User::getAge, 22));
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }
    b.or:或
        @SpringBootTest
        public class QueryTest03 {

            @Autowired
            private UserMapper userMapper;

            //  WHERE (age < ? AND age > ?)
            @Test
            void or() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.lt(User::getAge, 20).gt(User::getAge, 23);// age < 20 || age >=23
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }
    c.nested:非
        @SpringBootTest
        public class QueryTest03 {

            @Autowired
            private UserMapper userMapper;

            //  WHERE ((name = ? AND age <> ?))
            @Test
            void nested() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.nested(i->i.eq(User::getName,"Tom").ne(User::getAge,22));
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }

13.自定义条件查询
    a.apply:自定义查询条件
        @SpringBootTest
        public class QueryTest03 {

            @Autowired
            private UserMapper userMapper;

            // 自定义条件查询
            @Test
            void apply() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.apply("id = 1");
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }
        }

14.last查询
    a.在sql语句的最末尾拼接“字符串”
        @SpringBootTest
        public class QueryTest03 {

            @Autowired
            private UserMapper userMapper;
            // 最后添加字符拼接
            // SELECT id,name,age,email,`desc` FROM rainbowsea_user limit 0,2
            @Test
            void last() {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
                lambdaQueryWrapper.last("limit 0,2");
                List<User> users = userMapper.selectList(lambdaQueryWrapper);
                System.out.println(users);
            }

        }

3.9 [1]核心:UpdateWrapper

01.常用方法
    a.设置更新字段
        set(String column, Object val):设置要更新的字段和值
        setSql(String sql):设置更新的 SQL 片段
    b.条件构造
        eq(String column, Object val):等值条件
        ne(String column, Object val):不等值条件
        gt(String column, Object val):大于条件
        ge(String column, Object val):大于等于条件
        lt(String column, Object val):小于条件
        le(String column, Object val):小于等于条件
        between(String column, Object val1, Object val2):范围条件
        like(String column, Object val):模糊匹配条件
        isNull(String column):判断字段是否为 NULL
        isNotNull(String column):判断字段是否不为 NULL
        in(String column, Collection<?> coll):包含条件
        notIn(String column, Collection<?> coll):不包含条件
    c.逻辑条件
        and(Consumer<UpdateWrapper<T>> consumer):AND 逻辑条件
        or(Consumer<UpdateWrapper<T>> consumer):OR 逻辑条件
        nested(Consumer<UpdateWrapper<T>> consumer):嵌套条件
    d.自定义SQL
        apply(String sql, Object... args):自定义 SQL 片段
        last(String sql):在 SQL 语句末尾拼接字符串

02.示例代码
    a.代码
        import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
        import com.example.demo.entity.User;

        public class UpdateWrapperDemo {

            public void demo() {
                UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();

                // 设置更新字段
                updateWrapper.set("name", "Alice");
                updateWrapper.set("age", 30);
                updateWrapper.setSql("salary = salary + 1000");

                // 条件构造
                updateWrapper.eq("status", 1);
                updateWrapper.ne("department", "HR");
                updateWrapper.gt("experience", 5);
                updateWrapper.between("age", 25, 35);
                updateWrapper.like("name", "A%");
                updateWrapper.isNull("email");
                updateWrapper.in("id", List.of(1, 2, 3));

                // 逻辑条件
                updateWrapper.and(wrapper -> wrapper.eq("status", 1).ne("age", 30));
                updateWrapper.or(wrapper -> wrapper.eq("status", 0).gt("age", 25));
                updateWrapper.nested(wrapper -> wrapper.eq("status", 1).ne("age", 30));

                // 自定义 SQL
                updateWrapper.apply("DATE_FORMAT(date_column, '%Y-%m-%d') = {0}", "2023-10-01");
                updateWrapper.last("LIMIT 1");

                // 使用 updateWrapper 执行更新
                int rows = userMapper.update(null, updateWrapper);
                System.out.println("Updated rows: " + rows);
            }
        }
    b.说明
        设置更新字段:使用 set 和 setSql 方法指定要更新的字段和值。
        条件构造:使用各种条件方法构建更新条件
        逻辑条件:使用 and, or, nested 方法构建复杂的逻辑条件
        自定义 SQL:使用 apply 和 last 方法添加自定义 SQL 片段

3.10 [1]核心:LambdaQueryWrapper

01.概述
    a.定义
        LambdaQueryWrapper 是 MyBatis-Plus 提供的一个用于构建查询条件的工具类
        它允许开发者以 Lambda 表达式的方式构建查询条件,避免了字符串拼接带来的错误和不便,提高了代码的可读性和安全性
    b.原理
        LambdaQueryWrapper 的核心原理是利用 Java 的反射机制和 Lambda 表达式
        自动获取实体类的属性名,从而构建 SQL 查询条件
        通过 Lambda 表达式,开发者可以直接引用实体类的属性,避免了硬编码的字符串,减少了出错的可能性
    c.常用API
        eq()            等于
        ne()            不等于
        gt()            大于
        lt()            小于
        ge()            大于等于
        le()            小于等于
        like()          模糊查询
        between()       范围查询
        orderByAsc()    升序排序
        orderByDesc()   降序排序
        last()          添加 SQL 片段

02.使用步骤
    a.引入依赖
        确保项目中已经引入了 MyBatis-Plus 的依赖
    b.创建实体类
        定义与数据库表对应的实体类
    c.创建 Mapper 接口
        创建对应的 Mapper 接口,继承 BaseMapper
    d.使用 LambdaQueryWrapper
        在服务层或控制器中使用 LambdaQueryWrapper 构建查询条件

03.代码示例
    a.示例1: 查询年龄大于 18 的用户
        import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.stereotype.Service;

        import java.util.List;

        @Service
        public class UserService {

            @Autowired
            private UserMapper userMapper;

            public List<User> getUsersAbove18() {
                LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
                queryWrapper.gt(User::getAge, 18);
                return userMapper.selectList(queryWrapper);
            }
        }
    b.示例2: 查询名字包含 "张" 的用户
        public List<User> getUsersWithNameLike(String name) {
            LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.like(User::getName, name);
            return userMapper.selectList(queryWrapper);
        }
    c.示例3: 查询年龄在 20 到 30 之间的用户
        public List<User> getUsersBetween20And30() {
            LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.between(User::getAge, 20, 30);
            return userMapper.selectList(queryWrapper);
        }
    d.示例4: 按年龄升序排序
        public List<User> getAllUsersOrderedByAge() {
            LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.orderByAsc(User::getAge);
            return userMapper.selectList(queryWrapper);
        }
    e.示例5: 添加 SQL 片段
        public List<User> getUsersWithCustomSQL() {
            LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.last("LIMIT 10"); // 添加 SQL 片段
            return userMapper.selectList(queryWrapper);
        }

3.11 [1]核心:QueryWrapper/UpdateWrapper

00.QueryWrapper、UpdateWrapper
    QueryWrapper:查询构造器
    UpdateWrapper:更新构造器

01.QueryWrapper
    a.定义
        用于构建查询条件
    b.方法
        eq(String column, Object val): 等于
        ne(String column, Object val): 不等于
        gt(String column, Object val): 大于
        ge(String column, Object val): 大于等于
        lt(String column, Object val): 小于
        le(String column, Object val): 小于等于
        like(String column, Object val): 模糊匹配
    c.示例
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", "Tom").ge("age", 18);
        List<User> users = userMapper.selectList(queryWrapper);

02.UpdateWrapper
    a.定义
        用于构建更新条件
    b.方法
        set(String column, Object val): 设置更新字段
        eq(String column, Object val): 等于(作为更新条件)
        ne(String column, Object val): 不等于(作为更新条件)
    c.示例
        UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
        updateWrapper.eq("name", "Tom").set("age", 20);
        userMapper.update(null, updateWrapper);

3.12 [2]分页:3种

00.汇总
    1.逻辑分页(内存分页):在 Java 应用层面进行数据截取,代表为 RowBounds
    2.物理分页(数据库分页):在数据库层面通过 SQL 语句(如 LIMIT)实现分页
    3.插件化物理分页:通过插件自动实现物理分页,是目前最主流的方式,代表为 PageHeper 和 MyBatis-Plus 自带的分页插件

01.逻辑分页(内存分页):RowBounds
    a.实现原理
        `RowBounds` 是 MyBatis 提供的内存分页工具。
        执行完整的 SQL 查询,将结果集全部加载到 JVM 内存中。
        在内存中,根据 `RowBounds` 的 `offset` 和 `limit` 截取所需页数据。
    b.优缺点分析
        a.优点
            代码简单:无需修改 Mapper XML 或 SQL 语句。
            开发快速:适合快速原型开发或小数据量场景。
        b.缺点
            性能极差:总是查询全部数据,浪费数据库和网络带宽。
            内存溢出风险:大数据量时易导致内存溢出(OOM)。
    c.示例代码
        int offset = 10;
        int limit = 10;
        RowBounds rowBounds = new RowBounds(offset, limit);
        List<User> users = sqlSession.selectList("com.example.mapper.UserMapper.getAllUsers", null, rowBounds);
    d.适用场景与建议
        严禁在生产环境的大数据量业务中使用 `RowBounds`。
        仅适用于数据量极小且可预见的表、内部管理后台的非核心功能、临时开发或调试用途。

03.物理分页(数据库分页):原生 SQL
    a.实现原理
        利用数据库支持的分页语法,在 SQL 查询时只获取目标页数据。
        不同数据库的分页语法不同,如 MySQL 的 `LIMIT`,Oracle 的 `ROWNUM`。
    b.优缺点分析
        a.优点
            高性能:查询精确,数据库压力小,网络传输高效。
            低内存占用:应用层内存占用极低。
            数据实时性好:直接从数据库查询,保证数据最新。
        b.缺点
            需要手写 SQL:增加开发工作量。
            数据库不通用:不同数据库的分页语法不同。
            深分页性能问题:`OFFSET` 值大时,查询变慢。
    c.示例代码
        <select id="findUsersByPage" resultType="com.example.model.User">
            SELECT *
            FROM users
            ORDER BY id
            LIMIT #{offset}, #{pageSize}
        </select>
        List<User> findUsersByPage(@Param("offset") int offset, @Param("pageSize") int pageSize);
    d.适用场景与建议
        物理分页是所有面向用户、数据量大的生产环境场景的首选方案。

04.插件化物理分页
    a.PageHelper 插件
        a.核心原理
            `PageHelper` 通过 `ThreadLocal` 机制将分页参数与当前线程绑定。
            使用 MyBatis 拦截器,在 SQL 执行前自动重写 SQL 语句,添加物理分页逻辑。
        b.集成与使用
            a.添加依赖
                <dependency>
                    <groupId>com.github.pagehelper</groupId>
                    <artifactId>pagehelper-spring-boot-starter</artifactId>
                    <version>1.4.6</version>
                </dependency>
            b.使用方法
                PageHelper.startPage(2, 10);
                List<User> users = userMapper.selectAll();
                PageInfo<User> pageInfo = new PageInfo<>(users);
        c.核心优势与注意事项
            a.优势
                非侵入式:无需修改任何 Mapper XML。
                使用简单:只需一行代码即可开启分页。
                功能强大:自动执行 `COUNT` 查询获取总数。
            b.注意事项
                `PageHelper.startPage()` 仅对紧跟其后的第一条 `select` 语句有效。
                注意插件版本与 MyBatis、Spring Boot 版本的兼容性。
    b.MyBatis-Plus 自带分页插件
        a.核心原理与配置
            a.说明
                通过拦截器实现的物理分页。
                配置 `MybatisPlusInterceptor` 并添加 `PaginationInnerInterceptor`。
            b.代码
                @Configuration
                public class MybatisPlusConfig {
                    @Bean
                    public MybatisPlusInterceptor mybatisPlusInterceptor() {
                        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
                        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
                        return interceptor;
                    }
                }
        b.集成与使用
            a.定义 Mapper 方法
                IPage<User> selectUserPage(IPage<User> page, @Param("name") String name);
            b.调用方法
                IPage<User> page = new Page<>(1, 10);
                IPage<User> pageResult = userMapper.selectUserPage(page, "John");
        c.核心优势
            集成度高:与 `MyBatis-Plus` 的 `Wrapper` 条件构造器无缝集成。
            面向对象:分页参数和结果都封装在 `IPage` 对象中。
            自动适配:配置时指定数据库类型后,能自动生成对应的分页 SQL。

3.13 [2]分页:4种

00.汇总
    1.使用selectPage
    2.使用2种分页查询的写法
    3.使用PageHelper插件分页查询
    4.手动分页

00.准备
    a.表
        CREATE TABLE `school_student` (
          `id` int(11) NOT NULL AUTO_INCREMENT,
          `name` varchar(255) DEFAULT NULL,
          `sex` varchar(255) DEFAULT NULL,
          `age` int(11) DEFAULT NULL,
          PRIMARY KEY (`id`)
        ) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;

        INSERT INTO `avlicy`.`school_student`(`id`, `name`, `sex`, `age`) VALUES (1, 'av峰峰', '男', 1);
        INSERT INTO `avlicy`.`school_student`(`id`, `name`, `sex`, `age`) VALUES (2, '卢本伟', '男', 12);
        INSERT INTO `avlicy`.`school_student`(`id`, `name`, `sex`, `age`) VALUES (3, '小米粥', '女', 13);
        INSERT INTO `avlicy`.`school_student`(`id`, `name`, `sex`, `age`) VALUES (4, '黄米粥', '女', 15);
        INSERT INTO `avlicy`.`school_student`(`id`, `name`, `sex`, `age`) VALUES (5, '蓝米粥', '女', 11);
        INSERT INTO `avlicy`.`school_student`(`id`, `name`, `sex`, `age`) VALUES (6, '白米粥', '女', 17);
        INSERT INTO `avlicy`.`school_student`(`id`, `name`, `sex`, `age`) VALUES (7, '红米粥', '女', 15);
        INSERT INTO `avlicy`.`school_student`(`id`, `name`, `sex`, `age`) VALUES (8, '橙米粥', '女', 16);
        INSERT INTO `avlicy`.`school_student`(`id`, `name`, `sex`, `age`) VALUES (9, '青米粥', '女', 13);
        INSERT INTO `avlicy`.`school_student`(`id`, `name`, `sex`, `age`) VALUES (10, '紫米粥', '女', 12);
    b.配置类
        @Configuration
        //@MapperScan("com.example.demo.mapper")
        public class MybatisPlusConfig {

            /**
             * 新增分页拦截器,并设置数据库类型为mysql
             * @return
             */
            @Bean
            public MybatisPlusInterceptor mybatisPlusInterceptor() {
                MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
                interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
                return interceptor;
            }
        }

01.使用selectPage
    a.Service
        //分页参数
        Page<SchoolStudent> rowPage = new Page(page, pageSize);

        //queryWrapper组装查询where条件
        LambdaQueryWrapper<SchoolStudent> queryWrapper = new LambdaQueryWrapper<>();
        rowPage = this.baseMapper.selectPage(rowPage, queryWrapper);
        return rowPage;
    b.结果
        略

02.使用2种分页查询的写法
    a.xml
        <select id="getPageStudentTwo" resultType="com.example.demo.entity.base.SchoolStudent">
            select * from school_student
        </select>
    b.Mapper
        1.mybatis-plus中分页接口需要包含一个IPage类型的参数
        2.多个实体参数,需要添加@Param参数注解,方便在xml中配置sql时获取参数值
        注意这里我虽然加了@Param但是我并没有使用
        Page<SchoolStudent> getPageStudentTwo(Page<SchoolStudent> rowPage,@Param("schoolStudent") SchoolStudent schoolStudent);
    c.第1种写法
        @Override
        public IPage<SchoolStudent> getPageStudentTwo(Integer current, Integer size) {
            Page<SchoolStudent> rowPage = new Page(current, size);
            SchoolStudent schoolStudent = new SchoolStudent();
            rowPage = this.baseMapper.getPageStudentTwo(rowPage, schoolStudent);
            return rowPage;
        }
    d.第2种写法
        @Override
        public IPage<SchoolStudent> getPageStudentThree(Integer current, Integer size) {
            SchoolStudent schoolStudent = new SchoolStudent();
            Page pageStudentTwo = this.baseMapper.getPageStudentTwo(new Page(current, size), schoolStudent);
            return pageStudentTwo;
        }

03.使用PageHelper插件分页查询
    a.依赖
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.4.5</version>
        </dependency>
    b.代码
        @Override
        public PageInfo<SchoolStudent> getPageStudentFour(Integer current, Integer size) {
            //获取第1页,10条内容,默认查询总数count
            PageHelper.startPage(current, size);
            List<SchoolStudent> list = this.list();
            //用PageInfo对结果进行包装
            PageInfo page = new PageInfo(list);
            return page;
        }
    c.结果
        这是控制台打印的查询语句,大家发现最后的LIMIT 函数没
        正常来说mybatis-plus里是没有写的,是pagehelper加上去
        -----------------------------------------------------------------------------------------------------
        PageHelper.startPage(pageNum, pageSize)这个地方设置的两个值
        pagehelper会在你执行查询语句的时候帮你加上去,也就是LIMIT 的两个参数
        第一个参数是LIMIT 的起始下标,pagehelper会根据pageNum和pageSize自动给你算出
        第二个参数是LIMIT的 数据量,也就是pageSize
        -----------------------------------------------------------------------------------------------------
        而且我发现,pagehelper会执行两遍你写的查询语句
        第一遍会进行count(0),查出总条数
        第二遍就会利用你设置的参数帮你分页查询出pageSize条数据
        -----------------------------------------------------------------------------------------------------
        我之前想先进行树排序后再进行分页的想法,在使用pagehelper时是行不通的,因为会影响pagehelper的自动分页
        因此我得出在进行pagehelper分页的时候不可以给查询出的数据进行其他排序操作(查询语句中写order by是可以的)
        这可能就是pagehelper的局限之处

04.手动分页
    a.方法
        import java.util.List;
        import java.util.Map;
        import java.util.stream.Collectors;

        public class PaginationUtil {

            public static List<Map<String, Object>> getPaginatedList(List<Map<String, Object>> list, int currentPage, int pageSize) {
                // 计算总记录数
                int totalRecords = list.size();

                // 计算开始索引和结束索引
                int fromIndex = (currentPage - 1) * pageSize;
                int toIndex = Math.min(fromIndex + pageSize, totalRecords);

                // 检查索引范围
                if (fromIndex >= totalRecords || fromIndex < 0) {
                    return List.of(); // 返回空列表
                }

                // 截取分页数据
                return list.subList(fromIndex, toIndex);
            }
        }
    b.调用
        List<Map<String, Object>> originalList = // 初始化你的数据列表
        int currentPage = 1; // 当前页码
        int pageSize = 10; // 每页大小
        List<Map<String, Object>> paginatedList = PaginationUtil.getPaginatedList(originalList, currentPage, pageSize);

3.14 [2]分页:说明

00.常见误区
    每次查询全部再截取数据:内存分页,效率极低,除非数据量极小,否则不要用
    让数据库只查你需要的那一页数据:物理分页,正确做法

01.物理分页(推荐,效率高)
    a.原理
        物理分页是指在 SQL 层面直接通过数据库的分页语法(如 MySQL 的 LIMIT、Oracle 的 ROWNUM、SQL Server 的 OFFSET/FETCH 等)来限制返回的数据条数。
        只查询当前页需要的数据,数据库只返回部分数据,效率高,适合大数据量。
    b.示例(MySQL)
        SELECT * FROM user LIMIT 20, 10;
        -- 表示从第21条开始,取10条数据(第1页是0,10;第2页是10,10;第3页是20,10)
    c.MyBatis 配置方式
        可以直接在 XML 里写分页 SQL。
        也可以用 MyBatis-Plus、PageHelper 等插件自动拼接分页 SQL
    d.PageHelper 示例
        // PageHelper 会自动拦截 SQL,在 SQL 末尾加上 LIMIT。
        PageHelper.startPage(pageNum, pageSize);
        List<User> users = userMapper.selectAll();
        PageInfo<User> pageInfo = new PageInfo<>(users);

02.内存分页(不推荐,效率低)
    a.原理
        先查询出所有数据(不分页),然后在 Java 内存中对 List 进行截取(如 subList)。
        适合数据量很小的场景,大数据量会导致内存溢出和性能问题。
    b.示例
        List<User> allUsers = userMapper.selectAll();
        List<User> pageList = allUsers.subList(startIndex, endIndex);
    c.说明
        这种方式会把所有数据都查出来,浪费内存和带宽。

03.MyBatis 分页实现原理
    a.手动分页
        你可以在 Mapper 的 SQL 里手动写 LIMIT,并传入 offset 和 size 参数。
    b.插件自动分页(如 PageHelper)
        PageHelper 通过 MyBatis 的拦截器机制,自动在 SQL 末尾加上分页语句。
        你只需要写普通的查询 SQL,插件会自动帮你分页。
    c.MyBatis-Plus 分页
        MyBatis-Plus 也有自己的分页插件,原理类似。

04.总结
    物理分页(SQL 层面分页)是主流做法,效率高,推荐使用。
    内存分页(Java 层面分页)只适合小数据量,实际开发中不推荐。
    MyBatis 本身不自带分页功能,通常配合分页插件(如 PageHelper、MyBatis-Plus)实现自动分页。

3.15 [2]分页:拦截器

00.MyBatisPlus分页插件如何工作
    a.回答
        PaginationInnerInterceptor
    b.说明
        MyBatisPlus的分页插件通过在配置中添加【PaginationInnerInterceptor】来实现自动分页
        使用时,只需创建Page对象并传入查询方法中,插件会自动处理SQL查询的分页逻辑
        分页查询返回的结果包含记录列表和总记录数,简化了分页处理的复杂性

01.使用
    a.定义
        MyBatisPlus提供的分页插件用于简化数据库查询结果的分页处理
    b.引入依赖
        确保在 pom.xml 中引入 MyBatis Plus 的依赖。
    c.配置插件
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
            return interceptor;
        }
    d.分页查询
        Page<User> page = new Page<>(1, 10);                      // 第1页,每页10条
        IPage<User> userPage = userMapper.selectPage(page, null); // 传入分页对象和查询条件
        List<User> users = userPage.getRecords();                 // 获取记录列表
        long total = userPage.getTotal();                         // 获取总记录数

3.16 [2]分页:流式查询

00.汇总
    a.非流式查询 vs 流式查询
        非流式查询:内存使用随记录增长而增长
        流式查询:内存使用保持稳定,取决于批处理大小
    b.深分页问题
        MySQL 在处理大 OFFSET 时效率低下
        解决方案:限制返回总页数,或对超过阈值的页数进行 SQL 改写
    c.总结
        常规查询适用于小数据量或内存充足的场景
        流式查询和游标查询适用于大数据量处理,能够有效降低内存消耗
        在使用 MyBatis-Plus 时,分页查询会发送两条 SQL,一条用于查询数据,一条用于查询总数

01.常规查询
    a.特点
        一次性将所有数据加载到内存中。简单易用,但可能导致内存溢出,尤其是在处理大数据量时
    b.示例
        @Mapper
        public interface BigDataSearchMapper extends BaseMapper<BigDataSearchEntity> {
            @Select("SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment}")
            Page<BigDataSearchEntity> pageList(@Param("page") Page<BigDataSearchEntity> page, @Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper);
        }
    c.注意
        在不考虑深分页优化的情况下,可能导致数据库性能问题

02.流式查询
    a.特点
        查询结果以迭代器的形式返回,逐条处理数据。降低内存使用,但需要保持数据库连接打开
    b.MyBatis 流式查询接口
        Cursor 接口:可关闭、可遍历。
        方法:isOpen(), isConsumed(), getCurrentIndex()
    c.使用场景
        适用于需要处理大结果集但内存有限的场景

03.游标查询
    a.特点
        控制每次从数据库读取的数据量,逐批处理。节省内存消耗,适合处理百万级数据
    b.示例
        @Mapper
        public interface BigDataSearchMapper extends BaseMapper<BigDataSearchEntity> {
            @Select("SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment}")
            @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 100000)
            Page<BigDataSearchEntity> pageList(@Param("page") Page<BigDataSearchEntity> page, @Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper);

            @Select("SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment}")
            @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 100000)
            @ResultType(BigDataSearchEntity.class)
            void listData(@Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper, ResultHandler<BigDataSearchEntity> handler);
        }
    c.注意
        ResultSet.FORWARD_ONLY:游标只能向下滚动
        fetchSize:控制每次获取的数据量
        ResultType:指定返回实体类型

3.17 [2]分页:深分页问题

01.定义
    深分页问题,通常是指在分页查询中,OFFSET 值很大时,查询性能会显著下降

02.工作流程
    a.普通分页
        select * from table order by id limit m,n;
    b.先查出主键,通过主键关联,支持跨页,减少回表
        select * from table as a
        inner join (select id from table order by id limit m, n) as b
        on a.id = b.id order by a.id;
    c.取上一页最大主键做判断,不支持跨页插叙
        select * from table where id > #max_id# order by id limit 10, 10;
    d.先查出主键,通过主键做范围判断,支持跨页,减少回表。
        select * from table where id > (select id from table order by id limit m, 1) limit n;

03.解决方案
    a.使用索引
        确保分页字段上有适当的索引,以提高查询性能
    b.基于主键或索引的分页
        使用主键或其他唯一索引进行分页,而不是使用 OFFSET。这种方法通常称为“基于游标的分页”或“Keyset Pagination”
        -----------------------------------------------------------------------------------------------------
        public IPage<User> getUserPageById(int pageSize, Long lastId) {
            Page<User> page = new Page<>(1, pageSize);
            QueryWrapper<User> queryWrapper = new QueryWrapper<>();
            if (lastId != null) {
                queryWrapper.gt("id", lastId);
            }
            queryWrapper.orderByAsc("id");
            return userMapper.selectPage(page, queryWrapper);
        }
        -----------------------------------------------------------------------------------------------------
        在这种方法中,你需要传递最后一条记录的 ID(或其他唯一标识符)来获取下一页的数据
    c.限制最大页数
        限制用户可以请求的最大页数,避免过大的 OFFSET
    d.缓存结果
        对于特定的查询结果,使用缓存来减少数据库的负载

3.18 [2]分页,默认orderby

00.汇总
    a.问题
        产品大哥:大哥你这查询列表有问题啊,每次点一下查询,返回的数据不一样呢
        我:FKY 之前不是说好的吗,加了排序查询很卡,就取消了
        技术经理:卡主要是因为分页查询加了排序之后,mybatisPlus 生成的 count 也会有Order by就很慢,自己实现一个count 就行了
        我:分页插件在执行统计操作的时候,一般都会对Sql 简单的优化,会去掉排序的
    b.说明
        分页查询加了排序之后,mybatisPlus 生成的 count 也会有Order by,自己实现一个count 就行了
    c.如何关闭 ORDER BY
        如果你在某次查询中不希望使用 ORDER BY,可以通过以下方式确保查询中没有排序条件:
        1.检查查询条件:确保在 QueryWrapper 或自定义 SQL 中没有指定 ORDER BY
        2.使用自定义 SQL:如果使用自定义 SQL,确保 SQL 语句中没有 ORDER BY 子句

01.mybatisPlus分页插件count运行原理
    a.说明
        分页插件都是基于MyBatis 的拦截器接口Interceptor实现
    b.count SQL从获取到执行的主要流程
        a.确认count sql MappedStatement 对象
            先查询Page对象中 是否有countId(countId 为mapper sql id)
            有的话就用自定义的count sql,没有的话就自己通过查询语句构建一个count MappedStatement
        b.优化count sql
            得到countMs构建成功之后对count SQL进行优化,最后 执行count SQL,将结果 set 到page对象中
        c.源码
            public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
                IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);
                if (page == null || page.getSize() < 0 || !page.searchCount()) {
                    return true;
                }

                BoundSql countSql;
                // -------------------------------- 根据“countId”获取自定义的count MappedStatement
                MappedStatement countMs = buildCountMappedStatement(ms, page.countId());
                if (countMs != null) {
                    countSql = countMs.getBoundSql(parameter);
                } else {
                    //-------------------------------------------根据查询ms 构建统计SQL的MS
                    countMs = buildAutoCountMappedStatement(ms);
                    //-------------------------------------------优化count SQL
                    String countSqlStr = autoCountSql(page, boundSql.getSql());
                    PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
                    countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), parameter);
                    PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters());
                }

                CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql);
                //----------------------------------------------- 统计SQL
                List<Object> result = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql);
                long total = 0;
                if (CollectionUtils.isNotEmpty(result)) {
                    // 个别数据库 count 没数据不会返回 0
                    Object o = result.get(0);
                    if (o != null) {
                        total = Long.parseLong(o.toString());
                    }
                }
                // ---------------------------------------set count ret
                page.setTotal(total);
                return continuePage(page);
            }
    c.count SQL优化逻辑
        a.主要优化的是以下两点
            1.去除 SQl 中的order by
            2.去除 left join
        b.哪些情况count优化限制
            1.SQL 中 有 这些集合操作的 INTERSECT,EXCEPT,MINUS,UNION 直接不优化count
            2.包含groupBy 不去除orderBy
            3.order by 里带参数,不去除order by
            4.查看select 字段中是否动态条件,如果有条件字段,则不会优化 Count SQL
            5.包含 distinct、groupBy不优化
            6.如果 left join 是子查询,并且子查询里包含 ?(代表有入参) 或者 where 条件里包含使用 join 的表的字段作条件,就不移除 join
            7.如果 where 条件里包含使用 join 的表的字段作条件,就不移除 join
            8.如果 join 里包含 ?(代表有入参) 就不移除 join
        c.源码
            /**
             * 获取自动优化的 countSql
             *
             * @param page 参数
             * @param sql  sql
             * @return countSql
             */
            protected String autoCountSql(IPage<?> page, String sql) {
                if (!page.optimizeCountSql()) {
                    return lowLevelCountSql(sql);
                }
                try {
                    Select select = (Select) CCJSqlParserUtil.parse(sql);
                    SelectBody selectBody = select.getSelectBody();
                    // https://github.com/baomidou/mybatis-plus/issues/3920  分页增加union语法支持
                    //----------- SQL 中 有 这些集合操作的 INTERSECT,EXCEPT,MINUS,UNION 直接不优化count

                    if (selectBody instanceof SetOperationList) {
                        // ----lowLevelCountSql 具体实现: String.format("SELECT COUNT(*) FROM (%s) TOTAL", originalSql)
                        return lowLevelCountSql(sql);
                    }
                    ....................省略.....................
                    if (CollectionUtils.isNotEmpty(orderBy)) {
                        boolean canClean = true;
                        if (groupBy != null) {
                            // 包含groupBy 不去除orderBy
                            canClean = false;
                        }
                        if (canClean) {
                            for (OrderByElement order : orderBy) {
                                //-------------- order by 里带参数,不去除order by
                                Expression expression = order.getExpression();
                                if (!(expression instanceof Column) && expression.toString().contains(StringPool.QUESTION_MARK)) {
                                    canClean = false;
                                    break;
                                }
                            }
                        }
                        //-------- 清除order by
                        if (canClean) {
                            plainSelect.setOrderByElements(null);
                        }
                    }
                    //#95 Github, selectItems contains #{} ${}, which will be translated to ?, and it may be in a function: power(#{myInt},2)
                    // ----- 查看select 字段中是否动态条件,如果有条件字段,则不会优化 Count SQL
                    for (SelectItem item : plainSelect.getSelectItems()) {
                        if (item.toString().contains(StringPool.QUESTION_MARK)) {
                            return lowLevelCountSql(select.toString());
                        }
                    }
                    // ---------------包含 distinct、groupBy不优化
                    if (distinct != null || null != groupBy) {
                        return lowLevelCountSql(select.toString());
                    }
                    // ------------包含 join 连表,进行判断是否移除 join 连表
                    if (optimizeJoin && page.optimizeJoinOfCountSql()) {
                        List<Join> joins = plainSelect.getJoins();
                        if (CollectionUtils.isNotEmpty(joins)) {
                            boolean canRemoveJoin = true;
                            String whereS = Optional.ofNullable(plainSelect.getWhere()).map(Expression::toString).orElse(StringPool.EMPTY);
                            // 不区分大小写
                            whereS = whereS.toLowerCase();
                            for (Join join : joins) {
                                if (!join.isLeft()) {
                                    canRemoveJoin = false;
                                    break;
                                }
                                 .........................省略..............
                                } else if (rightItem instanceof SubSelect) {
                                    SubSelect subSelect = (SubSelect) rightItem;
                                    /* ---------如果 left join 是子查询,并且子查询里包含 ?(代表有入参) 或者 where 条件里包含使用 join 的表的字段作条件,就不移除 join */
                                    if (subSelect.toString().contains(StringPool.QUESTION_MARK)) {
                                        canRemoveJoin = false;
                                        break;
                                    }
                                    str = subSelect.getAlias().getName() + StringPool.DOT;
                                }
                                // 不区分大小写
                                str = str.toLowerCase();
                                if (whereS.contains(str)) {
                                    /*--------------- 如果 where 条件里包含使用 join 的表的字段作条件,就不移除 join */
                                    canRemoveJoin = false;
                                    break;
                                }

                                for (Expression expression : join.getOnExpressions()) {
                                    if (expression.toString().contains(StringPool.QUESTION_MARK)) {
                                        /* 如果 join 里包含 ?(代表有入参) 就不移除 join */
                                        canRemoveJoin = false;
                                        break;
                                    }
                                }
                            }
                            // ------------------ 移除join
                            if (canRemoveJoin) {
                                plainSelect.setJoins(null);
                            }
                        }
                    }
                    // 优化 SQL-------------
                    plainSelect.setSelectItems(COUNT_SELECT_ITEM);
                    return select.toString();
                } catch (JSQLParserException e) {
                  ..............
                }
                return lowLevelCountSql(sql);
            }

02.order by运行原理
    a.说明
        order by 排序,具体怎么排取决于优化器的选择
        如果优化器认为走索引更快,那么就会用索引排序
        否则,就会使用filesort (执行计划中extra中提示:using filesort)
        但是能走索引排序的情况并不多,并且确定性也没有那么强,很多时候,还是走的filesort
    b.索引排序
        索引排序,效率是最高的,就算order by 后面的字段是 索引列,也不一定就是通过索引排序
        这个过程是否一定用索引,完全取决于优化器的选择
    c.filesort排序
        a.说明
            如果不能走索引排序, MySQL 会执行filesort操作以读取表中的行并对它们进行排序
            在进行排序时,MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer,它的大小是由sort_buffer_size控制的
        b.sort_buffer_size的大小不同,会在不同的地方进行排序操作
            如果要排序的数据量小于 sort_buffer_size,那么排序就在内存中完成
            如果排序数据量大于sort_buffer_size,则需要利用磁盘临时文件辅助排序
        c.filesort排序具体实现方式
            a.说明
                FileSort是MySQL中用于对数据进行排序的一种机制
            b.主要有以下几种实现方式
                a.全字段排序
                    a.原理
                        将查询所需的所有字段,包括用于排序的字段以及其他SELECT列表中的字段
                        都读取到排序缓冲区中进行排序。这样可以在排序的同时获取到完整的行数据,减少访问原表数据的次数
                    b.适用场景
                        当排序字段和查询返回字段较少,并且排序缓冲区能够容纳这些数据时,全字段排序效率较高
                b.行指针排序
                    a.原理
                        只将排序字段和行指针(指向原表中数据行的指针)读取到排序缓冲区中进行排序
                        排序完成后,再根据行指针回表读取所需的其他字段数据
                    b.适用场景
                        当查询返回的字段较多,而排序缓冲区无法容纳全字段数据时
                        行指针排序可以减少排序缓冲区的占用,提高排序效率。但由于需要回表操作,可能会增加一定的I/O开销
                c.多趟排序
                    a.原理
                        如果数据量非常大,即使采用行指针排序,排序缓冲区也无法一次容纳所有数据
                        MySQL会将数据分成多个较小的部分,分别在排序缓冲区中进行排序
                        生成多个有序的临时文件。然后再将这些临时文件进行多路归并,最终得到完整的有序结果
                    b.适用场景
                        适用于处理超大数据量的排序操作,能够在有限的内存资源下完成排序任务
                        但会产生较多的磁盘I/O操作,性能相对较低
                c.优先队列排序
                    a.原理
                        结合优先队列数据结构进行排序。对于带有LIMIT子句的查询
                        MySQL会创建一个大小为LIMIT值的优先队列
                        在读取数据时,将数据放入优先队列中,根据排序条件进行比较和调整
                        当读取完所有数据或达到一定条件后,优先队列中的数据就是满足LIMIT条件的有序结果
                    b.适用场景
                        特别适用于需要获取少量排序后数据的情况,如查询排名前几的数据
                        可以避免对大量数据进行全量排序,提高查询效率

03.limit运行原理
    a.说明
        limit执行过程 对于 SQL 查询中 LIMIT 的使用,像 LIMIT 10000, 100 这种形式,执行顺序如下说明
    b.MySQL的执行顺序
        1.从数据表中读取所有符合条件的数据(包括排序和过滤)
        2.将数据按照 ORDER BY 排序
        3.根据 LIMIT 参数选择返回的记录:跳过前 10000 行数据(这个过程是通过丢弃数据来实现的)、然后返回接下来的 100 行数据
    c.总结
        LIMIT 是先检索所有符合条件的数据,然后丢弃掉前面的行,再返回指定的行数
        这解释了为什么如果数据集很大,LIMIT 会带来性能上的一些问题
        尤其是在有很大的偏移量(比如 LIMIT 10000, 100)时

3.19 [2]插件:数据权限

00.汇总
    a.说明
        MyBatisPlus底层基于JSQLParser来做解析
    b.示例
        普通的销售人员只能看到自己的数据
        部门的销售经理能看到当前部门的所有数据
        销售总监可以看到所有的销售数据

01.自定义注解@DataPermission,用于标识需要自动添加数据权限过滤的方法
    /**
     * 数据权限注解
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DataPermission {

    }

02.数据权限注解切面
    /**
     * 数据权限注解切面
     */
    @Aspect
    @Component
    public class DataPermissionAspect {

        /**
         * 环绕通知,拦截带有 @DataPermission 注解的方法
         * @param joinPoint
         * @return
         * @throws Throwable
         */
        @Around("@annotation(DataPermission)()")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();

            // 获取方法上的 @DataPermission 注解
            DataPermission dataPermission = method.getAnnotation(DataPermission.class);

            if (dataPermission != null) {
                // 将注解信息存储在上下文中,供 MyBatis 拦截器使用
                DataPermissionContext.set(dataPermission);
            }

            try {
                // 执行目标方法
                return joinPoint.proceed();
            } finally {
                // 方法执行完毕,清除数据权限上下文,避免内存泄露
                DataPermissionContext.clear();
            }
        }
    }

03.实现MyBatis-Plus拦截器:在自定义拦截器中加入对应的sql拼接代码
    /**
     * Mybatis数据权限拦截器
     */
    public class MybatisDataPermissionHandler implements MultiDataPermissionHandler {

        @Override
        public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
            try {
                // 获取当前线程中的数据权限信息
                DataPermission dataPermission = DataPermissionContext.get();
                if (dataPermission == null) {
                    return null;
                }

                // 数据权限相关的 SQL 片段
                User user = SecurityUtils.getCurrentUser();
                String dataScope = user.getPost() == null ? null : user.getPost().getDataScope();
                long deptId = user.getDept() == null ? -1 : user.getDept().getId();
                if (!StringUtils.hasText(dataScope)) {
                    return null;
                }
                String sqlSegment;
                if (DataScopeEnum.ALL.getValue().equals(dataScope)) {
                    return null;
                } else if (DataScopeEnum.DEPT.getValue().equals(dataScope)) {
                    sqlSegment = "dept_id = " + deptId;
                } else {
                    sqlSegment = "create_by_id = " + user.getId();
                }
                return CCJSqlParserUtil.parseCondExpression(sqlSegment);
            } catch (JSQLParserException e) {
                e.printStackTrace();
                return null;
            }
        }
    }

04.注册到MyBatis-Plus的拦截器配置中
    /**
     * Mybatis配置文件
     */
    @Configuration
    public class MybatisPlusConfig {

        @Bean
        public MybatisPlusInterceptor paginationInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MybatisDataPermissionHandler()));
            // 如果配置多个插件, 切记分页最后添加
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
            return interceptor;
        }
    }

05.在具体的查询方法上使用注解:用在controller、service、dao或mapper的任意方法上
    @Slf4j
    @RestController
    @AllArgsConstructor
    @RequestMapping("/api/test")
    public class TestController {

        private UserService userService;

        @DataPermission
        @GetMapping(value = "/list")
        public RestfulResult list() {
            return RestfulResult.success(userService.list());
        }
    }

3.20 [2]插件:动态表名

01.实现多租户系统的动态表名方案
    a.背景
        在实现多租户系统时,常见的方案是采用同库不同表的策略
        这种方法根据当前登录用户所属的租户,动态地选择不同的表进行查询操作
    b.通过自定义注解和插件来实现动态表名的功能
        a.注解
            自定义注解@DynamicTableName,用于标注动态表名的规则
        b.该注解包含两个内置参数
            tableName:表示当前表名
            tenant:代表租户ID经过排序后的字符串
        c.场景
            例如标注@DynamicTableName("biz_#{#tenant}_#{#tableName}")在一个实体类上
            当前表名为person,租户序列为aa
            那么最终生成的动态表名将是biz_aa_person
            我们可以使用同一个Entity类实现对不同租户表的查询,大大提高了代码的复用性和灵活性

02.核心代码
    a.自定义注解
        @Documented
        @Retention(RUNTIME)
        @Target(TYPE)
        public @interface DynamicTableName {
            String dynamicExpression() default "";
        }
    b.动态表名处理器
        public class DynamicTableNameHandler implements TableNameHandler {
            @Override
            public String dynamicTableName(String sql, String tableName) {
                TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
                if (tableInfo == null) {
                    return tableName;
                }
                Class<?> entityType = tableInfo.getEntityType();
                DynamicTableName annotation = entityType.getAnnotation(DynamicTableName.class);
                if (annotation != null) {
                    return DynamicTableNameUtils.parseTableName(tableInfo.getTableName(), annotation.dynamicExpression());
                } else {
                    return tableName;
                }
            }
        }
    c.表名解析工具:DynamicTableNameUtils工具类
        public abstract class DynamicTableNameUtils {
            public static String parseTableName(String tableName, String paramExpression) {
                StandardEvaluationContext context = new StandardEvaluationContext();
                ExpressionParser parser = new SpelExpressionParser();
                context.setVariable("tableName", tableName);
                context.setVariable("tenant", resolveTenantId(UserContext.getTenantId()));
                Expression expr = parser.parseExpression(paramExpression, new TemplateParserContext());
                return expr.getValue(context, String.class);
            }

            // ... 其他辅助方法 ...
        }

03.使用示例
    a.定义动态用户表
        @Data
        @EqualsAndHashCode(callSuper = true)
        @DynamicTableName(dynamicExpression = "biz_#{#tenant}_#{#tableName}")
        public class DynamicUser extends Model<DynamicUser> {
            private Long id;
            private String position;
            private String dept;
        }
    b.测试用例
        @Slf4j
        @RunWith(SpringRunner.class)
        @TestPropertySource(locations = "classpath:application-dynamic.properties")
        @ContextConfiguration(classes = {
                DataSourceAutoConfiguration.class,
                MybatisPlusAutoConfiguration.class,
                OrmMpConfiguration.class,
                TestConfig.class,
        })
        @SpringBootTest(classes = {DynamicTableNameTest.class})
        public class DynamicTableNameTest {
            // ... 测试方法和辅助方法 ...

            @Test
            public void testDynamicTableName() {
                // 模拟租户1
                MockSessionUser mockSessionUser = new MockSessionUser();
                mockSessionUser.setId(1L);
                mockSessionUser.setTenantId(1L);
                UserContext.setUser(mockSessionUser);

                List<DynamicUser> list = dynamicUserService.list();
                Assert.assertEquals(2, list.size());

                // 模拟租户2
                MockSessionUser mockSessionUser2 = new MockSessionUser();
                mockSessionUser2.setId(2L);
                mockSessionUser2.setTenantId(37L);
                UserContext.setUser(mockSessionUser2);

                List<DynamicUser> list2 = dynamicUserService.list();
                Assert.assertEquals(1, list2.size());
            }
        }

3.21 [2]插件:乐观锁/悲观锁

00.总结
    a.悲观锁:读多写少
        认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改
        因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观地认为,不加锁的并发操作一定会出问题
    b.乐观锁:写多读少
        正好和悲观锁相反,它获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁
        只是在更新数据的时候,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作
        如果没被其他线程修改则进行数据更新,如果被其他线程修改则不进行数据更新

01.乐观锁
    a.定义
        假设并发冲突很少,操作时不加锁,而在提交时通过版本号等机制检查数据是否被其他事务修改。
    b.实现
        在实体类中添加 @Version 注解的字段,表示版本号。
        插件配置:在 MyBatis Plus 配置中启用乐观锁插件。
        操作时,更新记录时自动检查版本号是否一致。
    c.示例
        @Version
        private Integer version; // 版本号字段

        // 更新操作
        User user = userMapper.selectById(1);
        user.setName("New Name");
        userMapper.updateById(user); // 更新时会检查版本号
    d.优点
        减少锁竞争,提高性能。
        适用于读多写少的场景。
    e.缺点
        如果并发冲突频繁,可能导致频繁重试,影响性能

02.悲观锁
    a.定义
        假设并发冲突很高,操作时对数据加锁,确保其他事务不能同时访问。
    b.实现
        通过数据库的锁机制(如 SELECT ... FOR UPDATE)在查询时加锁。
    c.示例
        // 使用悲观锁查询
        User user = userMapper.selectByIdForUpdate(1); // 假设实现了悲观锁查询方法
    d.优点
        确保数据一致性,适用于高并发的写操作场景。
    e.缺点
        增加了锁的竞争,可能导致性能下降和死锁风险。

3.22 [2]插件:控制台打印完整sql

00.汇总
    a.分类
        01.默认打印
        02.自定义拦截器
        03.自定义拦截器:Mybatis Log Ultra插件
        04.自定义拦截器:拦截 执行的sql语句 存储到 数据库
        05.自定义拦截器:用户每次在系统中进行update操作都需要形成操作记录
    b.注意事项
        a.统一日志框架
            应用中不可直接使用第三方日志组件(如 Log4j、Logback)的 API
            而应依赖使用统一日志框架(如 SLF4J、JCL—Jakarta Commons Logging)的 API
            这有助于日志管理的统一和灵活性
        b.扩展日志命名
            应用中的扩展日志(如打点、临时监控、访问日志等)命名方式应为 appName_logType_logName.log
            logType 表示日志类型,如 stats、monitor、access 等;logName 描述日志内容
            这样命名有助于通过文件名快速识别日志的应用、类型和目的
        c.避免重复打印
            避免重复打印日志,浪费磁盘空间,在日志配置中设置 additivity=false
        d.禁止使用System.out/System.err
            生产环境禁止使用 System.out 或 System.err 输出,或使用 e.printStackTrace() 打印异常堆栈
            System.out 是单例,可能造成死锁,并且生产环境通常会忽略控制台日志,导致信息丢失

01.默认打印
    a.配置
        mybatis-plus:
          configuration:
            log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    b.执行sql
        <select id="selectByUserName" resultType="com.example.pojo.User">
            select user_name userName , age from t_user where user_name = #{name}
        </select>
    c.控制台
        JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@f1ad052] will not be managed by Spring
        ==> Preparing: select user_name userName , age from t_user where user_name = ?
        ==> Parameters: zhangSan(String)
        <== Columns: userName, age
        <== Row: zhangSan, null
        <== Total: 1
        Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6786df71]

02.自定义拦截器
    a.MybatisPlusAllSqlLog
        a.说明
            该类实现了InnerInterceptor接口,主要用于在 MyBatis Plus 执行查询和更新操作之前记录相关的 SQL 信息
            包括参数值、SQL 语句的 ID 以及完整的 SQL 语句内容等,以便于进行调试和查看数据库交互的具体情况
        b.代码
            package com.ssm.subject.infra.config;

            import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
            import lombok.extern.log4j.Log4j2;
            import org.apache.ibatis.executor.Executor;
            import org.apache.ibatis.mapping.BoundSql;
            import org.apache.ibatis.mapping.MappedStatement;
            import org.apache.ibatis.mapping.ParameterMapping;
            import org.apache.ibatis.reflection.MetaObject;
            import org.apache.ibatis.session.Configuration;
            import org.apache.ibatis.session.ResultHandler;
            import org.apache.ibatis.session.RowBounds;
            import org.apache.ibatis.type.TypeHandlerRegistry;
            import org.springframework.util.CollectionUtils;

            import java.sql.SQLException;
            import java.text.DateFormat;
            import java.util.Date;
            import java.util.List;
            import java.util.Locale;
            import java.util.regex.Matcher;
            @Log4j2
            public class MybatisPlusAllSqlLog implements InnerInterceptor {

                @Override
                public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
                    logInfo(boundSql, ms, parameter);
                }

                @Override
                public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
                    BoundSql boundSql = ms.getBoundSql(parameter);
                    logInfo(boundSql, ms, parameter);
                }

                private static void logInfo(BoundSql boundSql, MappedStatement ms, Object parameter) {
                    try {
                        log.info("parameter = " + parameter);
                        // 获取到节点的id,即sql语句的id
                        String sqlId = ms.getId();
                        log.info("sqlId = " + sqlId);
                        // 获取节点的配置
                        Configuration configuration = ms.getConfiguration();
                        // 获取到最终的sql语句
                        String sql = getSql(configuration, boundSql, sqlId);
                        log.info("完整的sql:{}", sql);
                    } catch (Exception e) {
                        log.error("异常:{}", e.getLocalizedMessage(), e);
                    }
                }

                // 封装了一下sql语句,使得结果返回完整xml路径下的sql语句节点id + sql语句
                public static String getSql(Configuration configuration, BoundSql boundSql, String sqlId) {
                    return sqlId + ":" + showSql(configuration, boundSql);
                }

                // 进行?的替换
                public static String showSql(Configuration configuration, BoundSql boundSql) {
                    // 获取参数
                    Object parameterObject = boundSql.getParameterObject();
                    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
                    // sql语句中多个空格都用一个空格代替
                    String sql = boundSql.getSql().replaceAll("[\s]+", " ");
                    if (!CollectionUtils.isEmpty(parameterMappings) && parameterObject != null) {
                        // 获取类型处理器注册器,类型处理器的功能是进行java类型和数据库类型的转换
                        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
                        // 如果根据parameterObject.getClass()可以找到对应的类型,则替换
                        if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                            sql = sql.replaceFirst("\?",
                                    Matcher.quoteReplacement(getParameterValue(parameterObject)));
                        } else {
                            // MetaObject主要是封装了originalObject对象,提供了get和set的方法用于获取和设置originalObject的属性值,主要支持对JavaBean、Collection、Map三种类型对象的操作
                            MetaObject metaObject = configuration.newMetaObject(parameterObject);
                            for (ParameterMapping parameterMapping : parameterMappings) {
                                String propertyName = parameterMapping.getProperty();
                                if (metaObject.hasGetter(propertyName)) {
                                    Object obj = metaObject.getValue(propertyName);
                                    sql = sql.replaceFirst("\?",
                                            Matcher.quoteReplacement(getParameterValue(obj)));
                                } else if (boundSql.hasAdditionalParameter(propertyName)) {
                                    // 该分支是动态sql
                                    Object obj = boundSql.getAdditionalParameter(propertyName);
                                    sql = sql.replaceFirst("\?",
                                            Matcher.quoteReplacement(getParameterValue(obj)));
                                } else {
                                    // 打印出缺失,提醒该参数缺失并防止错位
                                    sql = sql.replaceFirst("\?", "缺失");
                                }
                            }
                        }
                    }
                    return sql;
                }

                // 如果参数是String,则添加单引号, 如果是日期,则转换为时间格式器并加单引号; 对参数是null和不是null的情况作了处理
                private static String getParameterValue(Object obj) {
                    String value;
                    if (obj instanceof String) {
                        value = "'" + obj.toString() + "'";
                    } else if (obj instanceof Date) {
                        DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT,
                                DateFormat.DEFAULT, Locale.CHINA);
                        value = "'" + formatter.format(new Date()) + "'";
                    } else {
                        if (obj != null) {
                            value = obj.toString();
                        } else {
                            value = "";
                        }
                    }
                    return value;
                }

            }
    b.配置SQL打印拦截器
        a.说明
            实现了Interceptor接口,主要用于拦截 MyBatis 执行的 SQL 语句(包括更新和查询操作)
            并记录这些 SQL 语句的执行时间,根据执行时间的不同范围输出相应的日志信息
            以便于对 SQL 执行性能进行监控和分析
        b.代码
            package com.ssm.subject.infra.config;

            import lombok.extern.log4j.Log4j2;
            import org.apache.ibatis.cache.CacheKey;
            import org.apache.ibatis.executor.Executor;
            import org.apache.ibatis.mapping.BoundSql;
            import org.apache.ibatis.mapping.MappedStatement;
            import org.apache.ibatis.plugin.*;
            import org.apache.ibatis.session.ResultHandler;
            import org.apache.ibatis.session.RowBounds;

            import java.util.Properties;

            @Log4j2
            @Intercepts({
                    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class,
                            Object.class}),
                    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
                            Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})
            public class SqlStatementInterceptor implements Interceptor {

                @Override
                public Object intercept(Invocation invocation) throws Throwable {
                    long startTime = System.currentTimeMillis();
                    try {
                        return invocation.proceed();
                    } finally {
                        long timeConsuming = System.currentTimeMillis() - startTime;
                        log.info("执行SQL:{}ms", timeConsuming);
                        if (timeConsuming > 999 && timeConsuming < 5000) {
                            log.info("执行SQL大于1s:{}ms", timeConsuming);
                        } else if (timeConsuming >= 5000 && timeConsuming < 10000) {
                            log.info("执行SQL大于5s:{}ms", timeConsuming);
                        } else if (timeConsuming >= 10000) {
                            log.info("执行SQL大于10s:{}ms", timeConsuming);
                        }
                    }
                }

                @Override
                public Object plugin(Object target) {
                    return Plugin.wrap(target, this);
                }

                @Override
                public void setProperties(Properties properties) {

                }
            }
    c.注入Spring容器
        @Configuration
        public class MybatisConfiguration {
            @Bean
            public MybatisPlusInterceptor mybatisPlusInterceptor(){
                MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
                mybatisPlusInterceptor.addInnerInterceptor(new MybatisPlusAllSqlLog());
                return mybatisPlusInterceptor;
            }

        }
    d.测试
        16:08:12.594 [http-nio-3000-exec-2] INFO  com.ssm.subject.infra.config.MybatisPlusAllSqlLog - parameter = {arg0=[5, 6], collection=[5, 6], list=[5, 6]}
        16:08:12.594 [http-nio-3000-exec-2] INFO  com.ssm.subject.infra.config.MybatisPlusAllSqlLog - sqlId = com.ssm.subject.infra.basic.mapper.SubjectLabelDao.queryByLabelIds
        16:08:12.594 [http-nio-3000-exec-2] INFO  com.ssm.subject.infra.config.MybatisPlusAllSqlLog - 完整的sql:com.ssm.subject.infra.basic.mapper.SubjectLabelDao.queryByLabelIds:select * from subject_label where id in ( 5 , 6 )

03.自定义拦截器:Mybatis Log Ultra插件
    a.步骤1
        a.说明
            增强了Mybatis的MappedStatement类,通过在项目启动时自动挂载上一个agent实现的
            它的作用是拦截到SQL和参数,然后将他们拼接成完整可执行的SQL,然后通过日志系统输出到控制台
        b.代码
            package com.tiktok.core.transformer;

            import net.bytebuddy.agent.builder.AgentBuilder;
            import net.bytebuddy.asm.Advice;
            import net.bytebuddy.matcher.ElementMatchers;
            import org.apache.ibatis.mapping.BoundSql;
            import org.apache.ibatis.mapping.MappedStatement;
            import org.apache.ibatis.mapping.ParameterMapping;
            import org.apache.ibatis.reflection.MetaObject;
            import org.apache.ibatis.session.Configuration;
            import org.apache.ibatis.type.TypeHandlerRegistry;

            import java.text.DateFormat;
            import java.tiktok.spy.SpyAPI;
            import java.util.Date;
            import java.util.List;
            import java.util.Locale;
            import java.util.regex.Matcher;

            public class MybatisGetBoundSqlTransformer implements AgentBuilderVisitor {

                private final static String clazz = "org.apache.ibatis.mapping.MappedStatement";

                @Override
                public String getClazz() {
                    return clazz;
                }

                @Override
                public AgentBuilder build(AgentBuilder agentBuilder) {
                    return agentBuilder
                            // 作用于类,指定拦截
                            .type(ElementMatchers.named(getClazz()))
                            // 作用于方法,匹配并修改
                            .transform((builder, typeDescription, classLoader, javaModule, protectionDomain) -> builder
                                    .visit(
                                            Advice.to(MybatisGetBoundSqlTransformer.MybatisGetBoundSqlInterceptor.class)
                                                    .on(ElementMatchers.named("getBoundSql")
                                                            .and(ElementMatchers.takesArguments(Object.class))
                                                            .and(ElementMatchers.returns(BoundSql.class))
                                                    )
                                    )
                            );
                }

                public static class MybatisGetBoundSqlInterceptor {
                    @Advice.OnMethodExit
                    public static void exit(@Advice.This MappedStatement mappedStatement, @Advice.Return BoundSql returnObject) {
                        try {
                            if (SpyAPI.checkEntrypoint()) {
                                Object parameterObject = returnObject.getParameterObject();
                                List<ParameterMapping> parameterMappings = returnObject.getParameterMappings();
                                final String sql = returnObject.getSql().replaceAll("\s+", " ");
                                final String mapperId = mappedStatement.getId();

                                if (parameterObject == null || parameterMappings == null || parameterMappings.isEmpty()) {
                                    SpyAPI.atExit("mybatis", mapperId, sql);
                                    return;
                                }

                                Configuration configuration = mappedStatement.getConfiguration();
                                TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
                                String newSql = String.copyValueOf(sql.toCharArray());

                                if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                                    String value;
                                    if (parameterObject instanceof String) {
                                        value = "'" + parameterObject + "'";
                                    } else if (parameterObject instanceof Date) {
                                        DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.getDefault());
                                        value = "'" + formatter.format(parameterObject) + "'";
                                    } else {
                                        value = parameterObject.toString();
                                    }
                                    newSql = newSql.replaceFirst("\?", Matcher.quoteReplacement(value));
                                } else {
                                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                                    for (ParameterMapping parameterMapping : parameterMappings) {
                                        String propertyName = parameterMapping.getProperty();
                                        Object parameter = null;
                                        if (metaObject.hasGetter(propertyName)) {
                                            parameter = metaObject.getValue(propertyName);
                                        } else if (returnObject.hasAdditionalParameter(propertyName)) {
                                            parameter = returnObject.getAdditionalParameter(propertyName);
                                        }
                                        String value;
                                        if (parameter instanceof String) {
                                            value = "'" + parameter + "'";
                                        } else if (parameter instanceof Date) {
                                            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.getDefault());
                                            value = "'" + formatter.format(parameter) + "'";
                                        } else {
                                            if (null != parameter) {
                                                value = parameter.toString();
                                            } else {
                                                value = "缺失";
                                            }
                                        }
                                        newSql = newSql.replaceFirst("\?", Matcher.quoteReplacement(value));
                                    }
                                }

                                SpyAPI.atExit("mybatis", mapperId, newSql);
                            }
                        } catch (Throwable throwable) {
                            throwable.printStackTrace();
                        }
                    }
                }

            }
        c.说明
            SpyAPI.atExit("mybatis", mapperId, newSql)是借鉴了arthas间谍类的设计,将用户应用中获取到的SQL转交给agent进行处理
    b.步骤2
        a.说明
            通过获取了被@RestContronller注解的类,拦截了所有的接口请求,这样做的目的是希望只有当接口被请求时
            才会在控制台中打印请求接口内执行的SQL,不必被持续不断滚动的SQL所干扰
        b.代码
            package com.tiktok.core.transformer;

            import net.bytebuddy.agent.builder.AgentBuilder;
            import net.bytebuddy.asm.Advice;
            import net.bytebuddy.matcher.ElementMatchers;
            import org.springframework.context.support.AbstractApplicationContext;
            import org.springframework.web.bind.annotation.RestController;

            import java.tiktok.spy.SpyAPI;
            import java.util.Map;

            public class AbstractApplicationContextTransformer implements AgentBuilderVisitor {

                private final static String clazz = "org.springframework.context.support.AbstractApplicationContext";

                @Override
                public String getClazz() {
                    return clazz;
                }

                @Override
                public AgentBuilder build(AgentBuilder agentBuilder) {
                    return agentBuilder
                            // 作用于类,指定拦截
                            .type(ElementMatchers.named(getClazz()))
                            // 作用于方法,匹配并修改
                            .transform((builder, typeDescription, classLoader, javaModule, protectionDomain) -> builder
                                    .visit(
                                            Advice.to(AbstractApplicationContextTransformer.AbstractApplicationContextInterceptor.class)
                                                    .on(ElementMatchers.named("finishRefresh")
                                                            .and(ElementMatchers.isProtected())
                                                            .and(ElementMatchers.takesNoArguments())
                                                    )
                                    )
                            );
                }

                public static class AbstractApplicationContextInterceptor {
                    @Advice.OnMethodExit
                    public static void exit(@Advice.This AbstractApplicationContext abstractApplicationContext) {
                        try {
                            Map<String, Object> restControllerMap = abstractApplicationContext.getBeansWithAnnotation(RestController.class);
                            SpyAPI.setRestControllerMap(restControllerMap);
                        } catch (Throwable throwable) {
                            // ignore
                        }
                    }
                }

            }
    c.总结
        以上两个步骤,一个负责拦截特定的请求,一个负责拼接SQL然后转发到agent内部
        在agent端就可以进行过滤、分类等精细的控制,通过插件端动态的下发规则,从而实现日志的精准输出

04.自定义拦截器:拦截 执行的sql语句 存储到 数据库
    a.定义 Mybatis 拦截器
        package com.erp.init.mybatisplus;

        import com.erp.init.sqlLog.SqLogUtil;
        import lombok.extern.slf4j.Slf4j;
        import org.apache.commons.lang3.StringUtils;
        import org.apache.ibatis.executor.statement.StatementHandler;
        import org.apache.ibatis.mapping.MappedStatement;
        import org.apache.ibatis.plugin.Interceptor;
        import org.apache.ibatis.plugin.Intercepts;
        import org.apache.ibatis.plugin.Invocation;
        import org.apache.ibatis.plugin.Plugin;
        import org.apache.ibatis.plugin.Signature;
        import org.apache.ibatis.reflection.MetaObject;
        import org.apache.ibatis.reflection.SystemMetaObject;
        import org.springframework.stereotype.Component;

        import java.lang.reflect.Proxy;
        import java.sql.Connection;
        import java.sql.Statement;
        import java.util.Map;
        import java.util.Properties;

        //在mybatis中可被拦截的类型有四种(按照拦截顺序):
        //        Executor: 拦截执行器的方法
        //        ParameterHandler: 拦截参数的处理
        //        ResultHandler:拦截结果集的处理
        //        StatementHandler: 拦截Sql语法构建的处理
        //@Intercepts({
        //    @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
        //    @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
        //    @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
        //})
        //  1. @Intercepts:标识该类是一个拦截器;
        //      @Signature:指明自定义拦截器需要拦截哪一个类型,哪一个方法;
        //    2.1 type:对应四种类型中的一种;
        //    2.2 method:对应接口中的哪类方法(因为可能存在重载方法);
        //    2.3 args:对应哪一个方法;
        @Intercepts({
                @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
                @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
        })
        @Slf4j
        @Component  // 必须要交给 spring boot 管理
        public class MybatisLogInterceptor implements Interceptor {

             //这个方法里 是重点  主要是拦截 需要执行的sql
            @Override
            public Object intercept(Invocation invocation) throws Throwable {
                // 执行方法
                Object result = invocation.proceed();

                // 获取MapperStatement对象,获取到sql的详细信息
                Object realTarget = realTarget(invocation.getTarget());
                // 获取metaObject对象
                MetaObject metaObject = SystemMetaObject.forObject(realTarget);
                // 获取MappedStatement对象
                MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
                // 获取方法的全类名称
                String methodFullName = ms.getId();

                // 判断是否是需要日志记录的方法
                //   我用的是 Mybatis 提供的方法  methodFullName  这个可以获取到 方法 全类名
                //  如果是 写的 原生 sql 执行的 没测试过  需要 测试一下使用
                Map<String, Object> map = SqLogUtil.verifyRecordLog(methodFullName);
                if (!map.isEmpty() && (boolean) map.get("isRecord")) {

                    Statement statement;
                    // 获取方法参数
                    Object[] args = invocation.getArgs();
                    Object firstArg = args[0];
                    if (Proxy.isProxyClass(firstArg.getClass())) {
                        statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement");
                    } else {
                        statement = (Statement) firstArg;
                    }
                     // 这个对象里 有好几个 statement 和 stmt
                    // 可以打打断点 看啊看 statement 这个对象里的数据结构
                    MetaObject stmtMetaObj = SystemMetaObject.forObject(statement);
                    Object stmt = stmtMetaObj.getValue("stmt");
                    MetaObject metaObject1 = SystemMetaObject.forObject(stmt);
                    Object statement1 = metaObject1.getValue("statement");
                    MetaObject metaObject2 = SystemMetaObject.forObject(statement1);
                    // mybatis 最后执行的sql 就在 这个对象里
                    Object stmt1 = metaObject2.getOriginalObject();
                    String finalSql=stmt1.toString();
                    //去掉不要的字符串
                    finalSql = finalSql.substring(finalSql.indexOf(":") + 1, finalSql.length());

                    log.info("最终sql: \n " + finalSql);

                    String saveLogSql = SqLogUtil.getSaveLogSql(methodFullName, (String) map.get("desc"), finalSql);

                    if (StringUtils.isNotBlank(saveLogSql)) {
                        Connection connection = statement.getConnection();

                        if (connection.isReadOnly()) { // 当前事务是只读事务,则重新用不同的Connection对象
                            Connection mysqlConnection = SqLogUtil.getMysqlConnection();
                            if (mysqlConnection != null) {
                                try {
                                    mysqlConnection.createStatement().execute(saveLogSql);
                                } catch (Exception e) {
                                    e.printStackTrace();
                                    log.error("拦截器记录日志出错!", e);
                                } finally {
                                    mysqlConnection.close();//关闭连接
                                }
                            }
                        } else {
                            connection.createStatement().execute(saveLogSql);
                        }
                    }
                }
                return result;
            }

            @Override
            public Object plugin(Object target) {
                if (target instanceof StatementHandler) {
                    return Plugin.wrap(target, this);
                }
                return target;
            }

            @Override
            public void setProperties(Properties prop) {

            }

            /**
             * <p>
             * 获得真正的处理对象,可能多层代理.
             * </p>
             */
            @SuppressWarnings("unchecked")
            public static <T> T realTarget(Object target) {
                if (Proxy.isProxyClass(target.getClass())) {
                    MetaObject metaObject = SystemMetaObject.forObject(target);
                    return realTarget(metaObject.getValue("h.target"));
                }
                return (T) target;
            }
        }
    b.创建一个 sql 存储工具类
        package com.erp.init.sqlLog;

        import com.alibaba.druid.sql.SQLUtils;
        import com.alibaba.druid.util.JdbcConstants;
        import lombok.extern.slf4j.Slf4j;
        import org.apache.ibatis.session.SqlSessionFactory;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.stereotype.Component;

        import javax.annotation.PostConstruct;
        import javax.sql.DataSource;
        import java.sql.Connection;
        import java.sql.SQLException;
        import java.time.LocalDateTime;
        import java.util.ArrayList;
        import java.util.HashMap;
        import java.util.List;
        import java.util.Map;

        @Component  // 必须要交给 spring boot 管理
        @Slf4j
        public class SqLogUtil {
            /**
             * 判断哪个方法 需要被记录
             * 这是一种方法 也可以设计成注解的形式 判断注解
             *  我用的是 Mybatis 提供的方法
             *  如果是 写的 原生 sql 执行的 没测试过  需要 测试一下使用
             */
            public static final String[] DEFAULT_RECORD_METHOD_START = {
                    "com.erp.base.mapper.YsDictMapper.updateById",
                    "com.erp.base.mapper.YsDictMapper.insert",
                    "com.erp.base.mapper.YsDictMapper.deleteById",
                    "com.erp.base.mapper.YsDictSubMapper.insert",
                    "com.erp.base.mapper.YsDictSubMapper.updateById",
                    "com.erp.base.mapper.YsDictSubMapper.deleteById",
                    "com.erp.base.mapper.YsFormMapper.insert",
                    "com.erp.base.mapper.YsFormMapper.updateById",
                    "com.erp.base.mapper.YsFormMapper.delete"
            };

            /**
             * 默认不记录的操作方法(记录日志的方法)
             * 这个是 执行 Mapper 里的方法
             * YsSqlLogMapper  类里的 方法
             */
            public static final String[] DEFAULT_NOT_RECORED_METHOD = new String[]{"com.erp.base.mapper.YsSqlLogMapper.saveSqlLog"};

            private static SqLogUtil logUtils;

            /**
             * 注入SqlSessionFactory对象
             */
            @Autowired
            private SqlSessionFactory sqlSessionFactory;

            /**
             * 注入DataSource对象
             */
            @Autowired
            private DataSource mysqlDataSource;

            public SqLogUtil() {
            }

            /**
             * 给logUtils对象赋值
             */
            @PostConstruct
            public void init() {
                logUtils = this;
                logUtils.sqlSessionFactory = this.sqlSessionFactory;
                logUtils.mysqlDataSource = this.mysqlDataSource;
            }

            /**
             * 验证方法是否需要日志记录
             *
             * @param methodFullName
             * @return
             */
            public static Map<String, Object> verifyRecordLog(String methodFullName) {
                Map<String, Object> resultMap = new HashMap<>();

                for (int i = 0; i < DEFAULT_NOT_RECORED_METHOD.length; i++) {
                    if (methodFullName.equals(DEFAULT_NOT_RECORED_METHOD[i])) {
                        return resultMap;
                    }
                }

                boolean isRecord = false;

                String desc = "";
                int flag = methodFullName.lastIndexOf(".");
                String classPath = methodFullName.substring(0, flag);
                String methodName = methodFullName.substring(flag + 1);
                Class<?> clazz = null;
                try {
                    clazz = Class.forName(classPath);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                    log.error("判断是否需要记录日志异常!", e);
                }
                if (clazz != null) {
                    if (verifyMethodName(methodFullName)) {
                        isRecord = true;
                    }
                }
                resultMap.put("isRecord", isRecord); // 是否记录
                resultMap.put("desc", desc); // 方法描述
                return resultMap;
            }


            /**
             * 判断方法名是否满足日志记录格式
             *
             * @param methodName
             * @return
             */
            public static boolean verifyMethodName(String methodName) {
                boolean methodNameFlag = false;
                for (int i = 0; i < DEFAULT_RECORD_METHOD_START.length; i++) {
                    if (methodName.startsWith(DEFAULT_RECORD_METHOD_START[i])) {
                        methodNameFlag = true;
                        break;
                    }
                }
                return methodNameFlag;
            }


            /**
             * 填充日记记录SQL参数
             *
             * @param methodFullName
             * @param desc
             * @param originalSql
             * @return
             */
            private static List<Object> getParamList(String methodFullName, String desc, String originalSql) {
                List<Object> paramList = new ArrayList<>();
                // 完整SQL语句
                paramList.add(handlerSql(originalSql));
                //时间
                paramList.add(LocalDateTime.now().toString());
                return paramList;
            }

            /**
             * 处理SQL语句
             *
             * @param originalSql
             * @return
             */
            private static String handlerSql(String originalSql) {
              //  String sql = originalSql.substring(originalSql.indexOf(":") + 1);
                // 将原始sql中的空白字符(\s包括换行符,制表符,空格符)替换为" "
                return originalSql.replaceAll("[\\s]+", " ");
            }

            /**
             * 获取日志保存SQL
             *
             * @param methodFullName
             * @param desc
             * @param originalSql
             * @return
             */
            public static String getSaveLogSql(String methodFullName, String desc, String originalSql) {
                String sql = logUtils.sqlSessionFactory.getConfiguration()
                        .getMappedStatement(DEFAULT_NOT_RECORED_METHOD[0]).getBoundSql(null).getSql();
                List<Object> paramList = getParamList(methodFullName, desc, originalSql);
                // paramList  是你需要存到数据库的 数据
                sql = paramList != null && !paramList.isEmpty() ? SQLUtils.format(sql, JdbcConstants.MYSQL, paramList) : null;
                return sql;
            }

            /**
             * 获取mysql Connection对象
             *
             * @return
             */
            public static Connection getMysqlConnection() {
                Connection conn = null;
                try {
                    conn = logUtils.mysqlDataSource.getConnection();
                } catch (SQLException e) {
                    e.printStackTrace();
                    log.error("保存日志时获取Connection对象异常!", e);
                }

                return conn;
            }
        }
    c.创建一个存储sql语句的Mapper方法
        package com.erp.base.mapper;

        import com.baomidou.mybatisplus.core.mapper.BaseMapper;
        import com.erp.api.entities.base.base.YsSqlLog;
        import org.apache.ibatis.annotations.Insert;
        import org.apache.ibatis.annotations.Mapper;

        /**
         * sql日志表 Mapper 接口
         */
        @Mapper
        public interface YsSqlLogMapper extends BaseMapper<YsSqlLog> {


            @Insert({"insert into ys_sql_log(sql_info,date) values(#{ysSqlLog.sqlInfo},#{ysSqlLog.createTime})"})
            int saveSqlLog(YsSqlLog ysSqlLog);

        }
    d.执行sql
         数据库表里就会记录执行的sql语句

05.自定义拦截器:用户每次在系统中进行update操作都需要形成操作记录
    a.定义注解
        /**
         * 日志
         *
         * @author LYX
         * @date 2022/12/03
         */
        @Target({ElementType.PARAMETER, ElementType.METHOD})
        @Retention(RetentionPolicy.RUNTIME)
        @Documented

        public @interface Log {


            /**
             * 处理类型
             *
             * @return
             */
            String handleType() default "";

            /**
             * 操作功能
             */
            String czgn() default "";
        }
    b.定义切点
        /**
         * 操作记录切点
         *
         * @author LYX
         * @date 2022/12/03
         */
        @Aspect
        @Component
        @DependsOn("springFactoryUtils")
        public class OperationRecord{

            private  static Map<String, jiLuService> beanMap = SpringFactoryUtils.getBeanMap(jiLuService.class);

            /**
             * 1,定义切点
             * 2,定义切点之前执行
             * 3,定义切点之后执行
             */
            @Pointcut("@annotation(com.example.lyxtest.annotation.Log)")
            public void logPointCut() {
            }
            /**
             * 在切点之前执行
             *
             * @param joinPoint 连接点
             * @param log       日志
             */
            @Before("logPointCut() && @annotation(log)")
            public void beforePointCut(JoinPoint joinPoint, Log log){
                //获取到方法入参
                Object[] args = joinPoint.getArgs();
                jiLuService jiLuService = beanMap.get(log.handleType());
                jiLuService.before(args,log);
            }
            /**
             * 在切点之后执行
             *
             * @param joinPoint 连接点
             * @param result    结果
             */
            @AfterReturning(returning = "result",pointcut = " @annotation(com.example.lyxtest.annotation.Log)")
            public void afterPointCut(JoinPoint joinPoint,Object result){
                JSONObject outJson = (JSONObject) result;
                if(200 == Integer.valueOf(outJson.get("code").toString())){
                    //获得方法入参
                    Object[] args = joinPoint.getArgs();
                    //获取handle
                    MethodSignature signature = (MethodSignature)joinPoint.getSignature();
                    String handle = signature.getMethod().getAnnotation(Log.class).handleType();
                    jiLuService jiLuService = beanMap.get(handle);
                    jiLuService.after(args,signature);
                }
            }
        }
    c.实现类
        /**
         * 操作记录实现类
         *
         * @author LYX
         * @date 2022/12/03
         */
        @Component
        @Slf4j
        public abstract class RecordService {

            /**
             * 之前
             *
             * @param args arg游戏
             */
            public  void before(Object[] args, Log loga){
                log.info("handle:{},入参:{}",loga.handleType(), args);
            };


            /**
             * 后
             *
             * @param args      arg游戏
             * @param signature 签名
             */
            public abstract void after(Object[] args, MethodSignature signature);
        }
    d.具体操作
        /**
         * 部门查询handle
         *
         * @author LYXLYX
         * @date 2022/12/03
         */
        @Component("yhcx")
        @Slf4j
        public class yhcxHandle extends RecordService {
            @Override
            public void after(Object[] args, MethodSignature signature) {
                User user = (User) args[0];
                log.info("操作功能:{},操作值:{}",signature.getMethod().getAnnotation(Log.class).czgn(),String.valueOf(user));
            }
        }
    e.Controller层中插入@Log注解
        @Log(handleType = "yhcx",czgn = "部门查询")

3.23 [2]插件: 防全表更新与删除

01.说明
    在实际开发中,全表更新和删除是非常危险的操作,在MybatisPlus中,提供了插件和防止这种危险操作的发生

02.演示:全表更新
    a.代码
        @Test
        public void testUpdateAll(){
            User user = new User();
            user.setGender(GenderEnum.MAN);
            userService.saveOrUpdate(user,null);
        }
    b.说明
        这是很危险的
        如何解决呢?注入 MybatisPlusInterceptor类,并配置 BlockAttackInnerInterceptor拦截器

03.注入 MybatisPlusInterceptor类,并配置 BlockAttackInnerInterceptor拦截器
    a.代码
        package com.rainbowsea.config;

        import com.baomidou.mybatisplus.annotation.DbType;
        import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
        import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
        import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
        import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
        import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
        import org.apache.ibatis.plugin.Interceptor;
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;

        @Configuration
        public class MybatisPlusConfig {
            @Bean
            public MybatisPlusInterceptor mybatisPlusInterceptor() {
                MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();

                /*
                通过配置类来指定一个具体数据库的分页插件,因为不同的数据库的方言不同
                具体也会给你从的分页语句也会不同,这里我们指定数据库为 MySQL数据库
                 */
                mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
                mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());  // 防全表更新与删除插件
                return mybatisPlusInterceptor;
            }
        }
    b.测试全表更新,会出现抛出异常,防止了全表更新
        @SpringBootTest
        public class QueryTest {

            @Autowired
            private UserService userService;

            @Test
            void allUpdate(){
                User user = new User();
                user.setId(999L);
                user.setName("wang");
                user.setEmail("[email protected]");
                userService.saveOrUpdate(user,null);
            }
        }
    c.结果
        直接报异常,报错,不然,继续往下执行

3.24 [3]SQL分析:p6spy

01.无数据源
    a.思路
        1.在pom.xml中添加P6Spy依赖
        2.将数据源URL修改为P6Spy格式,jdbc:p6spy:mysql://localhost:3306/yourdb
        3.配置spy.properties文件,设置日志输出格式
        4.运行时,P6Spy会自动记录SQL执行情况,包括执行的SQL语句和执行时间,方便进行分析
    b.添加依赖
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <dependency>
            <groupId>p6spy</groupId>
            <artifactId>p6spy</artifactId>
            <version>3.9.1</version>
        </dependency>
    c.配置数据源
        在 application.properties 或 application.yml 中,将数据源 URL 修改为 P6Spy 的格式:
        spring.datasource.url=jdbc:p6spy:mysql://localhost:3306/yourdb
    d.配置spy.propertiesy
        #3.2.1以上使用
        modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
        #3.2.1以下使用或者不配置
        #modulelist=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
        # 自定义日志打印
        logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
        #日志输出到控制台
        appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
        # 使用日志系统记录 sql
        #appender=com.p6spy.engine.spy.appender.Slf4JLogger
        # 设置 p6spy driver 代理
        deregisterdrivers=true
        # 取消JDBC URL前缀
        useprefix=true
        # 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
        excludecategories=info,debug,result,commit,resultset
        # 日期格式
        dateformat=yyyy-MM-dd HH:mm:ss
        # 实际驱动可多个
        #driverlist=org.h2.Driver
        # 是否开启慢SQL记录
        outagedetection=true
        # 慢SQL记录标准 2 秒
        outagedetectioninterval=2
    e.使用 MyBatis Plus
        配置完毕后,使用 MyBatis Plus 进行数据库操作,P6Spy 会自动记录 SQL 执行情况。

02.有数据源
    a.依赖
        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!--p6spy-->
        <dependency>
            <groupId>p6spy</groupId>
            <artifactId>p6spy</artifactId>
            <version>3.9.1</version>
        </dependency>
        <!--druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.18</version>
        </dependency>
        <!--dynamic-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.27</version>
            <scope>runtime</scope>
        </dependency>
        <!--sqlserver-->
        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>sqljdbc4</artifactId>
            <version>4.0</version>
            <scope>runtime</scope>
        </dependency>
        <!--oracle-->
        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>ojdbc6</artifactId>
            <version>11.2.0.3</version>
            <scope>runtime</scope>
        </dependency>
        <!--postgresql-->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.2.25</version>
            <scope>runtime</scope>
        </dependency>
    b.配置数据源
        spring:
          autoconfigure:
            exclude:  # 排除自动配置
              - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
          datasource:
            druid:
              stat-view-servlet:  # Druid监控servlet配置
                enabled: true  # 启用监控servlet
                loginUsername: admin  # 登录用户名
                loginPassword: 123456  # 登录密码
                allow:  # 允许访问的IP
              web-stat-filter:
                enabled: true  # 启用Web监控统计
            dynamic:
              p6spy: true  # 默认false,建议线上关闭
              druid:  # Druid连接池配置
                initial-size: 5  # 初始连接数
                min-idle: 5  # 最小空闲连接
                maxActive: 20  # 最大活动连接
                maxWait: 60000  # 获取连接等待超时时间
                timeBetweenEvictionRunsMillis: 60000  # 间隔多久检测一次空闲连接
                minEvictableIdleTimeMillis: 300000  # 空闲连接最小生存时间
                validationQuery: SELECT 1 FROM DUAL  # 验证查询
                testWhileIdle: true  # 申请连接的时候检测空闲时间
                testOnBorrow: false  # 申请连接时执行validationQuery检测
                testOnReturn: false  # 归还连接时执行validationQuery检测
                poolPreparedStatements: true  # 开启PSCache
                maxPoolPreparedStatementPerConnectionSize: 20  # 每个连接上PSCache的大小
                filters: stat,slf4j  # 启用的过滤器
                connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000  # 连接属性
              datasource:
                master:  # 主数据源
                  url: jdbc:mysql://127.0.0.1:3307/ydsz-boot-parent?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
                  username: root
                  password: 123456
                  driver-class-name: com.mysql.cj.jdbc.Driver
                eas-datasource:  # EAS数据源
                  url: jdbc:oracle:thin:@127.0.0.1:1521:ORCL
                  username: scott
                  password: tiger
                  driver-class-name: oracle.jdbc.driver.OracleDriver
    c.配置spy.propertiesy
        #3.2.1以上使用
        modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory
        #3.2.1以下使用或者不配置
        #modulelist=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
        # 自定义日志打印
        logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
        #日志输出到控制台
        appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
        # 使用日志系统记录 sql
        #appender=com.p6spy.engine.spy.appender.Slf4JLogger
        # 设置 p6spy driver 代理
        deregisterdrivers=true
        # 取消JDBC URL前缀
        useprefix=true
        # 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
        excludecategories=info,debug,result,commit,resultset
        # 日期格式
        dateformat=yyyy-MM-dd HH:mm:ss
        # 实际驱动可多个
        #driverlist=org.h2.Driver
        # 是否开启慢SQL记录
        outagedetection=true
        # 慢SQL记录标准 2 秒
        outagedetectioninterval=5
    d.使用 MyBatis Plus
        配置完毕后,使用 MyBatis Plus 进行数据库操作,P6Spy 会自动记录 SQL 执行情况。

3.25 [3]SQL注入:构造器

01.定义
    SQL 注入器是 MyBatis Plus 提供的功能,用于防止 SQL 注入攻击,确保数据库安全
    在配置时,通过使用 QueryWrapper 和 UpdateWrapper 等构造器来构建 SQL 条件,从而有效防止注入风险

02.工作原理
    通过对 SQL 查询条件的参数进行严格的验证和过滤,确保传入的参数不包含恶意的 SQL 代码

03.配置方法
    a.开启注入器:配置SQL注入器,通常使用DefaultSqlInjector,它会自动处理基本的CRUD操作
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件
            return interceptor;
        }
    b.使用Wrapper
        使用 QueryWrapper 和 UpdateWrapper 等构造器来构建查询条件,这样可以有效防止 SQL 注入
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", name); // 使用参数化查询
        List<User> users = userMapper.selectList(queryWrapper);

3.26 [4]字段类型:TypeHandler

00.MyBatisPlus字段类型处理器
    a.回答
        用于【自定义Java类型】与【数据库类型】之间的转换
    b.说明
        可以通过实现 BaseTypeHandler 接口,重写相应的方法来处理特殊数据类型
        在实体类中,使用 @TableField 注解指定自定义类型处理器,从而实现特定字段的处理逻辑
        这对于处理复杂数据类型或需要特殊转换的字段非常有用

01.创建自定义类型处理器:实现 BaseTypeHandler 或 TypeHandler 接口,并重写相应的方法
    public class CustomTypeHandler extends BaseTypeHandler<YourType> {
        @Override
        public void setNonNullParameter(PreparedStatement ps, int i, YourType parameter, JdbcType jdbcType) throws SQLException {
            // 将 Java 类型转换为数据库类型
            ps.setString(i, parameter.toString());
        }

        @Override
        public YourType getNullableResult(ResultSet rs, String columnName) throws SQLException {
            // 从 ResultSet 中获取数据并转换为 Java 类型
            String value = rs.getString(columnName);
            return YourType.fromString(value);
        }

        @Override
        public YourType getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
            // 从 ResultSet 中获取数据并转换为 Java 类型
            String value = rs.getString(columnIndex);
            return YourType.fromString(value);
        }

        @Override
        public YourType getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
            // 从 CallableStatement 中获取数据并转换为 Java 类型
            String value = cs.getString(columnIndex);
            return YourType.fromString(value);
        }
    }

02.在实体类中使用自定义类型处理器:使用 @TableField 注解指定自定义类型处理器
    public class User {
        @TableField(typeHandler = CustomTypeHandler.class)
        private YourType customField;
        // 其他字段和方法
    }

03.配置 MyBatis Plus
    确保 MyBatis Plus 已正确配置,以便在执行 SQL 时使用自定义的类型处理器

3.27 [4]字段类型:自定义Json类型

01.参数设置
    a.描述
        当MyBatis执行SQL语句时,需要将用户传入的方法参数或者Mapper XML文件中定义的参数值
        设置到PreparedStatement对象中
    b.过程
        对于非基本类型的参数,如自定义对象、枚举或其他复杂类型,MyBatis将通过查找对应的TypeHandler实现类来完成转换工作
        MyBatis根据参数的Java类型找到对应的TypeHandler,然后调用其setParameter方法
        这个方法会将Java类型的数据转换为JDBC可识别的数据库类型,并调用PreparedStatement的set方法将转换后的数据写入预编译的SQL语句中

02.结果集映射
    a.描述
        在查询执行完毕后,MyBatis需要将从ResultSet中读取的数据转换成Java类型并填充到目标对象属性上
    b.过程
        MyBatis通过结果映射配置来确定将结果集中的哪些列映射到Java对象的哪些属性上
        当MyBatis从ResultSet中获取某列数据时,它会根据结果映射配置所关联的Java类型,找到相应的TypeHandler
        然后,MyBatis调用TypeHandler的getResult方法,该方法将从数据库返回的JDBC类型数据转换为Java类型,并最终赋值给目标Java对象的属性

03.自定义TypeHandler
    a.描述
        在MyBatis中,虽然已经提供了丰富的内置TypeHandler来处理常见的数据类型
        但在实际开发中,有时候我们可能需要处理一些特殊的数据类型或者定制化的数据转换逻辑
    b.TypeHandler接口
        要编写自定义的TypeHandler,首先需要实现MyBatis提供的TypeHandler接口
        该接口定义了处理结果集方法getResult和处理参数的方法setParameter
    c.BaseTypeHandler抽象类
        实际上,我们在日常开发中常使用的是继承BaseTypeHandler抽象类
        BaseTypeHandler实现了TypeHandler接口,提供了对TypeHandler接口中方法的默认实现,包括空值处理、异常处理等
    d.MappedJdbcTypes和MappedTypes
        在自定义的TypeHanlder时,可以使用@MappedJdbcTypes和@MappedTypes注解,显示的指定你的TypeHanlder要处理的JDBC类型和Java类型
        @MappedJdbcTypes注解用于指定该TypeHandler支持的JDBC类型
        @MappedTypes注解则用于指定Java类型

04.注册TypeHandler
    a.全局配置
        可以在创建SqlSessionFactory时,通过SqlSessionFactoryBean的setTypeHandlers的方法全局指定你的TypeHandler
    b.局部指定
        可以在对应的XML映射文件中进行配置指定

05.自定义Json类型
    a.描述
        以Mysql的JSON数据类型为例,查询以及保存user_info表中的address字段
        因address字段在库中以JSON存储,使用对象AddressBO接收
    b.定义一个专门处理数据JSON类型数据与Java对象相互转换的抽象TypeHandler
        public abstract class JsonBaseTypeHandler<T> extends BaseTypeHandler<T> {
            private static final ObjectMapper objectMapper;
            static {
                objectMapper = new ObjectMapper();
                objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
                objectMapper.disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES);
                objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
            }
            private T parse(String json) {
                try {
                    if (StringUtils.isBlank(json)) {
                        return null;
                    }
                    return objectMapper.readValue(json, specificType());
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            private String toJsonString(T obj) {
                if (obj == null){
                    return "";
                }
                try {
                    return objectMapper.writeValueAsString(obj);
                } catch (JsonProcessingException e) {
                    throw new RuntimeException(e);
                }
            }
            @Override
            public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
                String content = parameter == null ? null : toJsonString(parameter);
                ps.setString(i, content);
            }
            @Override
            public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
                return this.parse(rs.getString(columnName));
            }
            @Override
            public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
                return this.parse(rs.getString(columnIndex));
            }
            @Override
            public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
                return this.parse(cs.getString(columnIndex));
            }
            protected abstract TypeReference<T> specificType();
        }
    c.具体的字段转换到相应的Java对象时,继承这个抽象类
        public class AddressTypeHandler extends JsonBaseTypeHandler<AddressBO> {
            @Override
            protected TypeReference<AddressBO> specificType() {
                return new TypeReference<AddressBO>() {};
            }
        }
    d.指定查询的ResultMap以及插入的sql中的address字段的TypeHandler为AddressTypeHandler的全路径
        <resultMap id="BaseResultMap" type="com.springboot.mybatis.entity.UserInfoDO">
          <id column="id"  property="id" />
          <result column="user_name" jdbcType="VARCHAR" property="userName" />
          <result column="address" jdbcType="VARCHAR" property="address" typeHandler="com.springboot.mybatis.handler.AddressTypeHandler"/>
          <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
        </resultMap>

        <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
          select *  from user_info where id = #{id,jdbcType=BIGINT}
        </select>

        <insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.springboot.mybatis.entity.UserInfoDO" useGeneratedKeys="true">
          insert into user_info (user_name, address, create_time
            )  values (#{userName,jdbcType=VARCHAR}, #{address,jdbcType=VARCHAR,typeHandler=com.springboot.mybatis.handler.AddressTypeHandler},
              #{createTime,jdbcType=TIMESTAMP}
            )
        </insert>
    e.UserInfoDO中的address属性由String修改为AddressBO
        @Data
        public class UserInfoDO {
            private Long id;
            private String userName;
            private AddressBO address;
            private Date createTime;
        }
    f.执行插入以及查询的方法
        @Test
        public void insertUserTest(){
            UserInfoDO userInfoDO = new UserInfoDO();
            userInfoDO.setUserName("lisi");
            AddressBO addressBO = new AddressBO();
            addressBO.setCity("New York");
            addressBO.setStreet("123 Main St");
            addressBO.setCountry("US");
            List<Integer> zipcodes = new ArrayList<>();
            zipcodes.add(94507);
            zipcodes.add(94582);
            addressBO.setZipcodes(zipcodes);
            userInfoDO.setAddress(addressBO);
            userInfoDO.setCreateTime(new Date());
            userInfoMapper.insert(userInfoDO);
        }

        @Test
        public void listUserTest(){
            UserInfoDO userInfoDO = userInfoMapper.selectByPrimaryKey(6L);
            System.out.println(userInfoDO);
        }

3.28 [4]字段类型:FastjsonTypeHandler

01.字段类型处理
    a.描述
        在某些场景下,实体类中使用Map集合,而数据库中使用JSON格式存储。字段类型处理器用于实现类型转换
    b.实现步骤
        在实体类中添加Map类型字段
        使用@TableField注解指定字段类型处理器
    c.代码示例
        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @TableName(autoResultMap = true)//查询时将json字符串封装为Map集合
        public class User extends Model<User> {

            @TableId(type = IdType.ASSIGN_UUID)  // 全局唯一标识符,定义为一个字符串主键,注意是字符串,所以数据表的主键要为字符串类型才行
            // 对应的 Java bean 对象当中的属性值,也要为 字符串类型
            private String id;
            private String name;
            private Integer age;
            private String email;

            @TableLogic(value = "1", delval = "0")  // 标注删除状态
            private Integer status;
            private GenderEnum gender;

            // @TableName(autoResultMap = true)//查询时将json字符串封装为Map集合
            @TableField(typeHandler = FastjsonTypeHandler.class)//指定字段类型处理器
            private Map<String, String> contact;  // 联系方式
        }
    d.引入Fastjson依赖
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
    e.测试类
        @SpringBootTest
        public class TypeHandLerTest {
            @Autowired
            private UserMapper userMapper;

            @Test
            void typeHandler() {
                User user = new User();
                user.setName("zhang3");
                user.setAge(28);
                user.setEmail("[email protected]");
                user.setStatus(1);
                HashMap<String, String> contact = new HashMap<>();
                contact.put("phone","010-1234567");
                contact.put("tel","13388889999");
                user.setContact(contact);
                userMapper.insert(user);
            }
        }

3.29 [4]字段类型:oracle返回字段大写

00.汇总
    双引号""
    resultType="com.demo.pojo.User"
    返回字段使用resultMap
    解决mybatis用Map返回的字段全变大写的问题(废弃)

01.双引号""
    a.说明
        如果resultType的对象使用Map,返回来的字段名全是大写
    b.解决
        // 为字段取一个别名,别名用双引号包起来,记住是双引号""
        select name as "name" from user_info;

02.resultType="com.demo.pojo.User"
    a.说明
        返回类型resultType映射到实体类型javaBean
    b.以“com.demo.pojo.User”为例
        设置select语句的返回类型resultType为:resultType="com.demo.pojo.User"

03.返回字段使用resultMap
    a.说明
        如果返回的是实体类,属性是resultType,如果返回的是map,属性是resultMap
    b.示例
       <resultMap id="mapDemo" type="java.util.map">
         <result column="userName" property="userName" />
         <result column="userAge" property="userAge" />
       </resultMap>

04.解决mybatis用Map返回的字段全变大写的问题(废弃)
    a.说明
        在 MyBatis 中,自定义转换器(TypeHandler)通常是全局配置的
        适用于所有数据源和 SQL 会话。MyBatis-Plus 的配置也是基于 MyBatis 的
        因此自定义转换器在 SqlSessionFactory 中配置后,会对所有使用该 SqlSessionFactory 的数据源生效
        -----------------------------------------------------------------------------------------------------
        全局自定义转换器:自定义转换器在 SqlSessionFactory 中配置后,会对所有数据源生效。这意味着无论你使用哪个数据源,都会应用相同的转换器
        特定数据源的自定义转换器:如果你需要为特定数据源配置不同的转换器,可以考虑为每个数据源创建不同的 SqlSessionFactory,并在其中配置不同的转换器
        使用 @DS 注解:@DS 注解用于动态数据源切换,但不直接影响自定义转换器的配置。它主要用于指定方法或类使用哪个数据源
        -----------------------------------------------------------------------------------------------------
        使用这种方式,注意【影响MP分页,以及其他SQL的识别】
    b.CamelCaseMapTypeHandler
        /**
         * MP:自定义TypeHandler,Oracle返回内容从【全大写】变成【全小写】
         */
        public class CamelCaseMapTypeHandler extends BaseTypeHandler<Map<String, Object>> {

            @Override
            public void setNonNullParameter(PreparedStatement ps, int i, Map<String, Object> parameter, JdbcType jdbcType) throws SQLException {
                // Implement if needed
            }

            @Override
            public Map<String, Object> getNullableResult(ResultSet rs, String columnName) throws SQLException {
                return convertToCamelCaseMap(rs);
            }

            @Override
            public Map<String, Object> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
                return convertToCamelCaseMap(rs);
            }

            @Override
            public Map<String, Object> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
                ResultSet rs = cs.getResultSet();
                return rs != null ? convertToCamelCaseMap(rs) : null;
            }

            private Map<String, Object> convertToCamelCaseMap(ResultSet rs) throws SQLException {
                Map<String, Object> resultMap = new HashMap<>();
                int columnCount = rs.getMetaData().getColumnCount();
                for (int i = 1; i <= columnCount; i++) {
                    String columnName = rs.getMetaData().getColumnName(i);
                    String camelCaseName = convertToCamelCase(columnName);
                    resultMap.put(camelCaseName, rs.getObject(i));
                }
                return resultMap;
            }

            private String convertToCamelCase(String columnName) {
                StringBuilder result = new StringBuilder();
                boolean nextUpperCase = false;
                for (char c : columnName.toCharArray()) {
                    if (c == '_') {
                        nextUpperCase = true;
                    } else {
                        if (nextUpperCase) {
                            result.append(Character.toUpperCase(c));
                            nextUpperCase = false;
                        } else {
                            result.append(Character.toLowerCase(c));
                        }
                    }
                }
                return result.toString();
            }
        }
    c.CustomMyBatisPlusConfig
        /**
         * MP:注册 TypeHandler
         */
        @Configuration
        @Order(1) // 确保这个配置类在 MybatisPlusSaasConfig 之后加载
        @MapperScan("cn.jggroup.**.mapper*")
        public class CustomMyBatisPlusConfig extends MybatisPlusSaasConfig {

            @Autowired
            private DataSource dataSource;

            @Bean
            public SqlSessionFactory sqlSessionFactory() throws Exception {
                SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
                sqlSessionFactoryBean.setDataSource(dataSource); // 设置数据源
                // 使用 MyBatis 的默认配置
        //        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        //        configuration.setMapUnderscoreToCamelCase(true); // 启用下划线转驼峰
        //        sqlSessionFactoryBean.setConfiguration(configuration);

                // 注册 自定义TypeHandler,Oracle返回内容从【全大写】变成【全小写】
                sqlSessionFactoryBean.setTypeHandlers(new CamelCaseMapTypeHandler());
                sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:cn/jggroup/device/mapper/xml/*.xml"));
                return sqlSessionFactoryBean.getObject();
            }
        }

3.30 [5]表名前缀:@TableName

00.MyBatisPlus表名前缀
    a.回答
        配置文件:mybatis-plus.global-config.db-config.table-prefix
        实体类:@TableName自定义表名
    b.说明
        在查询和操作时,系统会自动为表名添加指定的前缀,可以在实体类中使用@TableName 注解自定义表名

01.在配置文件中设置
    mybatis-plus.global-config.db-config.table-prefix=prefix_

02.使用表名映射
    @TableName("prefix_user")
    public class User {
        private Long id;
        private String name;
        // 其他字段和方法
    }

3.31 [5]对应关系:@TableField

00.区别:@TableField、@TableId
    a.@TableId
        用于主键字段的映射,指定表中的主键列
    b.@TableField
        用于非主键字段的映射,灵活配置字段属性

01.什么是@TableField
    a.定义
        @TableField 是MyBatisPlus中的一个注解,用来解决实体类字段和数据库表字段之间的映射问题
        它帮助处理字段命名不一致或不需要映射的字段
    b.用途
        当数据库字段名和实体类属性名不一致时,或者有字段不想映射到数据库时,使用@TableField
    c.示例
        数据库字段命名使用下划线风格,而Java中使用驼峰命名法时,@TableField可以适配

02.基本用法
    a.value属性
        a.说明
            在 @TableField 注解中,最常用的属性是value,用于指定数据库中与实体类字段对应的列名
        b.数据库中有字段user_name,实体类中使用userName
            @TableField("user_name")
            private String userName;

03.应用场景
    a.基本映射:数据库字段名与实体类字段名不一致
        a.数据库表结构
            CREATE TABLE user (
                id BIGINT(20) NOT NULL AUTO_INCREMENT,
                user_name VARCHAR(30) NOT NULL,
                age INT(11),
                email VARCHAR(50),
                PRIMARY KEY (id)
            );
        b.实体类映射
            public class User {
                private Long id;

                @TableField("user_name")
                private String userName;

                private Integer age;
                private String email;
            }
    b.忽略字段:不映射数据库中的某些字段
        a.示例
            public class User {
                private Long id;

                @TableField("user_name")
                private String userName;

                private Integer age;
                private String email;

                @TableField(exist = false) // 此字段不映射到数据库
                private String temporaryData;
            }
    c.插入或更新时忽略字段
        a.示例
            @TableField(fill = FieldFill.INSERT)
            private LocalDateTime createTime;

04.动态选择字段:@TableField(select = false)
    a.@TableField(select = false)
        a.说明
            有些字段不希望在查询时被默认查出,比如密码字段,可以用来排除
        b.示例
            @TableField(select = false)
            private String password;

05.自动填充字段
    a.示例
        @TableField(fill = FieldFill.INSERT_UPDATE)
        private LocalDateTime updateTime;
    b.MP注入处理器
        @Slf4j
        public class InjectionMetaObjectHandler implements MetaObjectHandler {

            @Override
            public void insertFill(MetaObject object) {
                try {
                    if (ObjectUtil.isNotNull(object) && object.getOriginalObject() instanceof BaseEntity) {
                        BaseEntity entity = (BaseEntity) object.getOriginalObject();
                        Date date = ObjectUtil.isNotNull(entity.getCreateTime()) ? entity.getCreateTime() : new Date();
                        String username = StrUtil.isNotBlank(entity.getCreateBy())
                                ? entity.getCreateBy() : LoginHelper.getNickNameNe();
                        entity.setCreateBy(username);
                        entity.setUpdateBy(username);
                        entity.setCreateTime(date);
                        entity.setUpdateTime(date);
                    }
                } catch (Exception e) {
                    throw new ServiceException("新增自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
                }
            }

            @Override
            public void updateFill(MetaObject object) {
                try {
                    if (ObjectUtil.isNotNull(object) && object.getOriginalObject() instanceof BaseEntity) {
                        BaseEntity entity = (BaseEntity) object.getOriginalObject();
                        entity.setUpdateBy(LoginHelper.getUsernameNe());
                        entity.setUpdateTime(new Date());
                    }
                } catch (Exception e) {
                    throw new ServiceException("修改自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
                }
            }
        }

06.动态查询控制
    a.@TableField(condition = SqlCondition.LIKE)
        a.说明
             如果有些字段只在特定条件下查询,我们可以通过@TableField(condition = SqlCondition.LIKE)设置字段的查询条件
        b.示例
            @TableField(condition = SqlCondition.LIKE)
            private String email;

3.32 [5]主键自增:@TableId

00.MybatisPlus时自增id为什么不是从1开始?
    a.error原因
        默认采用:雪花算法生成ID策略
    b.解决:使用数据库自增ID作为主键
        a.方式1
            @TableId(type = IdType.AUTO)
            private Integer id;
        b.方式2
            mybatis-plus:
              global-config:
                db-config:
                  #主键类型  0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)";
                  id-type: AUT0
                  # 默认数据库表下划线命名
                  table-underline: true

01.MySQL:默认支持自增主键
    a.表设计
        CREATE TABLE user (
            id BIGINT AUTO_INCREMENT PRIMARY KEY,
            name VARCHAR(100)
        );
    b.XML:使用 useGeneratedKeys 和 keyProperty 属性
        <insert id="insert" parameterType="Person" useGeneratedKeys="true" keyProperty="id">
            INSERT INTO person(name, pswd)
            VALUE (#{name}, #{pswd})
        </insert>
    c.实体类:使用 @TableId 注解标记主键,并设置 type 为 IdType.AUTO
        import com.baomidou.mybatisplus.annotation.IdType;
        import com.baomidou.mybatisplus.annotation.TableId;

        public class User {
            @TableId(type = IdType.AUTO) // 设置主键自增
            private Long id;
            private String name;
            // 其他字段和方法
        }
    d.插入数据:当插入新记录时,无需手动设置主键字段的值,数据库会自动生成
        User user = new User();
        user.setName("Tom");
        userMapper.insert(user); // id 会自动生成

02.Oracle:不支持自增字段,需要通过序列(Sequence)和触发器(Trigger)来模拟自增
    a.使用序列(Sequence)
        a.创建序列
            使用 SQL 语句创建一个序列,用于生成主键值。
            CREATE SEQUENCE user_seq
            START WITH 1
            INCREMENT BY 1
            NOCACHE; -- 可选,表示不缓存序列值
        b.创建表
            创建表时,不需要设置自增,主键值在插入时通过序列获取。
            CREATE TABLE user (
                id NUMBER PRIMARY KEY,
                name VARCHAR2(100)
            );
        c.插入数据
            在插入记录时,通过调用序列的 NEXTVAL 来获取下一个主键值。
            INSERT INTO user (id, name) VALUES (user_seq.NEXTVAL, 'Tom');
    b.使用触发器(Trigger)
        a.创建触发器
            CREATE OR REPLACE TRIGGER user_trigger
            BEFORE INSERT ON user
            FOR EACH ROW
            BEGIN
                :NEW.id := user_seq.NEXTVAL; -- 使用序列的下一个值
            END;
        b.插入数据
            现在可以在插入时不需要显式地指定主键值。
            INSERT INTO user (name) VALUES ('Tom'); -- id 自动赋值

3.33 [5]逻辑删除:@TableLogic

00.MyBatisPlus逻辑删除
    a.回答
        deleted字段,标识@TableLogic
    b.说明
        MyBatis Plus 通过在实体类中添加逻辑删除字段(如deleted)并使用@TableLogic来实现逻辑删除
        执行删除操作时,MyBatisPlus会将该字段设置为“已删除”状态(通常是1),而不是物理删除数据
        在查询时,逻辑删除的数据会被自动过滤,不会返回给用户,这样可以有效地保留数据

01.常用信息
    a.添加逻辑删除字段:在实体类中添加一个字段,通常为 is_deleted 或 deleted,类型为 Integer 或 Boolean
        public class User {
            private Long id;
            private String name;

            @TableLogic // 表示逻辑删除字段
            private Integer deleted; // 0: 未删除, 1: 已删除
        }
    b.启用逻辑删除
        在 MyBatis Plus 的配置类中,确保逻辑删除功能启用,通常不需要额外配置
    c.执行删除操作
        使用 deleteById 或 delete 方法时,MyBatis Plus 会自动将逻辑删除标记,而不是物理删除记录
        userMapper.deleteById(1L); // 实际上是将 deleted 字段设置为 1
    d.查询时过滤已删除数据
        查询时,逻辑删除的数据不会被返回,MyBatis Plus 自动处理

02.逻辑删除
    a.描述
        在实际开发中,数据通常不会被完全删除,而是通过逻辑删除来标记数据的状态
        逻辑删除通过增加一个字段表示数据的状态,1表示可用,0表示不可用
    b.实现步骤
        在表中增加一个字段表示删除状态
        实体类添加一个字段用于对应表中的字段
        使用@TableLogic注解标注删除状态
    c.代码示例
        import com.baomidou.mybatisplus.annotation.TableLogic;
        import com.baomidou.mybatisplus.annotation.TableId;
        import com.baomidou.mybatisplus.annotation.IdType;
        import com.baomidou.mybatisplus.annotation.TableName;
        import lombok.Data;
        import lombok.NoArgsConstructor;
        import lombok.AllArgsConstructor;

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @TableName(autoResultMap = true)
        public class User {
            @TableId(type = IdType.ASSIGN_UUID)
            private String id;
            private String name;
            private Integer age;
            private String email;
            @TableLogic(value = "1", delval = "0")
            private Integer status;
        }
    d.测试逻辑删除
        删除操作将数据状态由1变为0
        查询时自动过滤status=0的数据
    e.全局配置
        mybatis-plus:
          global-config:
            db-config:
              logic-delete-value: 1
              logic-delete-field: status
              logic-not-delete-value: 0

3.34 [5]通用枚举:GenderEnum

01.通用枚举
    a.描述
        枚举用于表示一组固定的值,例如性别。通过枚举可以更好地管理这些固定值
    b.实现步骤
        在表中添加一个字段表示枚举类型
        定义枚举类并使用@EnumValue注解
    c.代码示例
        package com.rainbowsea.enums;

        import com.baomidou.mybatisplus.annotation.EnumValue;

        public enum GenderEnum {
            MAN(0,"男"),
            WOMAN(1,"女");

            @EnumValue
            private Integer gender;
            @EnumValue
            private String genderName;

            GenderEnum(Integer gender, String genderName) {
                this.gender = gender;
                this.genderName = genderName;
            }
        }
    d.实体类添加相关字段
        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @TableName(autoResultMap = true)//查询时将json字符串封装为Map集合
        public class User extends Model<User> {

            @TableId(type = IdType.ASSIGN_UUID)  // 全局唯一标识符,定义为一个字符串主键,注意是字符串,所以数据表的主键要为字符串类型才行
            private String id;
            private String name;
            private Integer age;
            private String email;

            @TableLogic(value = "1", delval = "0")  // 标注删除状态
            private Integer status;

            // 自定义枚举
            private GenderEnum gender;
        }
    c.测试
        @SpringBootTest
        public class enumTest {

            @Autowired
            private UserMapper userMapper;

            @Test
            void enumTest() {
                User user = new User();
                user.setName("LiHu2");
                user.setAge(18);
                user.setEmail("[email protected]");
                user.setStatus(1);
                user.setGender(GenderEnum.WOMAN);
                System.out.println(GenderEnum.WOMAN);
                userMapper.insert(user);
            }
        }

3.35 [6]字段问题:更新null

00.汇总
    解决MybatisPlus updateById 更新数据时,将 没传的数据 也更新成了 null

01.问题描述
    在日常项目开发过程中,经常会使用Mybatis-plus的updateById()方法
    快速将接收到的参数或者查询结果中原本不为null的字段更新为null
    并且该字段在数据库中可为null,这个时候使用updateById()并不能实现这个操作,不会报错
    但是对应的字段并没有更新为null

02.Mybatis-plus的字段策略(FieldStrategy)有三种策略:
    IGNORED     0 忽略
    NOT_NULL    1 非 NULL,默认策略,通过接口更新数据时数据为NULL值时将不更新进数据库
    NOT_EMPTY   2 非空

03.解决方案
    a.直接在mapper.xml中写sql
         update table A set 字段a = null where 字段b = 条件
    b.设置全局的FieldStrategy
        #yml文件格式:
        mybatis-plus:
          global-config:
            #字段策略 0:"忽略判断",1:"非 NULL 判断",2:"非空判断"
            field-strategy: 0
    c.对指定的字段单独设置field-strategy
        @TableField(updateStrategy = FieldStrategy.IGNORED)
        private String updateBy;
    d.推荐:使用update方法结合UpdateWrapper方式更新
        不会影响其它方法,不需要修改全局配置,也不需要在字段上单独加注解
        只需要在使用的时候设置一下要修改的字段为null就可以更新成功
        -----------------------------------------------------------------------------------------------------
        User user=userService.lambdaQuery().eq(User::getUserId,userId).one();
        if(user!=null){
            userService.update(user,new UpdateWrapper<User>().lambda()
                       .set(User::getUserName,null)
                       .eq(User::getUserId,user.getUserId()));
        }

3.36 [6]字段问题:自动插入

01.自动填充1
    a.描述
        自动填充用于在插入或更新时自动填充某些字段,例如创建时间和更新时间
    b.实现步骤
        在实体类中添加需要自动填充的字段
        使用@TableField注解指定填充时机
    c.配置
        mybatis-plus:
          configuration:
            log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 开启 Log 日志信息打印
            map-underscore-to-camel-case: true # 开启驼峰,下划线映射规则
    d.代码示例
        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @TableName(autoResultMap = true)//查询时将json字符串封装为Map集合
        public class User extends Model<User> {
            @TableId(type = IdType.ASSIGN_UUID)  // 全局唯一标识符,定义为一个字符串主键,注意是字符串,所以数据表的主键要为字符串类型才行
            // 对应的 Java bean 对象当中的属性值,也要为 字符串类型
            private String id;
            private String name;
            private Integer age;
            private String email;
            @TableLogic(value = "1", delval = "0")  // 标注删除状态
            private Integer status;
            private GenderEnum gender;
            @TableField(typeHandler = FastjsonTypeHandler.class)//指定字段类型处理器
            private Map<String, String> contact;  // 联系方式


            @TableField(fill = FieldFill.INSERT)
            private Date createTime;
            @TableField(fill = FieldFill.INSERT_UPDATE)
            private Date updateTime;
        }
    e.编写自动填充处理器,实现MetaObjectHandler
        package com.rainbowsea.handler;

        import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
        import org.apache.ibatis.reflection.MetaObject;
        import org.springframework.stereotype.Component;

        import java.util.Date;


        @Component
        public class MyMetaHandler implements MetaObjectHandler {
            @Override
            public void insertFill(MetaObject metaObject) {
                setFieldValByName("createTime",new Date(),metaObject);
                setFieldValByName("updateTime",new Date(),metaObject);
            }

            @Override
            public void updateFill(MetaObject metaObject) {
                setFieldValByName("updateTime",new Date(),metaObject);
            }
        }
    f.测试插入
        @SpringBootTest
        public class FillTest {

            @Autowired
            private UserMapper userMapper;

            @Test
            void testFillInsert() {
                User user = new User();
                user.setName("LiHu666");
                user.setAge(18);
                user.setEmail("[email protected]");
                user.setStatus(1);

                userMapper.insert(user);
            }
        }
    g.测试更新
        @SpringBootTest
        public class FillTest {

            @Autowired
            private UserMapper userMapper;

            @Test
            void testFillUpdate() {
                //1837826196875464706
                User user = new User();
                user.setName("LiHu666");
                user.setId("1837826196875464706");
                user.setAge(18);
                user.setEmail("[email protected]");
                user.setStatus(1);

                userMapper.updateById(user);

            }
        }

02.自动填充2
    a.项目环境配置
        a.依赖
            <dependencies>
                <!-- MyBatis-Plus -->
                <dependency>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-boot-starter</artifactId>
                    <version>3.5.1</version>
                </dependency>
                <!-- 其他必要依赖 -->
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </dependency>
                <!-- MySQL 驱动 -->
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <scope>runtime</scope>
                </dependency>
            </dependencies>
        b.配置数据源
            spring:
              datasource:
                url: jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
                username: your_username
                password: your_password
                driver-class-name: com.mysql.cj.jdbc.Driver

            mybatis-plus:
              mapper-locations: classpath:/mapper/*.xml
              type-aliases-package: com.example.entity
              global-config:
                db-config:
                  id-type: auto
    b.创建基础实体类
        package com.example.utils;

        import com.baomidou.mybatisplus.annotation.FieldFill;
        import com.baomidou.mybatisplus.annotation.TableField;
        import com.fasterxml.jackson.annotation.JsonIgnore;
        import com.fasterxml.jackson.annotation.JsonInclude;
        import lombok.Data;

        import java.io.Serializable;
        import java.util.Date;
        import java.util.HashMap;
        import java.util.Map;

        @Data
        public class BaseEntity implements Serializable {

            private static final long serialVersionUID = 1L;

            /**
             * 搜索值(暂时忽略)
             */
            @JsonIgnore
            @TableField(exist = false)
            private String searchValue;

            /**
             * 创建者
             */
            @TableField(fill = FieldFill.INSERT)
            private String createBy;

            /**
             * 创建时间
             */
            @TableField(fill = FieldFill.INSERT)
            private Date createTime;

            /**
             * 更新者
             */
            @TableField(fill = FieldFill.INSERT_UPDATE)
            private String updateBy;

            /**
             * 更新时间
             */
            @TableField(fill = FieldFill.INSERT_UPDATE)
            private Date updateTime;

            /**
             * 请求参数(暂时忽略)
             */
            @JsonInclude(JsonInclude.Include.NON_EMPTY)
            @TableField(exist = false)
            private Map<String, Object> params = new HashMap<>();

        }
    c.实现 MetaObjectHandler 接口
        a.创建 MyMetaObjectHandler 类
            package com.example.config;

            import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
            import org.apache.ibatis.reflection.MetaObject;
            import org.springframework.stereotype.Component;

            import java.util.Date;

            @Component
            public class MyMetaObjectHandler implements MetaObjectHandler {

                @Override
                public void insertFill(MetaObject metaObject) {
                    // 填充创建时间和更新时间
                    this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
                    this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
                    // 可以根据业务需求获取当前用户,填充创建者和更新者
                    this.strictInsertFill(metaObject, "createBy", String.class, getCurrentUser());
                    this.strictInsertFill(metaObject, "updateBy", String.class, getCurrentUser());
                }

                @Override
                public void updateFill(MetaObject metaObject) {
                    // 更新时只填充更新时间和更新者
                    this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
                    this.strictUpdateFill(metaObject, "updateBy", String.class, getCurrentUser());
                }

                // 示例方法:获取当前用户信息
                private String getCurrentUser() {
                    // 这里可以集成 Spring Security 或其他上下文获取实际的当前用户
                    return "system"; // 这是一个占位符,实际应用中会替换为真实用户信息
                }
            }
        b.配置生效
            通过在 MyMetaObjectHandler 类上使用 @Component 注解,Spring 会自动管理这个类,并在实体插入和更新时调用它来填充字段
    d.实体类继承 BaseEntity
        package com.example.entity;

        import com.baomidou.mybatisplus.annotation.TableName;
        import com.example.utils.BaseEntity;
        import lombok.Data;

        @Data
        @TableName("your_table")
        public class YourEntity extends BaseEntity {
            // 其他字段
            private String name;
            private Integer age;
        }
    e.处理数据插入与更新
        a.Service 层示例
            @Service
            public class YourEntityService {

                @Autowired
                private YourEntityMapper yourEntityMapper;

                public void saveEntity(YourEntity entity) {
                    yourEntityMapper.insert(entity);
                }

                public void updateEntity(YourEntity entity) {
                    yourEntityMapper.updateById(entity);
                }
            }
        b.验证自动填充功能
            通过简单的单元测试或集成测试,验证 createTime 和 updateTime 字段在插入和更新时是否正确自动填充
    f.其他注意事项
        a.字段类型
            确保数据库中的 createTime 和 updateTime 字段类型与 Java 实体类中的类型相匹配(通常是 DATETIME 类型)
        b.时间格式
            如果需要统一时间格式,可以在配置文件中设置 Spring Boot 的全局时间格式,或使用 @JsonFormat 注解指定序列化时的格式
            spring:
              jackson:
                date-format: yyyy-MM-dd HH:mm:ss
                time-zone: GMT+8

3.37 [6]字段问题:自动更新

00.描述
    a.自动插入
        当订单创建时,需要设置insert_by,insert_time,update_by,update_time的值
    b.自动更新
        在进行订单状态更新时,则只需要更新update_by,update_time的值

01.普通做法
    a.订单创建
        public void create(Order order){
            // ...其他代码
            // 设置审计字段
            Date now = new Date();
            order.setInsertBy(appContext.getUser());
            order.setUpdateBy(appContext.getUser());
            order.setInsertTime(now);
            order.setUpdateTime(now);
            orderDao.insert(order);
        }
    b.订单更新
        public void update(Order order){
            // ...其他代码

            // 设置审计字段
            Date now = new Date();
            order.setUpdateBy(appContext.getUser());
            order.setUpdateTime(now);
            orderDao.insert(order);
        }
    c.问题
        需要在每个方法中按照不同的业务逻辑决定设置哪些字段
        在业务模型变多后,每个模型的业务方法中都要进行设置,重复代码太多

02.拦截器
    a.自定义拦截器
        自定义Interceptor最重要的是要实现plugin方法和intercept方法
        在plugin方法中我们可以决定是否要进行拦截进而决定要返回一个什么样的目标对象
        在intercept方法就是要进行拦截的时候要执行的方法
        对于plugin方法而言,其实Mybatis已经为我们提供了一个实现
        Mybatis中有一个叫做Plugin的类,里面有一个静态方法wrap(Object target,Interceptor interceptor)
        通过该方法可以决定要返回的对象是目标对象还是对应的代理
    b.可以定义一个接口,需要更新审计字段的模型都统一实现该该接口
        public interface BaseDO {
        }

        public class Order implements BaseDO{

            private Long orderId;

            private String orderNo;

            private Integer orderStatus;

            private String insertBy;

            private String updateBy;

            private Date insertTime;

            private Date updateTime;
            //... getter ,setter
        }
    c.自定义拦截器
        @Component("ibatisAuditDataInterceptor")
        @Intercepts({@Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class})})
        public class IbatisAuditDataInterceptor implements Interceptor {

            private Logger logger = LoggerFactory.getLogger(IbatisAuditDataInterceptor.class);

            @Override
            public Object intercept(Invocation invocation) throws Throwable {
                // 从上下文中获取用户名
                String userName = AppContext.getUser();

                Object[] args = invocation.getArgs();
                SqlCommandType sqlCommandType = null;

                for (Object object : args) {
                    // 从MappedStatement参数中获取到操作类型
                    if (object instanceof MappedStatement) {
                        MappedStatement ms = (MappedStatement) object;
                        sqlCommandType = ms.getSqlCommandType();
                        logger.debug("操作类型: {}", sqlCommandType);
                        continue;
                    }
                    // 判断参数是否是BaseDO类型
                    // 一个参数
                    if (object instanceof BaseDO) {
                        if (SqlCommandType.INSERT == sqlCommandType) {
                            Date insertTime = new Date();
                            BeanUtils.setProperty(object, "insertedBy", userName);
                            BeanUtils.setProperty(object, "insertTimestamp", insertTime);
                            BeanUtils.setProperty(object, "updatedBy", userName);
                            BeanUtils.setProperty(object, "updateTimestamp", insertTime);
                            continue;
                        }
                        if (SqlCommandType.UPDATE == sqlCommandType) {
                            Date updateTime = new Date();
                            BeanUtils.setProperty(object, "updatedBy", userName);
                            BeanUtils.setProperty(object, "updateTimestamp", updateTime);
                            continue;
                        }
                    }
                    // 兼容MyBatis的updateByExampleSelective(record, example);
                    if (object instanceof ParamMap) {
                        logger.debug("mybatis arg: {}", object);
                        @SuppressWarnings("unchecked")
                        ParamMap<Object> parasMap = (ParamMap<Object>) object;
                        String key = "record";
                        if (!parasMap.containsKey(key)) {
                            continue;
                        }
                        Object paraObject = parasMap.get(key);
                        if (paraObject instanceof BaseDO) {
                            if (SqlCommandType.UPDATE == sqlCommandType) {
                                Date updateTime = new Date();
                                BeanUtils.setProperty(paraObject, "updatedBy", userName);
                                BeanUtils.setProperty(paraObject, "updateTimestamp", updateTime);
                                continue;
                            }
                        }
                    }
                    // 兼容批量插入
                    if (object instanceof DefaultSqlSession.StrictMap) {
                        logger.debug("mybatis arg: {}", object);
                        @SuppressWarnings("unchecked")
                        DefaultSqlSession.StrictMap<ArrayList<Object>> map = (DefaultSqlSession.StrictMap<ArrayList<Object>>) object;
                        String key = "collection";
                        if (!map.containsKey(key)) {
                            continue;
                        }
                        ArrayList<Object> objs = map.get(key);
                        for (Object obj : objs) {
                            if (obj instanceof BaseDO) {
                                if (SqlCommandType.INSERT == sqlCommandType) {
                                    Date insertTime = new Date();
                                    BeanUtils.setProperty(obj, "insertedBy", userName);
                                    BeanUtils.setProperty(obj, "insertTimestamp", insertTime);
                                    BeanUtils.setProperty(obj, "updatedBy", userName);
                                    BeanUtils.setProperty(obj, "updateTimestamp", insertTime);
                                }
                                if (SqlCommandType.UPDATE == sqlCommandType) {
                                    Date updateTime = new Date();
                                    BeanUtils.setProperty(obj, "updatedBy", userName);
                                    BeanUtils.setProperty(obj, "updateTimestamp", updateTime);
                                }
                            }
                        }
                    }
                }
                return invocation.proceed();
            }

            @Override
            public Object plugin(Object target) {
                return Plugin.wrap(target, this);
            }

            @Override
            public void setProperties(Properties properties) {
            }
        }
        -----------------------------------------------------------------------------------------------------
        通过上面的代码可以看到,我们自定义的拦截器IbatisAuditDataInterceptor实现了Interceptor接口
        在我们拦截器上的@Intercepts注解,type参数指定了拦截的类是Executor接口的实现
        method 参数指定拦截Executor中的update方法,因为数据库操作的增删改操作都是通过update方法执行
    d.配置拦截器插件
        <bean id="transSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
            <property name="dataSource" ref="transDataSource" />
            <property name="mapperLocations">
                <array>
                    <value>classpath:META-INF/mapper/*.xml</value>
                </array>
            </property>
            <property name="plugins">
                <array>
                    <!-- 处理审计字段 -->
                    <ref bean="ibatisAuditDataInterceptor" />
                </array>
            </property>

4 常见应用

4.1 [1]优化:12个

00.汇总
    01.避免使用isNull判断
    02.明确Select字段
    03.批量操作方法替代循环
    04.Exists方法子查询
    05.使用orderBy代替last
    06.使用LambdaQuery确保类型安全
    07.用between代替ge和le
    08.排序字段注意索引
    09.分页参数设置
    10.条件构造处理NulI值
    11.查询性能追踪
    12.枚举类型映射
    13.自动处理逻辑删除
    14.乐观锁更新保护
    15.递增和递减:setlncrBy和setDecrBy

01.避免使用isNull判断
    a.代码示例
        // ❌ 不推荐
        LambdaQueryWrapper<User> wrapper1 = new LambdaQueryWrapper<>();
        wrapper1.isNull(User::getStatus);

        // ✅ 推荐:使用具体的默认值
        LambdaQueryWrapper<User> wrapper2 = new LambdaQueryWrapper<>();
        wrapper2.eq(User::getStatus, UserStatusEnum.INACTIVE.getCode());
    b.原因
        使用具体的默认值可以提高代码的可读性和维护性
        NULL值会使索引失效,导致MySQL无法使用索引进行查询优化
        NULL值的比较需要特殊的处理逻辑,增加了CPU开销
        NULL值会占用额外的存储空间,影响数据压缩效率

02.明确Select字段
    a.代码示例
        // ❌ 不推荐
        // 默认select 所有字段
        List<User> users1 = userMapper.selectList(null);

        // ✅ 推荐:指定需要的字段
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.select(User::getId, User::getName, User::getAge);
        List<User> users2 = userMapper.selectList(wrapper);
    b.原因
        避免大量无用字段的网络传输开销
        可以利用索引覆盖,避免回表查询
        减少数据库解析和序列化的负担
        降低内存占用,特别是在大量数据查询时

03.批量操作方法替代循环
    a.代码示例
        // ❌ 不推荐
        for (User user : userList) {
            userMapper.insert(user);
        }

        // ✅ 推荐
        userService.saveBatch(userList, 100);  // 每批次处理100条数据

        // ✅ 更优写法:自定义批次大小
        userService.saveBatch(userList, BatchConstants.BATCH_SIZE);
    b.原因
        减少数据库连接的创建和销毁开销
        批量操作可以在一个事务中完成,提高数据一致性
        数据库可以优化批量操作的执行计划
        显著减少网络往返次数,提升吞吐量

04.Exists方法子查询
    a.代码示例
        // ❌ 不推荐
        wrapper.inSql("user_id", "select user_id from order where amount > 1000");

        // ✅ 推荐
        wrapper.exists("select 1 from order where order.user_id = user.id and amount > 1000");

        // ✅ 更优写法:使用LambdaQueryWrapper
        wrapper.exists(orderService.lambdaQuery()
            .gt(Order::getAmount, 1000)
            .apply("order.user_id = user.id"));
    b.原因
        EXISTS是基于索引的快速查询,可以使用到索引
        EXISTS在找到第一个匹配项就会停止扫描
        IN子查询需要加载所有数据到内存后再比较
        当外表数据量大时,EXISTS的性能优势更明显

05.使用orderBy代替last
    a.代码示例
        // ❌ 不推荐:SQL注入风险
        wrapper.last("ORDER BY " + sortField + " " + sortOrder);

        // ❌ 不推荐:直接字符串拼接
        wrapper.last("ORDER BY FIELD(status, 'active', 'pending', 'inactive')");

        // ✅ 推荐:使用 Lambda 安全排序
        wrapper.orderBy(true, true, User::getStatus);

        // ✅ 推荐:多字段排序示例
        wrapper.orderByAsc(User::getStatus)
               .orderByDesc(User::getCreateTime);
    b.原因
        直接拼接SQL容易导致SQL注入攻击
        动态SQL可能破坏SQL语义完整性
        影响SQL语句的可维护性和可读性
        last会绕过MyBatis-Plus的安全检查机制

06.使用LambdaQuery确保类型安全
    a.代码示例
        // ❌ 不推荐:字段变更后可能遗漏
        QueryWrapper<User> wrapper1 = new QueryWrapper<>();
        wrapper1.eq("name", "张三").gt("age", 18);

        // ✅ 推荐
        LambdaQueryWrapper<User> wrapper2 = new LambdaQueryWrapper<>();
        wrapper2.eq(User::getName, "张三")
                .gt(User::getAge, 18);

        // ✅ 更优写法:使用链式调用
        userService.lambdaQuery()
            .eq(User::getName, "张三")
            .gt(User::getAge, 18)
            .list();
    b.原因
        编译期类型检查,避免字段名拼写错误
        IDE可以提供更好的代码补全支持
        重构时能自动更新字段引用
        提高代码的可维护性和可读性

07.用between代替ge和le
    a.代码示例
        // ❌ 不推荐
        wrapper.ge(User::getAge, 18)
               .le(User::getAge, 30);

        // ✅ 推荐
        wrapper.between(User::getAge, 18, 30);

        // ✅ 更优写法:条件动态判断
        wrapper.between(ageStart != null && ageEnd != null,
                       User::getAge, ageStart, ageEnd);
    b.原因
        生成的SQL更简洁,减少解析开销
        数据库优化器可以更好地处理范围查询
        代码更易读,语义更清晰
        减少重复编写字段名的机会

08.排序字段注意索引
    a.代码示例
        // ❌ 不推荐
        // 假设lastLoginTime无索引
        wrapper.orderByDesc(User::getLastLoginTime);

        // ✅ 推荐
        // 主键排序
        wrapper.orderByDesc(User::getId);

        // ✅ 更优写法:组合索引排序
        wrapper.orderByDesc(User::getStatus)  // status建立了索引
               .orderByDesc(User::getId);     // 主键排序
    b.原因
        索引天然具有排序特性,可以避免额外的排序操作
        无索引排序会导致文件排序,极大影响性能
        当数据量大时,内存排序可能导致溢出
        利用索引排序可以实现流式读取

09.分页参数设置
    a.代码示例
        // ❌ 不推荐
        wrapper.last("limit 1000");  // 一次查询过多数据

        // ✅ 推荐
        Page<User> page = new Page<>(1, 10);
        userService.page(page, wrapper);

        // ✅ 更优写法:带条件的分页查询
        Page<User> result = userService.lambdaQuery()
            .eq(User::getStatus, "active")
            .page(new Page<>(1, 10));
    b.原因
        控制单次查询的数据量,避免内存溢出
        提高首屏加载速度,优化用户体验
        减少网络传输压力
        数据库资源利用更合理

10.条件构造处理Null值
    a.代码示例
        // ❌ 不推荐
        if (StringUtils.isNotBlank(name)) {
            wrapper.eq("name", name);
        }
        if (age != null) {
            wrapper.eq("age", age);
        }

        // ✅ 推荐
        wrapper.eq(StringUtils.isNotBlank(name), User::getName, name)
               .eq(Objects.nonNull(age), User::getAge, age);

        // ✅ 更优写法:结合业务场景
        wrapper.eq(StringUtils.isNotBlank(name), User::getName, name)
               .eq(Objects.nonNull(age), User::getAge, age)
               .eq(User::getDeleted, false)  // 默认查询未删除记录
               .orderByDesc(User::getCreateTime);  // 默认按创建时间倒序
    b.原因
        优雅处理空值,避免无效条件
        减少代码中的if-else判断
        提高代码可读性
        防止生成冗余的SQL条件

11.查询性能追踪
    a.代码示例
        // ❌ 不推荐:简单计时,代码冗余
        public List<User> listUsers(QueryWrapper<User> wrapper) {
            long startTime = System.currentTimeMillis();
            List<User> users = userMapper.selectList(wrapper);
            long endTime = System.currentTimeMillis();
            log.info("查询耗时:{}ms", (endTime - startTime));
            return users;
        }

        // ✅ 推荐:使用 Try-with-resources 自动计时
        public List<User> listUsersWithPerfTrack(QueryWrapper<User> wrapper) {
            try (PerfTracker.TimerContext ignored = PerfTracker.start()) {
                return userMapper.selectList(wrapper);
            }
        }

        // 性能追踪工具类
        @Slf4j
        public class PerfTracker {
            private final long startTime;
            private final String methodName;

            private PerfTracker(String methodName) {
                this.startTime = System.currentTimeMillis();
                this.methodName = methodName;
            }

            public static TimerContext start() {
                return new TimerContext(Thread.currentThread().getStackTrace()[2].getMethodName());
            }

            public static class TimerContext implements AutoCloseable {
                private final PerfTracker tracker;

                private TimerContext(String methodName) {
                    this.tracker = new PerfTracker(methodName);
                }

                @Override
                public void close() {
                    long executeTime = System.currentTimeMillis() - tracker.startTime;
                    if (executeTime > 500) {
                        log.warn("慢查询告警:方法 {} 耗时 {}ms", tracker.methodName, executeTime);
                    }
                }
            }
        }
    b.原因
        业务代码和性能监控代码完全分离
        try-with-resources 即使发生异常,close() 方法也会被调用,确保一定会记录耗时
        不需要手动管理计时的开始和结束
        更优雅

12.枚举类型映射
    a.代码示例
        // 定义枚举
        public enum UserStatusEnum {
            NORMAL(1, "正常"),
            DISABLED(0, "禁用");

            @EnumValue  // MyBatis-Plus注解
            private final Integer code;
            private final String desc;
        }

        // ✅ 推荐:自动映射
        public class User {
            private UserStatusEnum status;
        }

        // 查询示例
        userMapper.selectList(
            new LambdaQueryWrapper<User>()
                .eq(User::getStatus, UserStatusEnum.NORMAL)
        );
    b.原因
        类型安全
        自动处理数据库和枚举转换
        避免魔法值
        代码可读性更强

13.自动处理逻辑删除
    a.代码示例
        @TableLogic  // 逻辑删除注解
        private Integer deleted;

        // ✅ 推荐:自动过滤已删除数据
        public List<User> getActiveUsers() {
            return userMapper.selectList(null);  // 自动过滤deleted=1的记录
        }

        // 手动删除
        userService.removeById(1L);  // 实际是更新deleted状态
    b.原因
        数据不丢失
        查询自动过滤已删除数据
        支持数据恢复
        减少手动编写删除逻辑

14.乐观锁更新保护
    a.代码示例
        public class Product {
            @Version  // 乐观锁版本号
            private Integer version;
        }

        // ✅ 推荐:更新时自动处理版本
        public boolean reduceStock(Long productId, Integer count) {
            LambdaUpdateWrapper<Product> wrapper = new LambdaUpdateWrapper<>();
            wrapper.eq(Product::getId, productId)
                   .ge(Product::getStock, count);

            Product product = new Product();
            product.setStock(product.getStock() - count);

            return productService.update(product, wrapper);
        }
    b.原因
        防止并发冲突
        自动处理版本控制
        简化并发更新逻辑
        提高数据一致性

15.递增和递减:setIncrBy 和 setDecrBy
    a.代码示例
        // ❌ 不推荐:使用 setSql
        userService.lambdaUpdate()
            .setSql("integral = integral + 10")
            .update();

        // ✅ 推荐:使用 setIncrBy
        userService.lambdaUpdate()
            .eq(User::getId, 1L)
            .setIncrBy(User::getIntegral, 10)
            .update();

        // ✅ 推荐:使用 setDecrBy
        userService.lambdaUpdate()
            .eq(User::getId, 1L)
            .setDecrBy(User::getStock, 5)
            .update();
    b.原因
        类型安全
        避免手动拼接sql,防止sql注入
        代码可维护性更强,更清晰

4.2 [1]坑点:5类

01.MyBatis 核心 Bug 汇总
    a.OGNL 表达式相关 Bug
        a.字符串比较类型错误
            a.影响版本
                全版本
            b.错误现象
                使用单引号导致字符与字符串比较失败
            c.解决方案
                方案1:使用双引号 `<if test='name == "1"'>`
                方案2:调用 `toString()` `<if test="name == '1'.toString()">`
            d.参考链接
                [MyBatis OGNL 文档](https://mybatis.org/mybatis-3/dynamic-sql.html)
                [Stack Overflow 解答](https://stackoverflow.com/questions/24013855/lazy-loading-using-mybatis-3-with-java)
        b.对于任何非字符串(String)的对象类型,如 Date、Integer、BigDecimal 等,条件判断中应该只使用 != null 来检查,而 != '' 的检查仅对 String 类型有效
            a.影响版本
                MyBatis 3.3.0
            b.错误现象
                `invalid comparison: java.util.Date and java.lang.String`
            c.解决方案
                推荐方案:移除空字符串比较 `<if test="createTime != null">`
                临时方案:降级至 MyBatis 3.2.8
            d.参考链接
                [GitHub Issue](https://github.com/mybatis/mybatis-3/issues/2383)
                [博客案例](https://blog.csdn.net/xiyang_1990/article/details/131813903)
        c.继承方法调用异常
            a.影响版本
                < 3.5.11
            b.错误现象
                OGNL调用继承方法时抛出 `IllegalArgumentException`
            c.解决方案
                升级至 MyBatis 3.5.11+ 版本
            d.参考链接
                [MyBatis 发布日志](https://blog.mybatis.org)
                [修复 PR](https://github.com/mybatis/mybatis-3/pull/2385)
    b.配置与集成 Bug
        a.Mapper扫描失效
            a.影响版本
                Spring Boot 集成
            b.错误现象
                `Invalid bound statement (not found)` 或 Mapper 无法注入异常
            c.解决方案
                方案1:正确配置 `@MapperScan("com.example.mapper")`
                方案2:使用 `<mapper class>` 替代 `<package>`
                方案3:检查版本兼容性
            d.参考链接
                [Spring Boot 集成文档](https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/)
                [Stack Overflow](https://stackoverflow.com/questions/77236695/i-encountered-a-problem-in-the-process-of-integrating-mybatis-in-using-springboo)
        b.循环依赖问题
            a.影响版本
                Spring 集成
            b.错误现象
                `BeanCurrentlyInCreationException: Circular dependency`
            c.解决方案
                方案1:使用 `@Lazy` 注解延迟初始化
                方案2:调整Bean初始化顺序
                方案3:分离配置类
            d.参考链接
                [GitHub Issue](https://github.com/mybatis/spring/issues/58)
                [解决方案](https://lightrun.com/answers/mybatis-spring-sqlsessionfactorybean-falls-in-circular-dependencies-by-spring-boots-datasourceinitializer)
        c.JAR包XML扫描失败
            a.影响版本
                打包部署
            b.错误现象
                JAR包内Mapper XML文件无法被扫描到
            c.解决方案
                方案1:使用 `classpath*:mapper/*.xml` 配置
                方案2:明确指定XML文件路径
                方案3:检查Maven资源配置
            d.参考链接
                [MyBatis 配置文档](https://mybatis.org/mybatis-3/configuration.html)
                [Maven 资源配置](https://maven.apache.org/plugins/maven-resources-plugin/)
    c.性能相关 Bug
        a.懒加载配置误解
            a.影响版本
                全版本
            b.错误现象
                `lazyLoadingEnabled` 默认false,导致懒加载未生效
            c.解决方案
                手动设置 `<setting name="lazyLoadingEnabled" value="true"/>`
            d.参考链接
                [MyBatis 设置文档](https://mybatis.org/mybatis-3/configuration.html#settings)
                [性能优化指南](https://dev.to/chat2db/why-is-your-mybatis-slow-one-line-of-config-can-double-its-performance-3jfa)
        b.N+1查询问题
            a.影响版本
                全版本
            b.错误现象
                嵌套查询导致1条主查询+N条关联查询
            c.解决方案
                方案1:使用JOIN查询替代嵌套SELECT
                方案2:配置懒加载策略
                方案3:启用二级缓存
            d.参考链接
                [N+1问题分析](https://planetscale.com/blog/what-is-n-1-query-problem-and-how-to-solve-it)
                [MyBatis 缓存](https://mybatis.org/mybatis-3/sqlmap-xml.html#cache)
        c.启动缓慢问题
            a.影响版本
                Linux环境
            b.错误现象
                MyBatis首次查询耗时20+秒
            c.解决方案
                方案1:配置JVM参数 `-Djava.security.egd=file:/dev/urandom`
                方案2:检查hostname配置
                方案3:优化连接池参数
            d.参考链接:
                [启动优化](https://groups.google.com/g/mybatis-user/c/_B5v0pQQ-rI)
                [HikariCP 优化](https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing)
    d.Java版本兼容性Bug
        a.Java 17反射访问限制
            a.影响版本
                MyBatis 3.5.7 + Java 17
            b.错误现象
                `InaccessibleObjectException: Unable to make field accessible`
            c.解决方案
                方案1:升级至支持Java 17的MyBatis版本
                方案2:添加JVM参数 `--add-opens java.base/java.lang=ALL-UNNAMED`
            d.参考链接
                [Java 17兼容性](https://github.com/mybatis/mybatis-3/issues/2383)
                [JVM参数配置](https://stackoverflow.com/questions/77590308/error-when-springboot3-combines-with-mybatis-plus-3-5-and-jdk-17)

02.MyBatis-Plus 特有 Bug 汇总
    a.事务管理 Bug
        a.saveBatch隐式事务冲突
            a.影响版本
                全版本
            b.错误现象
                `Transaction rolled back because it has been marked as rollback-only`
            c.解决方案
                推荐方案:避免在@Transactional方法中使用saveBatch
                替代方案:使用MybatisBatch手动管理事务
                临时方案:分离事务边界
            d.参考链接:
                [事务冲突分析](https://blog.csdn.net/xiyang_1990/article/details/131813903)
                [MybatisBatch文档](https://baomidou.com/en/guides/batch-operation/)
        b.多事务管理器冲突
            a.影响版本
                多数据源
            b.错误现象
                `No qualifying bean of type 'PlatformTransactionManager'`
            c.解决方案
                明确指定事务管理器 `@Transactional(transactionManager = "primaryTxManager")`
            d.参考链接
                [多数据源配置](https://blog.csdn.net/qq_42764468/article/details/124944636)
                [Spring 事务管理](https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction)
    b.多租户插件 Bug
        a.JSQLParser解析失败
            a.影响版本
                3.4.x - 3.5.x
            b.错误现象
                `JSQLParserException: Encountered unexpected token`
            c.解决方案
                方案1:使用 `@InterceptorIgnore(tenantLine="true")` 跳过解析
                方案2:升级JSQLParser版本排除冲突依赖
                方案3:简化SQL语法避开不支持函数
            d.参考链接
                [JSQLParser升级指南](https://blog.csdn.net/qq_24615389/article/details/132046825)
                [Gitee Issue](https://gitee.com/baomidou/mybatis-plus/issues/I7MJ5G)
        b.多语句执行限制
            a.影响版本
                全版本
            b.错误现象
                `multi-statement not allow`
            c.解决方案
                方案1:数据源URL添加 `allowMultiQueries=true`
                方案2:Druid配置 `WallConfig.setMultiStatementAllow(true)`
            d.参考链接
                [Druid配置](https://blog.csdn.net/weixin_41716049/article/details/130953393)
                [多语句配置](https://github.com/alibaba/druid/wiki/FAQ)
        c.租户隔离失效
            a.影响版本
                全版本
            b.错误现象
                自定义SQL未自动添加租户条件
            c.解决方案
                方案1:实现 `TenantLineHandler` 接口
                方案2:配置忽略表清单
                方案3:检查SQL解析范围
            d.参考链接
                [多租户配置](https://baomidou.com/en/plugins/tenant/)
                [实践案例](https://www.cnblogs.com/Apear/p/18196003)
    c.分页插件 Bug
        a.分页total为0
            a.影响版本
                3.4+
            b.错误现象
                分页查询有数据但total和pages字段显示为0
            c.解决方案
                方案1:使用 `MybatisPlusInterceptor` 替代旧版拦截器
                方案2:确保配置 `@Bean` 注解
                方案3:检查拦截器注册顺序
            d.参考链接
                [分页配置指南](https://blog.csdn.net/weixin_43715214/article/details/124929786)
                [官方文档](https://baomidou.com/en/plugins/pagination/)
        b.分页插件不生效
            a.影响版本
                全版本
            b.错误现象
                分页参数传入但SQL未添加LIMIT子句
            c.解决方案
                正确配置分页拦截器 `interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL))`
            d.参考链接
                [配置示例](https://blog.csdn.net/qq_39084776/article/details/124922593)
                [调试指南](https://www.cnblogs.com/lcxz8686TL/p/17288561.html)
    d.字段策略 Bug
        a.updateById更新失效
            a.影响版本
                全版本
            b.错误现象
                字段为null时更新条数为0
            c.解决方案
                方案1:全局配置 `field-strategy: IGNORED`
                方案2:字段级 `@TableField(strategy = FieldStrategy.IGNORED)`
                方案3:使用 `UpdateWrapper.set()` 方法
            d.参考链接
                [字段策略详解](https://blog.csdn.net/m0_58847451/article/details/137101144)
                [配置参考](https://www.cnblogs.com/nxjblog/p/14006935.html)
        b.@TableField注解失效
            a.影响版本
                全版本
            b.错误现象
                手写SQL中@TableField映射不生效
            c.解决方案
                手写SQL场景使用原生MyBatis的resultMap配置
            d.参考链接:
                [注解失效说明](https://www.cnblogs.com/kuangdaoyizhimei/p/15965206.html)
                [ResultMap配置](https://mybatis.org/mybatis-3/sqlmap-xml.html#Result_Maps)
        c.Oracle NULL更新异常
            a.影响版本
                Oracle数据库
            b.错误现象
                更新NULL值时缺少jdbcType参数
            c.解决方案
                XML中明确指定 `#{field, jdbcType=VARCHAR}`
            d.参考链接
                [Oracle兼容性](https://gitee.com/baomidou/mybatis-plus/issues/I90175)
                [JDBC类型映射](https://mybatis.org/mybatis-3/configuration.html#typeHandlers)
    e.严重版本Bug
        a.3.4.3.1 ClassCastException
            a.错误现象
                `MybatisConfiguration$StrictMap$Ambiguity cannot be cast to ResultMap`
            b.紧急修复方案
                立即降级:回退至 3.4.3
                或升级:升级至 3.4.3.2+
            c.参考链接
                [严重Bug报告](https://blog.csdn.net/mqq2502513332/article/details/119897649)
                [版本对比](https://github.com/baomidou/mybatis-plus/releases)
        b.< 3.5.3.1 安全漏洞CVE-2023-25330
            a.错误现象
                多租户插件SQL注入风险
            b.紧急修复方案
                立即升级至 3.5.3.1+ 修复漏洞
            c.参考链接
                [安全公告](https://github.com/aquasecurity/trivy/discussions/5986)
                [漏洞详情](https://nvd.nist.gov/vuln/detail/CVE-2023-25330)
        c.3.1.1+ 新日期类型失效
            a.错误现象
                `LocalDateTime` 映射失败
            b.紧急修复方案
                方案1:升级Druid至1.1.21+
                方案2:回退MP至3.1.0
                方案3:更换HikariCP数据源
            c.参考链接
                [兼容性问题](https://baomidou.com/en/reference/question/)
                [Druid升级](https://github.com/alibaba/druid/releases)
        d.3.5.7+ saveBatch返回值错误
            a.错误现象
                `Db.saveBatch` 始终返回false
            b.紧急修复方案
                移除驱动配置中的 `rewriteBatchedStatements=true` 参数
            c.参考链接
                [Bug分析](https://baomidou.com/en/reference/question/)
                [驱动配置](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-performance-extensions.html)

03.数据库兼容性Bug
    a.PostgreSQL 相关Bug
        a.ON CONFLICT语法解析
            a.错误现象
                `INSERT ... ON CONFLICT` 语法 JSQLParser 解析失败
            b.解决方案
                升级JSQLParser至4.7+版本
            c.参考链接
                [PostgreSQL语法支持](https://github.com/baomidou/mybatis-plus/issues/2700)
                [JSQLParser更新日志](https://github.com/JSQLParser/JSqlParser/releases)
        b.ARRAY函数不支持
            a.错误现象
                `ARRAY['value1','value2']` 语法触发解析异常
            b.解决方案
                推荐:使用大括号语法 `'{value1,value2}'`
                或:跳过多租户解析
            c.参考链接
                [语法兼容](https://blog.csdn.net/socilents/article/details/134372237)
                [PostgreSQL文档](https://www.postgresql.org/docs/current/arrays.html)
    b.MySQL 相关Bug
        a.批量更新语法错误
            a.错误现象
                批量操作提示 `multi-statement not allow`
            b.解决方案
                连接串添加 `allowMultiQueries=true` 参数
            c.参考链接
                [MySQL批量操作](https://stackoverflow.com/questions/22829539/mybatis-batch-update-exception)
                [连接参数文档](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-security.html)
    c.OceanBase 相关Bug
        a.批量插入缓冲区异常
            a.错误现象
                `Buffer length is less than expected payload length`
            b.解决方案
                方案1:调整批次大小减少单次数据量
                方案2:升级OceanBase驱动版本
            c.参考链接
                [OceanBase社区](https://ask.oceanbase.com/t/topic/35608224)
                [驱动优化](https://github.com/oceanbase/oceanbase-connector-j)

04.环境相关Bug
    a.Windows环境插件失效
        a.错误现象
            相同配置在macOS正常,Windows环境插件不生效
        b.解决方案
            排查项:路径分隔符差异、文件权限、字符编码
            解决:使用系统无关的路径配置
        c.参考链接
            [跨平台配置](https://community.sonarsource.com/t/plugin-works-on-mac-but-not-windows/101793)
            [路径配置最佳实践](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config)
    b.Spring Boot 版本兼容性对照表
        | Spring Boot 版本 | 推荐 MyBatis-Plus 版本 | Java 版本要求 | 关键注意事项
        |-----------------|-----------------------|--------------|-------------
        | **2.6.x**       | 3.4.3.2 - 3.5.3       | Java 8+      | 避免使用3.4.3.1版本
        | **2.7.x**       | 3.5.3.1+              | Java 8+      | 生产环境推荐,安全漏洞已修复
        | **3.0.x**       | 3.5.9+                | Java 17+     | 必须Java 17+,注意反射访问限制
        | **3.1.x**       | 3.5.9+                | Java 17+     | 稳定版本组合
        | **3.2.x**       | 3.5.10+               | Java 17+     | 最新稳定版本

05.版本兼容性 Bug 汇总
    a.MyBatis-Plus 3.4.3.1 版本严重 Bug
        a.问题描述
            此版本存在一个严重的类型转换 Bug,会导致应用启动或运行时抛出以下异常:
            `Caused by: java.lang.ClassCastException: com.baomidou.mybatisplus.core.MybatisConfiguration$StrictMap$Ambiguity cannot be cast to org.apache.ibatis.mapping.ResultMap`
        b.解决方案
            立即避开此版本。可以选择降级至 `3.4.3` 或升级至 `3.4.3.2` 及更高版本。
    b.安全漏洞 CVE-2023-25330
        a.影响版本
            MyBatis-Plus < 3.5.3.1
        b.问题描述
            在特定场景下,多租户插件(TenantLineInnerInterceptor)在处理某些复杂的 SQL 时存在 SQL 注入风险。
        c.解决方案
            立即将 MyBatis-Plus 升级至 `3.5.3.1` 或更高版本。
 
            备选方案:回退 MyBatis-Plus 至 `3.1.0`。
    d.Spring Boot 与 Java 版本兼容性
        a.版本对照关系
            Spring Boot 2.x: 推荐使用 MyBatis-Plus 3.4.x ~ 3.5.x。
            Spring Boot 3.x: 必须使用 MyBatis-Plus 3.5.3.2+ (推荐最新版)。
            Java 17+: 必须使用 MyBatis-Plus 3.5.3+。
    e.Db.saveBatch 返回值异常
        a.影响版本
            MyBatis-Plus 3.5.7+
        b.问题描述
            在数据库连接配置了 `rewriteBatchedStatements=true` 的情况下,调用 `Db.saveBatch()` 方法始终返回 `false`,即使数据已成功插入。
        c.解决方案
            从数据库连接 URL 中移除 `rewriteBatchedStatements=true` 参数。

4.3 [1]坑点:9类

01.MyBatis 核心配置陷阱
    a.数据源配置错误
        a.问题描述
            数据库连接池(DataSource)配置不当,可能导致数据库连接失败或应用性能严重下降。
        b.常见错误
            数据库 URL、用户名或密码拼写错误。
            生产环境未使用合适的连接池类型(如 POOLED)。
            使用的 JDBC 驱动版本与数据库服务器版本不兼容。
        c.解决方案与最佳实践
            推荐在生产环境中使用 `POOLED` 类型的连接池,并确保 JDBC 驱动版本匹配。
            <configuration>
              <environments default="development">
                <environment id="development">
                  <transactionManager type="JDBC"/>
                  <dataSource type="POOLED">
                    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
                    <property name="username" value="user"/>
                    <property name="password" value="pass"/>
                  </dataSource>
                </environment>
              </environments>
            </configuration>
        d.Maven 依赖示例
            <dependency>
              <groupId>mysql</groupId>
              <artifactId>mysql-connector-java</artifactId>
              <version>8.0.29</version>
            </dependency>
    b.Mapper 接口扫描失效
        a.问题描述
            应用启动或运行时抛出 `Invalid bound statement (not found)` 异常,表明 Mapper 接口没有被 MyBatis 或 Spring 正确扫描并代理。
        b.常见原因
            `@MapperScan` 注解中指定的扫描路径不正确。
            Spring Boot 与 MyBatis-Spring-Boot-Starter 版本不兼容。
            项目中存在多个版本的 MyBatis 相关 JAR 包,导致依赖冲突。
            Mapper XML 文件没有被正确编译到 target 目录。
        c.解决方案与代码示例
            a.方案1:使用 `@MapperScan` 注解指定正确的包路径。
                @Configuration
                @MapperScan("com.example.mappers")
                public class MyBatisConfig {
                    // ...
                }
            b.方案2:在 MyBatis 核心配置文件中使用 `<mappers>` 标签精确指定。
                <mappers>
                  <mapper resource="com/example/mappers/UserMapper.xml"/>
                  <mapper class="com.example.mappers.ProductMapper"/>
                </mappers>
    c.SQL 映射结构化问题
        a.问题描述
            在多个查询中重复编写相同的字段列表或查询条件,导致代码冗余,难以维护。
        b.解决方案:使用 `<sql>` 和 `<include>`
            通过 `<sql>` 标签定义可复用的 SQL 片段,然后在需要的地方使用 `<include>` 标签引用。
            <sql id="Base_Column_List">
              id, username, email
            </sql>

            <select id="selectUser" resultType="User">
              SELECT
              <include refid="Base_Column_List"/>
              FROM users
              WHERE id = #{id}
            </select>

02.OGNL 表达式相关陷阱
    a.字符串比较错误
        a.问题描述
            在 `<if>` 标签中,OGNL 表达式会将单引号 `'1'` 解析为 `char` 类型,导致与 `String` 类型的变量比较时失败。
        b.错误示例
            <if test="name != null and name == '1'">
              AND id = #{id}
            </if>
        c.解决方案
            a.方案1:使用双引号将字符包装成字符串。
                <if test='name != null and name == "1"'>
                  AND id = #{id}
                </if>
            b.方案2:调用 `toString()` 方法进行类型转换。
                <if test="name != null and name == '1'.toString()">
                  AND id = #{id}
                </if>
    b.对于任何非字符串(String)的对象类型,如 Date、Integer、BigDecimal 等,条件判断中应该只使用 != null 来检查,而 != '' 的检查仅对 String 类型有效
        a.影响版本
            MyBatis 3.3.0
        b.问题描述
            在此特定版本中,将 `java.util.Date` 类型的参数与空字符串 `''` 进行比较会抛出类型转换异常。
        c.错误示例
            <if test="createTime != null and createTime != ''">
              date(create_time) = date(#{createTime,jdbcType=TIMESTAMP})
            </if>
        d.解决方案与建议
            a.方案1 (推荐): 移除多余的空字符串比较,仅保留 `null` 判断。
                <if test="createTime != null">
                  date(create_time) = date(#{createTime,jdbcType=TIMESTAMP})
                </if>
            b.方案2 (备选): 避免使用 3.3.0 版本,可降级至 3.2.8 或升级至更高版本。

03.MyBatis-Plus 事务管理陷阱
    a.saveBatch() 隐式事务问题
        a.问题描述
            MyBatis-Plus 的 `saveBatch()` 方法内部已经声明了 `@Transactional`。如果外部业务方法也声明了事务,会导致事务嵌套和传播行为异常,可能使外部事务的回滚失效。
        b.错误场景示例
            @Transactional(rollbackFor = Exception.class)
            public Boolean batchInsert(List<Entity> list) {
                // 此处 saveBatch 的内部事务可能先于外部事务提交,导致外部异常时无法回滚
                entityService.saveBatch(list);
                return Boolean.TRUE;
            }
        c.解决方案
            a.方案1:绕过 Service 层,直接调用 Mapper 的批量插入方法,让整个操作处于同一个事务中。
                @Transactional(rollbackFor = Exception.class)
                public Boolean batchInsert(List<Entity> list) {
                    return entityMapper.insertBatch(list) > 0;
                }
            b.方案2:使用手动事务管理,完全控制事务的生命周期。
                @Autowired
                private MybatisBatch mybatisBatch;

                public Boolean batchInsert(List<Entity> list) {
                    return mybatisBatch.execute(sqlSession -> {
                        EntityMapper mapper = sqlSession.getMapper(EntityMapper.class);
                        list.forEach(mapper::insert);
                        return true;
                    });
                }
    b.多数据源下事务管理器冲突
        a.问题描述
            当应用中配置了多个数据源时,如果存在多个 `PlatformTransactionManager` 类型的 Bean,Spring 将无法确定使用哪一个,导致事务功能失效。
        b.解决方案
            在使用 `@Transactional` 注解时,通过 `transactionManager` 属性明确指定要使用的事务管理器 Bean 的名称。

            @Transactional(transactionManager = "primaryTransactionManager")
            public void primaryDbOperation() {
                // 主数据库操作
            }

            @Transactional(transactionManager = "secondaryTransactionManager")
            public void secondaryDbOperation() {
                // 从数据库操作
            }

04.MyBatis-Plus 分页插件陷阱
    a.分页查询 total 和 pages 为 0
        a.问题描述
            分页查询能够返回数据列表,但分页结果对象中的 `total` (总记录数) 和 `pages` (总页数) 字段恒为 0。
        b.常见原因
            未在 Spring 配置中注册分页拦截器 Bean。
            注册了分页拦截器,但忘记在其配置方法上添加 `@Bean` 注解。
            使用了与当前 MyBatis-Plus 版本不兼容的旧版分页拦截器。
        c.正确配置示例 (MyBatis-Plus 3.4.x+)
            @Configuration
            @MapperScan("com.example.dao")
            public class MyBatisPlusConfig {

                @Bean
                public MybatisPlusInterceptor mybatisPlusInterceptor() {
                    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
                    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
                    return interceptor;
                }
            }
    b.分页配置版本兼容性
        a.版本对应关系
            MyBatis-Plus < 3.4.0: 使用旧的 `PaginationInterceptor`。
            MyBatis-Plus >= 3.4.0: 必须使用新的 `MybatisPlusInterceptor` 并添加 `PaginationInnerInterceptor`。

05.SQL 解析异常问题
    a.JSQLParser 解析复杂 SQL 失败
        a.问题描述
            在使用特定数据库(如 PostgreSQL)的 JSON 函数或其他非标准 SQL 语法时,MyBatis-Plus 的内置 SQL 解析器 JSQLParser 无法识别,从而抛出 `JSQLParserException`。
        b.解决方案
            a.方案1:在方法上使用 `@InterceptorIgnore` 注解,跳过对此条 SQL 的自动解析(例如多租户、分页等)。
                @InterceptorIgnore(tenantLine = "true")
                public List<Entity> complexQuery() {
                    return mapper.complexQuery();
                }
            b.方案2:升级 JSQLParser 依赖至支持该语法的最新版本。
                <dependency>
                    <groupId>com.github.jsqlparser</groupId>
                    <artifactId>jsqlparser</artifactId>
                    <version>4.7</version>
                </dependency>
            c.方案3:调整 SQL 语法,使其能被 JSQLParser 识别。
    b.多语句执行限制
        a.问题描述
            在执行包含多条SQL语句(以分号 `;` 分隔)的操作时,数据库驱动或连接池(如 Druid)出于安全考虑会默认禁止,并报错 `multi-statement not allow`。
        b.解决方案
            a.方案1:修改数据源 URL,添加 `allowMultiQueries=true` 参数。
                spring.datasource.url=jdbc:mysql://localhost:3306/db?allowMultiQueries=true
            b.方案2:如果使用 Druid 连接池,需要修改其 Wall-Filter 的配置,允许执行多语句。
                @Configuration
                public class DruidConfig {
                    @Bean
                    public WallConfig wallConfig() {
                        WallConfig config = new WallConfig();
                        config.setMultiStatementAllow(true);
                        return config;
                    }
                }

06.字段更新策略问题
    a.updateById() 更新失效
        a.问题描述
            调用 MyBatis-Plus 提供的 `updateById()` 方法后,发现数据库中某些字段并未更新,但方法没有报错。
        b.根本原因
            MyBatis-Plus 的默认字段更新策略是 `NOT_NULL`,这意味着当实体类中某个字段的值为 `null` 时,该字段将不会被包含在最终生成的 UPDATE SQL 语句中。
        c.字段策略说明
            `IGNORED`: 忽略所有判断,任何字段都会被更新,即使值为 `null`。
            `NOT_NULL`: (默认值) 仅更新值不为 `null` 的字段。
            `NOT_EMPTY`: 仅更新值不为 `null` 且不为空字符串的字段。
        d.解决方案与代码示例
            a.方案1:全局修改更新策略。
                mybatis-plus:
                  global-config:
                    db-config:
                      field-strategy: IGNORED
            b.方案2:在实体类字段上使用注解,进行局部修改。
                public class User {
                    @TableField(updateStrategy = FieldStrategy.IGNORED)
                    private String nickName; // 允许将 nickName 更新为 null
                }
            c.方案3:使用 `UpdateWrapper` 精确控制更新行为,此方法会忽略所有策略。
                UpdateWrapper<User> wrapper = new UpdateWrapper<>();
                wrapper.eq("id", userId)
                       .set("nick_name", null); // 强制将 nick_name 设置为 null
                userMapper.update(null, wrapper);
    b.Oracle 数据库 NULL 值更新异常
        a.问题描述
            在 Oracle 数据库环境下,当尝试将字段更新为 `null` 时,如果 XML 映射文件中没有明确指定 `jdbcType`,可能会因无法推断类型而抛出异常。
        b.解决方案
            在 XML 的 `UPDATE` 语句中,为所有可能为 `null` 的字段显式添加 `jdbcType` 属性。
            <update id="updateUser">
              UPDATE user SET
                name = #{name, jdbcType=VARCHAR},
                age = #{age, jdbcType=INTEGER}
              WHERE id = #{id}
            </update>

07.性能相关陷阱
    a.懒加载配置陷阱
        a.问题描述
            MyBatis 官方文档曾描述 `lazyLoadingEnabled` 默认为 `true`,但实际源码中其默认值为 `false`,导致开发者以为懒加载已开启,实际并未生效。
        b.解决方案
            必须在 MyBatis 配置文件中手动开启懒加载。
            <settings>
              <setting name="lazyLoadingEnabled" value="true"/>
              <setting name="aggressiveLazyLoading" value="false"/>
            </settings>
        c.注意事项
            `aggressiveLazyLoading` 应设为 `false`,否则调用对象的任意方法都会触发其所有属性的加载,使懒加载形同虚设。
            开启缓存和懒加载时,可能会遇到序列化异常,需要确保关联对象也实现了 `Serializable` 接口。
    b.N+1 查询性能问题
        a.问题表现
            在进行一对多或多对多查询时,先执行 1 条主 SQL 查询主表,然后根据主表结果,再执行 N 条关联 SQL 去查询关联表,造成大量数据库交互,性能低下。
        b.解决方案
            关联查询(JOIN): 使用 `<association>` 或 `<collection>` 的 `select` 属性改为 `resultMap` 配合 `JOIN` 语句,在一次查询中返回所有数据。
            懒加载: 对于非必要立即使用的关联数据,开启懒加载。
            二级缓存: 启用二级缓存可以有效减少对数据库的重复查询。
    c.项目启动缓慢问题
        a.常见原因
            在某些 Linux 环境下,由于熵池(Entropy Pool)不足,导致 `SecureRandom` 初始化阻塞,从而影响依赖它来生成 ID 的组件(如 Snowflake)。
        b.解决方案
            在启动应用的 Java 命令中,指定一个非阻塞的随机数生成源。
            java -Djava.security.egd=file:/dev/urandom -jar app.jar

08.数据库兼容性问题
    a.PostgreSQL 特殊语法解析失败
        a.问题描述
            使用 PostgreSQL 的 `INSERT ... ON CONFLICT` 这类非标准 SQL 语法时,MyBatis-Plus 的内置解析器 JSQLParser 无法识别,导致解析异常。
        b.解决方案
            升级 JSQLParser 依赖至 `4.7` 或更高版本。
    b.MySQL 批量操作支持
        a.问题描述
            默认情况下,MySQL 驱动禁止在一次请求中执行多条 SQL 语句(以分号分隔),导致批量更新或插入失败。
        b.解决方案
            在数据库连接 URL 中添加 `allowMultiQueries=true` 参数。
            spring.datasource.url=jdbc:mysql://localhost:3306/db?allowMultiQueries=true
    c.OceanBase 批量插入异常
        a.问题描述
            在使用 OceanBase 数据库进行批量插入时,可能会遇到 `Buffer length is less than expected payload length` 的异常。
        b.解决方案
            尝试减小批量插入的批次大小(batch size),或升级 OceanBase 的 JDBC 驱动版本。

09.结果映射与类型处理
    a.构造器映射依赖字段顺序
        a.问题描述
            当实体类仅有全参构造器时,MyBatis 会尝试按查询结果的字段顺序与构造器参数顺序进行匹配。一旦 SQL 查询的字段顺序发生改变,就会导致属性映射错乱。
        b.解决方案
            a.方案1 (推荐): 为实体类提供一个无参构造器。
                @NoArgsConstructor // Lombok 注解
                public class User { ... }
            b.方案2: 在 `<resultMap>` 中使用 `<constructor>` 标签,明确指定结果集列与构造器参数的映射关系。
                <resultMap id="userResultMap" type="User">
                  <constructor>
                    <idArg column="id" javaType="Long"/>
                    <arg column="name" javaType="String"/>
                  </constructor>
                </resultMap>
    b.Map 结果集处理问题
        a.问题一:下划线无法转驼峰
            当 `resultType` 设置为 `java.util.Map` 时,MyBatis 默认不会将数据库列的下划线命名(e.g., `user_name`)自动转换为 Map 中 key 的驼峰命名(`userName`)。
        b.问题一解决方案
            开启全局的 `mapUnderscoreToCamelCase` 配置。
            @Configuration
            public class MyBatisConfig {
                @Bean
                public ConfigurationCustomizer configurationCustomizer() {
                    return configuration -> {
                        configuration.setMapUnderscoreToCamelCase(true);
                    };
                }
            }
        c.问题二:null 值的 key 丢失
            默认情况下,如果查询结果的某个字段值为 `null`,那么在最终返回的 Map 中,将不会包含这个字段对应的 key。
        d.问题二解决方案
            开启 `call-setters-on-nulls` 配置,强制为 `null` 值的字段也在 Map 中创建 key。
            mybatis:
              configuration:
                call-setters-on-nulls: true

4.4 [1]查询:5个

00.汇总
    1.查询条数
    2.分页问题
    3.参数数量
    4.参数类型
    5.批量条数

00.示例
    a.代码
        import java.util.List;

        public class StaffService {

            private static final int QUERY_BATCH_SIZE = 1000; // 查询批量大小
            private static final int INSERT_BATCH_SIZE = 500; // 插入批量大小

            private StaffDao staffDao;

            public StaffService(StaffDao staffDao) {
                this.staffDao = staffDao;
            }

            // 查询并处理员工列表
            public void processStaffList() {
                Long lastStaffId = null;
                while (true) {
                    List<Staff> staffList = staffDao.getStaffList(lastStaffId, QUERY_BATCH_SIZE);
                    if (staffList.isEmpty()) {
                        break;
                    }

                    // ...处理逻辑

                    // 获取当前批次最后一个员工的ID,用于下一次查询
                    lastStaffId = staffList.get(staffList.size() - 1).getStaffId();
                }
            }

            // 批量插入员工数据
            public void batchInsertStaff(List<Staff> staffList) {
                int totalSize = staffList.size();
                int pageCount = totalSize / INSERT_BATCH_SIZE;

                for (int i = 0; i <= pageCount; i++) {
                    int start = i * INSERT_BATCH_SIZE;
                    int end = Math.min(start + INSERT_BATCH_SIZE, totalSize);
                    List<Staff> subList = staffList.subList(start, end);

                    // 插入子列表
                    staffDao.batchInsertStaff(subList);
                }
            }
        }
    b.说明
        a.查询条数
            使用 QUERY_BATCH_SIZE 来限制每次查询的记录数,避免查询结果集过大导致数据库性能问题
            使用 lastStaffId 来解决深度分页问题,确保不会漏掉数据
        b.分页问题
            通过传入 lastStaffId 来进行分页查询,避免使用 offset 导致的性能问题
        c.参数数量
            在批量插入时,使用 INSERT_BATCH_SIZE 来限制每次插入的记录数,避免参数过多导致数据库错误
        d.参数类型
            确保在插入操作中指定参数类型,尤其是对于可能为空的字段,使用适当的类型处理
        e.批量条数
            使用分页操作对批量插入的条数进行限制,提高操作效率,避免数据库 hang 住

01.查询条数
    a.代码
        public List<Staff> processStaffList(){
            int offset = 0;
            List<Staff> staffList = staffDao.getStaffList(offset);
            while(true){
                //...处理逻辑
                if(staffList.size() < 50000){
                    break;
                }
                offset += 50000;
                staffList = staffDao.getStaffList(offset);
            }
        }
    b.说明
        上面的查询想一次想查回50000条数据,很有可能数据库不能返回50000条
        一般数据库都有查询结果集限制,比如MySQL会受两个参数的限制:
        1.max_allowed_packet:返回结果集大小,默认4M,超过这个大小结果集就会被截断
        2.max_execution_time:一次查询执行时间,默认值是0表示没有限制,如果超过这个时间,MySQL会终止查询,返回结果
        所以,如果结果集太大不能全部返回,而我们在代码中每次传入的offset都是基于上次的offset加50000,那必定会漏掉部分数据

02.分页问题
    如果单表数据量非常大,offset会很大造成深度分页问题,查询效率低下
    我们可以通过传入一个起始的staffId来解决深度分页问题
    ---------------------------------------------------------------------------------------------------------
    public List<Staff> processStaffList(){
        List<Staff> staffList = staffDao.getStaffList(null);
        while(true){
            //...处理逻辑
            if(staffList.size() < 1000){
                break;
            }
            Staff lastStaffInPage = staffList.get(staffList.size() - 1);
            staffList = staffDao.getStaffList(lastStaffInPage.getStaffId());
        }
    }

03.参数数量
    如果staffList数量太大,会导致整条语句参数过多
    如果使用Oracle数据库,参数数量超过65535,会报ORA-7445([opiaba]when using more than 65535 bind variables)的错误,导致数据库奔溃
    一定要对参数数量进行限制。参数太多,也可能会抛出下面异常

04.参数类型
    插入语句并没有指定参数类型
    这样会有一个问题,虽然一个字段我们定义成可以为空,但是通过参数传进来的这个字段值是空,就会抛出下面异常导致插入失败
    要保证程序健壮性,就要给插入语句中参数指定类型

05.批量条数
    批量操作是为了减少应用和数据库的交互,提高操作效率。但是如果对插入、更新这些批量操作不做条数限制
    很可能会导致操作效率低下甚至数据库hang住。我们可以通过分页操作对批量条数做一些限制
    ---------------------------------------------------------------------------------------------------------
    public List<Staff> processStaffList(){
        List<Staff> staffList = ...;
        int pageSize = 500;
        int pageCount = staffList / pageSize;
        for(int i = 0; i < pageCount + 1; i++){
            List<Staff> subList = (i == pageCount)? staffList.subList(i * pageSize, staffList.size()) :
                  staffList.subList(i * pageSize, (i + 1) * pageSize);
            staffDao.batchInsertStaff(subList);
        }
    }

4.5 [1]配置:16个

01.MyBatis-Plus常用配置
    a.log-impl
        说明:指定 MyBatis 的日志实现类,用于输出 SQL 日志
        示例:org.apache.ibatis.logging.stdout.StdOutImpl 输出到控制台
    b.map-underscore-to-camel-case
        说明:开启下划线转驼峰命名规则,数据库字段为下划线命名,实体类为驼峰命名
        示例:true 开启,false 关闭
    c.use-deprecated-executor
        说明:是否使用 MyBatis 的过时执行器,避免缓存出现问题
        示例:false(默认),true 使用过时执行器
    d.configuration-factory
        说明:指定一个类,该类实现了 org.apache.ibatis.session.Configuration 接口,用于自定义 MyBatis 配置
        示例:com.example.MyConfigurationFactory
    e.cache-enabled
        说明:是否启用二级缓存
        示例:true 启用,false 禁用
    f.lazy-loading-enabled
        说明:是否启用延迟加载
        示例:true 启用,false 禁用
    g.aggressive-lazy-loading
        说明:是否启用激进的延迟加载
        示例:true 启用,false 禁用
    h.multiple-result-sets-enabled
        说明:是否允许返回多个结果集
        示例:true 允许,false 不允许
    i.use-column-label
        说明:是否使用列标签代替列名
        示例:true 使用,false 不使用
    j.default-statement-timeout
        说明:设置默认的语句超时时间(秒)
        示例:30 表示 30 秒
    k.default-fetch-size
        说明:设置默认的获取数量
        示例:100 表示每次获取 100 条记录
    l.safe-row-bounds-enabled
        说明:是否启用安全的 RowBounds
        示例:true 启用,false 禁用
    m.safe-result-handler-enabled
        说明:是否启用安全的 ResultHandler
        示例:true 启用,false 禁用
    n.map-underscore-to-camel-case
        说明:是否开启下划线转驼峰
        示例:true 开启,false 关闭
    o.local-cache-scope
        说明:设置本地缓存范围
        示例:SESSION 或 STATEMENT
    p.jdbc-type-for-null
        说明:设置空值的 JDBC 类型
        示例:NULL

02.MP配置
    a.示例1
        mybatis-plus:
          configuration:
            log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
            map-underscore-to-camel-case: true
            use-deprecated-executor: false
            cache-enabled: false
            lazy-loading-enabled: false
            aggressive-lazy-loading: false
            multiple-result-sets-enabled: true
            use-column-label: true
            default-statement-timeout: 30
            default-fetch-size: 100
            safe-row-bounds-enabled: false
            safe-result-handler-enabled: true
            local-cache-scope: SESSION
            jdbc-type-for-null: NULL
    b.示例2
        mybatis-plus:
          # mapper-locations:SQL映射文件的加载问题
          # cn.jggroup.config.mybatis.MybatisPlusSaasConfig:Mapper接口的注册问题
          mapper-locations: classpath*:cn/jggroup/**/mapper/xml/*.xml
          global-config:
            banner: false
            db-config:
              #主键类型  0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)";
              id-type: ASSIGN_ID
              # 默认数据库表下划线命名
              table-underline: true
          configuration:
            # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
            #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
            # 返回类型为Map,显示null对应的字段
            call-setters-on-nulls: true

03.日志
    logging:
      level:
        root: info                   # 设置根日志级别为info
        jdk: info                    # JDK相关日志级别
        druid: info                  # Druid数据库连接池日志级别
        javax: info                  # Java扩展包日志级别
        sun.rmi: info                # Java RMI相关日志级别
        io.netty: info               # Netty网络框架日志级别
        Validator: info              # 数据校验相关日志级别
        springfox: warn              # Springfox(Swagger)文档日志级别
        io.lettuce: info             # Lettuce Redis客户端日志级别
        org.apache: info             # Apache通用组件日志级别
        org.quartz: info             # Quartz定时任务日志级别
        com.alibaba: info            # 阿里巴巴组件日志级别
        com.baomidou: warn           # MyBatis-Plus框架日志级别
        io.springfox: warn           # Springfox核心组件日志级别
        org.hibernate: info          # Hibernate框架日志级别
        javax.management: info       # JMX管理扩展日志级别
        okhttp3.internal: info       # OkHttp内部实现日志级别
        org.mybatis.spring: info     # MyBatis Spring集成日志级别
        druid.sql.statement: debug   # Druid SQL语句执行日志级别
        _org.springframework: info   # Spring框架内部日志级别
        com.alibaba.druid.pool.DruidDataSource: warn  # Druid数据源连接池日志级别
        org.spring: info                                     # Spring核心功能日志级别
        org.springframework: info                            # Spring框架通用日志级别
        org.springframework.boot.StartupInfoLogger: off      # 关闭启动信息日志
        org.springframework.boot.SpringApplication: off      # 关闭Spring应用启动日志
        org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext: warn  # Web容器上下文日志级别
        org.springframework.context.support: warn   # Spring上下文支持类日志级别
        cn.jggroup: debug                          # 业务代码主包日志级别
        cn.jggroup.system.mapper: debug            # 系统模块数据访问层日志级别

4.6 [2]where 1=1

01.使用Mybatis + where 1 = 1
    前后端同时做好接口参数校验
    复用和专用要做平衡,条件允许情况下将复用 SQLMap 拆分成更细粒度的 SQLMap
    编写代码时,需要考虑资源占用量,做好预防性编程

02.OOM事故
    当构建动态 SQL 查询时,条件通常会追加到 WHERE 子句后,而以 WHERE 1 = 1 开头,可以轻松地使用 AND 追加其他条件
    用户中心在上线后,竟然每隔三四个小时就发生了内存溢出问题 ,经过通过和 DBA 沟通,发现高频次出现全表查询用户表
    执行 SQL 变成 SELECT FROM t_lottery_usert WHERE 1=1
    查看日志后,发现前端传递的参数出现了空字符串,在代码中并没有做参数校验,所以才出现全表查询
    当时用户表的数据是 1000万 ,调用几次,用户中心服务就 OOM 了

4.7 [2]param注解

01.图示
    场景                必加@Param       核心原因
    多基础类型参数           √            需区分param1/param2与业务名
    参数取别名              √            统一方法参数与SQL引用名
    SQL用${}拼接           √            需明确拼接的参数名称
    动态SQL引用参数         √            标签中需参数名定位值
    单参数+无动态SQL        ×            MyBatis自动映射单个基础类型参数

02.问题
    a.什么情况下使用呢?
        用springboot可以不用,不用springboot要用,spring编译后会自己给你补上param再变成class,
        其它编译会直接抹去名称编译到class,这样会出现找不到参数(仅用于在mapper里直接写sql)
        如果要写在xml,个人建议不管用不用springboot都加上param
    b.为什么只知道param1 param2?就不能反射出参数名么?
        Java默认编译会把参数名换成param1、param2(丢了原名字),MyBatis反射只能拿到这个。
        想让它认原参数名,得改编译配置,但加@Param更简单还兼容,所以常用后者

4.8 [2]批处理+流处理

00.汇总
    a.批处理和流处理的区别
        a.数据处理的定义和性质
            批处理处理大块数据,流处理实时处理数据
        b.延迟和处理时间
            批处理具有更高的延迟,流处理提供更低的延迟
        c.用例和应用
            批处理用于不需要立即处理数据的情况,流处理用于需要立即采取行动的情况
        d.容错性和可靠性
            批处理可以重新启动作业,流处理需要复杂的容错机制
        e.可扩展性和性能
            批处理针对吞吐量优化,流处理针对高吞吐量和低延迟设计
        f.复杂性和设置
            批处理设置简单,流处理设置复杂
    b.如何选择
        a.数据量
            处理大量数据时,批处理可能更好
        b.实时需求
            需要实时洞察或行动时,流处理更适合
        c.复杂性
            需要复杂算法和数据转换时,批处理可能更合适
        d.数据性质
            有限且可预测大小的数据适合批处理,无界、持续的数据流适合流处理。

01.批处理
    a.定义
        批处理是一种传统的数据处理方法,通常在一段时间内收集和存储数据,然后在预定的时间间隔内对这些数据进行批量处理
    b.适合的场景
        高吞吐量:适用于处理大量数据,如数据仓库、数据集成和数据清洗
        复杂处理:需要复杂算法、数据转换和数据汇总的任务
        离线处理:通常在离线模式下进行,结果存储供以后使用
    c.优点
        效率:通过大批量处理数据来优化资源利用
        简便性:处理静止数据,不需要复杂的事件处理系统
        可扩展性:可以通过在多台机器上分配工作负载进行水平扩展
        容错性:提供内置的容错机制,确保数据完整性
        成本效益:在非高峰时段高效利用计算资源
    d.缺点
        延迟:在数据收集和处理之间引入延迟
        缺乏实时洞察:无法提供最新洞察或实现实时决策
        对突变响应不佳:无法立即适应意外的数据激增或模式变化
        数据陈旧:处理完成时,数据可能已经陈旧
        资源密集:大批量处理可能需要大量计算资源
    e.使用框架
        Apache Hadoop:分布式存储和处理大数据
        Apache Spark:支持批处理和实时处理,处理速度快
        Apache Flink:高性能,低延迟的数据流处理
        Apache Beam:统一的编程模型,可在多种执行引擎上运行

02.流处理
    a.定义
        流处理是一种数据处理方法,在数据到达时立即对其进行处理,而不是等待一段时间后再进行处理
    b.适用场景
        实时洞察:适合需要即时洞察和响应的任务
        低延迟任务:需要快速处理和低延迟的应用
        持续处理任务:需要持续不断处理数据的任务
    c.优点
        实时处理:允许立即处理数据,适合依赖及时洞察的应用
        可扩展性:可以横向扩展,处理越来越多的数据量
        灵活性:适应数据类型和处理逻辑的变化
    d.缺点
        复杂性:管理流处理系统可能很复杂
        更高的成本:可能需要更强大的系统和持续运行
        数据丢失的潜在风险:存在丢失实时数据的风险
    e.使用框架
        Apache Kafka:用于构建实时数据流应用和数据管道
        Apache Flink:支持低延迟和高吞吐量的数据处理
        Apache Storm:适用于处理高速和大规模的数据流

4.9 [2]唯一索引产生重复数据

00.汇总
    a.背景
        在MySQL中,唯一索引的作用是确保列中的所有值都是唯一的
        然而,在某些情况下,即使加了唯一索引,仍然可能出现重复数据
    b.可能原因
        a.并发插入导致的竞态条件
            在高并发环境下,多个事务可能同时插入相同的数据,导致唯一性约束在某些情况下失效
            解决方案:使用事务和锁机制来确保插入操作的原子性
        b.忽略错误的插入操作
            使用INSERT IGNORE或REPLACE INTO语句时,如果插入的数据违反唯一约束,MySQL会忽略错误或替换现有行,而不是抛出错误
            解决方案:避免使用INSERT IGNORE或REPLACE INTO,改用INSERT并处理唯一性约束错误
        c.字符集和排序规则问题
            不同的字符集和排序规则可能导致看似不同的值被视为相同。例如,某些排序规则不区分大小写
            解决方案:确保数据库和表的字符集和排序规则设置正确,符合业务需求
        d.数据导入工具的限制
            使用某些数据导入工具(如LOAD DATA INFILE)时,可能会忽略唯一性约束
            解决方案:在数据导入后,手动检查并清理重复数据
        e.索引未生效
            唯一索引可能由于某些原因未生效,例如索引被禁用或损坏
            解决方案:检查索引状态,确保索引已启用并正常工作
    c.检查和解决步骤
        a.检查索引状态:
            SHOW INDEX FROM table_name;
        b.检查字符集和排序规则:
            SHOW CREATE TABLE table_name;
        c.处理重复数据:使用查询找出重复数据,并手动删除或合并
            SELECT column_name, COUNT(*)
            FROM table_name
            GROUP BY column_name
            HAVING COUNT(*) > 1;
        d.调整插入逻辑:
            确保插入操作在事务中进行,并使用适当的锁机制
            避免使用INSERT IGNORE或REPLACE INTO,改用INSERT并处理唯一性约束错误

01.还原问题现场
    a.说明
        在商品组防重表中,为了防止重复数据,创建了唯一索引
    b.代码
        CREATE TABLE `product_group_unique` (
          `id` bigint NOT NULL,
          `category_id` bigint NOT NULL,
          `unit_id` bigint NOT NULL,
          `model_hash` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
          `in_date` datetime NOT NULL,
          PRIMARY KEY (`id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

        ALTER TABLE product_group_unique ADD UNIQUE INDEX
        ux_category_unit_model(category_id, unit_id, model_hash);

02.唯一索引字段包含NULL
    a.问题
        如果唯一索引的字段中包含NULL值,唯一性约束可能不会生效
    b.示例
        当model_hash字段为NULL时,插入重复数据不会被拦截
    c.解决方案
        确保创建唯一索引的字段不允许为NULL,或者在应用层进行数据校验

03.逻辑删除表加唯一索引
    a.问题
        逻辑删除表中添加唯一索引可能导致重复数据无法插入
    b.解决方案
        删除状态+1:每次逻辑删除时,将delete_status字段加1,确保唯一性
        增加时间戳字段:在逻辑删除时,写入时间戳,确保每次删除的记录唯一
        增加ID字段:在逻辑删除时,使用一个新的delete_id字段记录当前记录的主键ID

04.重复历史数据如何加唯一索引
    a.问题
        表中已存在历史重复数据,如何加唯一索引?
    b.解决方案
        增加防重表:将数据初始化到防重表中
        增加ID字段:在原表中增加delete_id字段,区分历史重复数据

05.给大字段加唯一索引
    a.问题
        大字段加唯一索引可能导致索引过长
    b.解决方案
        增加Hash字段:对大字段取Hash值,生成较短的新值,作为唯一索引的一部分
        不加唯一索引:通过其他技术手段保证唯一性,如单线程处理或使用Redis分布式锁

06.批量插入数据
    a.问题
        批量插入数据时,如何保证唯一性?
    b.解决方案
        使用唯一索引:直接批量INSERT,数据库自动判断重复数据
        Redis分布式锁:在批量操作中,使用Redis锁来控制并发

4.10 [3]多线程批量:2种

01.业务
    由于业务需求,需要将之前用 AES 加密保存的用户敏感信息解密并以明文形式保存
    然而,订单表已经有 7000 万+ 条数据,直接操作可能导致内存溢出,因此需要分批处理

02.解决方案
    a.方案一:使用多线程和 CountDownLatch
        a.分批处理
            每次查询 1 万条数据
        b.多线程处理
            将 1 万条数据分成 10 份,每个线程处理 1000 条
        c.线程同步
            使用 CountDownLatch 等待所有线程完成后再处理下一批数据
        d.注意事项
            a.线程安全
                确保对共享数据的访问是线程安全的,使用同步机制如锁
            b.数据视图
                subList 返回的是原列表的视图,修改原列表会影响 subList,需要创建新的列表来避免此问题
            c.依赖注入
                在多线程中使用 Spring 的 @Autowired 可能会失效,可以通过构造函数传递依赖或使用 ApplicationContext.getBean 静态获取 Bean
    b.方案二:使用共享队列
        a.消息队列
            将查询结果作为消息队列,每个线程从队列中获取 1000 条数据进行处理
        b.线程安全
            在访问共享队列时加锁,确保线程安全
        c.动态查询
            当队列为空时,查询下一批数据

03.方案一:使用多线程和 CountDownLatch
    a.示例
        public static void main(String[] args) {
            List<Integer> list = new ArrayList<>();
            for (int i = 0; i < 100; i++) {
                list.add(i);
            }
            while (list.size() >= 10) {
                List<Integer> dataList = new ArrayList<>(list.subList(list.size() - 10, list.size()));
                ListThread listThread = new ListThread(dataList);
                listThread.start();
                list = list.subList(0, list.size() - 10);
            }
            list.add(33333);
        }

        class ListThread extends Thread {
            private List<Integer> list;

            public ListThread(List<Integer> list) {
                this.list = list;
            }

            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "开始打印数据======");
                for (int i = 0; i < list.size(); i++) {
                    System.out.println(list.get(i));
                }
            }
        }
        -----------------------------------------------------------------------------------------------------
        subList 返回的是原列表的视图,修改原列表会影响 subList,需要创建新的列表来避免此问题
    b.示例2
        import org.springframework.context.ApplicationContext;
        import org.springframework.context.annotation.AnnotationConfigApplicationContext;

        import java.util.ArrayList;
        import java.util.List;
        import java.util.concurrent.CountDownLatch;
        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;

        public class OrderProcessingService {

            private static final int BATCH_SIZE = 10000;
            private static final int THREAD_COUNT = 10;
            private static final ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

            public static void main(String[] args) {
                OrderProcessingService service = context.getBean(OrderProcessingService.class);
                service.processOrders();
            }

            public void processOrders() {
                int maxId = getMaxOrderId(); // 获取订单表的最大ID
                int currentId = 0;

                while (currentId < maxId) {
                    List<Order> orders = fetchOrders(currentId, BATCH_SIZE);
                    if (orders.isEmpty()) {
                        break;
                    }

                    CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
                    ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);

                    for (int i = 0; i < THREAD_COUNT; i++) {
                        int start = i * (BATCH_SIZE / THREAD_COUNT);
                        int end = Math.min(start + (BATCH_SIZE / THREAD_COUNT), orders.size());
                        List<Order> subList = new ArrayList<>(orders.subList(start, end));

                        executor.submit(() -> {
                            try {
                                processOrderBatch(subList);
                            } finally {
                                latch.countDown();
                            }
                        });
                    }

                    try {
                        latch.await(); // 等待所有线程完成
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }

                    executor.shutdown();
                    currentId = orders.get(orders.size() - 1).getId();
                }
            }

            private List<Order> fetchOrders(int startId, int limit) {
                // 从数据库中查询订单数据
                // 示例:return orderMapper.selectOrders(startId, limit);
                return new ArrayList<>(); // 返回查询结果
            }

            private void processOrderBatch(List<Order> orders) {
                // 处理订单数据
                // 示例:orderMapper.updateOrders(orders);
            }

            private int getMaxOrderId() {
                // 获取订单表的最大ID
                // 示例:return orderMapper.getMaxOrderId();
                return 1000000; // 示例返回值
            }
        }

        class Order {
            private int id;
            // 其他订单字段

            public int getId() {
                return id;
            }

            // 其他 getter 和 setter 方法
        }

        class AppConfig {
            // Spring 配置类
        }
        -----------------------------------------------------------------------------------------------------
        分批处理:每次查询 1 万条数据,通过 fetchOrders 方法从数据库获取数据。
        多线程处理:使用 ExecutorService 创建固定数量的线程池,将 1 万条数据分成 10 份,每个线程处理 1000 条
        线程同步:使用 CountDownLatch 等待所有线程完成后再处理下一批数据
        线程安全:确保对共享数据的访问是线程安全的,使用 synchronized 或其他同步机制
        数据视图:使用 new ArrayList<>(subList) 创建新的列表,避免 subList 的视图问题
        依赖注入:通过 ApplicationContext 获取 Spring Bean,确保在多线程中可以使用 Spring 的依赖注入

04.方案二:使用共享队列
    a.说明
        使用共享队列来处理大数据量的订单表数据
        这个方案通过将查询结果作为消息队列,每个线程从队列中获取数据进行处理,并在队列为空时动态查询下一批数据
    b.代码
        import java.util.ArrayList;
        import java.util.List;
        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;
        import java.util.concurrent.LinkedBlockingQueue;

        public class OrderProcessingService {

            private static final int BATCH_SIZE = 10000;
            private static final int THREAD_COUNT = 10;
            private static final LinkedBlockingQueue<Order> orderQueue = new LinkedBlockingQueue<>();

            public static void main(String[] args) {
                OrderProcessingService service = new OrderProcessingService();
                service.processOrders();
            }

            public void processOrders() {
                ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);

                // 启动线程池
                for (int i = 0; i < THREAD_COUNT; i++) {
                    executor.submit(this::processOrderBatch);
                }

                int maxId = getMaxOrderId(); // 获取订单表的最大ID
                int currentId = 0;

                while (currentId < maxId) {
                    List<Order> orders = fetchOrders(currentId, BATCH_SIZE);
                    if (orders.isEmpty()) {
                        break;
                    }

                    // 将订单数据加入队列
                    orderQueue.addAll(orders);

                    // 更新 currentId 为当前批次最后一个订单的 ID
                    currentId = orders.get(orders.size() - 1).getId();
                }

                executor.shutdown();
            }

            private void processOrderBatch() {
                while (true) {
                    List<Order> orders = new ArrayList<>();
                    orderQueue.drainTo(orders, 1000); // 从队列中获取最多 1000 条数据

                    if (orders.isEmpty()) {
                        // 如果队列为空,检查是否还有未处理的数据
                        synchronized (orderQueue) {
                            if (orderQueue.isEmpty()) {
                                return; // 所有数据处理完毕,退出线程
                            }
                        }
                    } else {
                        // 处理订单数据
                        // 示例:orderMapper.updateOrders(orders);
                    }
                }
            }

            private List<Order> fetchOrders(int startId, int limit) {
                // 从数据库中查询订单数据
                // 示例:return orderMapper.selectOrders(startId, limit);
                return new ArrayList<>(); // 返回查询结果
            }

            private int getMaxOrderId() {
                // 获取订单表的最大ID
                // 示例:return orderMapper.getMaxOrderId();
                return 1000000; // 示例返回值
            }
        }

        class Order {
            private int id;
            // 其他订单字段

            public int getId() {
                return id;
            }

            // 其他 getter 和 setter 方法
        }
    c.说明
        消息队列:使用 LinkedBlockingQueue 作为共享队列,将查询结果加入队列中,每个线程从队列中获取数据进行处理
        线程安全:在访问共享队列时使用 synchronized 确保线程安全,特别是在检查队列是否为空时
        动态查询:当队列为空时,检查是否还有未处理的数据,如果没有则退出线程
        线程池:使用 ExecutorService 创建固定数量的线程池,处理队列中的数据

4.11 [3]mybatis批量:6问

00.汇总
    a.分类
        1.MyBatis批量入库时,xml的foreach和java的foreach,性能上有什么区别?
        2.在MyBatis中,对于<foreach>标签的使用,通常有几种常见的优化方法?
        3.MyBatis foreach批量插入会有什么问题?
        4.当使用foreach进行批量插入时,如何处理可能出现的事务问题?内存不足怎么办?
        5.MyBati foreach批量插入时如何处理死锁问题?
        6.mybatis foreach批量插入时如果数据库连接池耗尽,如何处理?
    b.如何降低单次事务时间?
        优化SQL语句
        分批插入
        调整事务隔离级别
        使用更高效的批量插入方法

01.MyBatis批量入库时,xml的foreach和java的foreach,性能上有什么区别?
    a.Java循环语句一条一条入库
        每一条SQL都需要涉及到一次数据库的操作,包括网络IO以及磁盘IO,效率低下
    b.xml中使用foreach
        一次性发送给数据库执行,只需进行一次网络IO,提高效率
    c.内存溢出OOM问题
        xml中的foreach可能导致内存溢出,因为它会一次性将所有数据加载到内存中
        Java中的foreach可以分批次处理数据,减少内存使用
    d.复杂操作
        Java中的foreach可以直接利用Java的强大功能进行复杂计算或转换

02.在MyBatis中,对于<foreach>标签的使用,通常有几种常见的优化方法?
    a.分批次处理数据
        避免一次性传递过大的数据集合到foreach中
    b.预编译SQL语句
        优化SQL语句,减少foreach编译的工作量
    c.利用mybatis的缓存机制
        减少数据库的访问次数
    d.使用mybatis的懒加载特性
        延迟加载关联数据,减少一次性加载的数据量

03.MyBatis foreach批量插入会有什么问题?
    a.内存消耗
        foreach在处理大量数据时会消耗大量内存,可能导致内存溢出
    b.数据量限制
        有些数据库对单条SQL语句中可以插入的数据量有限制
    c.事务管理
        需要注意事务的管理,部分插入失败可能需要回滚操作
    d.SQL复杂性
        foreach会使SQL语句变得复杂,影响代码的可读性和可维护性

04.当使用foreach进行批量插入时,如何处理可能出现的事务问题?内存不足怎么办?
    a.事务管理
        使用数据库事务确保一组操作要么全部成功,要么全部失败
    b.分批插入
        考虑分批插入,减少内存使用和事务问题
    c.优化SQL语句
        避免因SQL语句过长或过于复杂而导致的问题
    d.使用更高效的批量插入方法
        比如MySQL的INSERT INTO ... VALUES语法

05.MyBatis foreach批量插入时如何处理死锁问题?
    a.优化SQL语句
        确保SQL语句高效,减少事务持有锁的时间
    b.设置锁超时
        为事务设置合理的锁超时时间
    c.使用乐观锁
        乐观锁用于读取频繁、写入较少的场景
    d.分批插入
        考虑分批插入,减少死锁的可能性
    e.调整事务隔离级别
        较低的隔离级别可能减少死锁,但可能导致其他问题

06.MyBatis foreach批量插入时如果数据库连接池耗尽,如何处理?
    a.增加最大连接数
        增加数据库连接池的最大连接数
    b.优化SQL语句
        减少每个连接的使用时间
    c.分批插入
        避免一次性占用过多的连接
    d.调整事务隔离级别
        降低事务隔离级别减少每个事务持有连接的时间
    e.使用更高效的批量插入方法
        比如MySQL的INSERT INTO ... VALUES语法
    f.定期检查并关闭空闲时间过长的连接
        释放连接资源

4.12 [3]mybatisplus批量1:5种

00.汇总
    01.原生批量插入:使用 foreach 标签拼接 SQL,由于拼接的 SQL 过大(4.56M)导致程序执行报错
    02.原生批量插入:INSERT ... ON DUPLICATE KEY UPDATE,允许在插入时,如果遇到主键或唯一键冲突,可以执行更新操作,而不是简单地抛出错误
    03.MP批量插入:saveBatch,底层是单条插入,通过循环遍历待插入的数据集合,并在每次达到指定的批量大小(默认是 1000 条)时执行一次批量插入操作
    04.MP批量插入:saveBatch,开启rewriteBatchedStatements=true
    05.自定义批量1:实现InsertBatchSomeColumn方法批量插入,底层也是拼接sql,但无需手动编写sql语句
    06.自定义批量2:实现InsertBatchSomeColumn方法批量插入,底层也是拼接sql,但无需手动编写sql语句
    07.原生批量插入:使用update+trim实现case+when的拼接,使用insert时,可最后加上ON DUPLICATE KEY UPDATE

01.原生批量插入:使用 foreach 标签拼接 SQL,由于拼接的 SQL 过大(4.56M)导致程序执行报错
    a.说明
        原生批量插入方法的缺点是当拼接的 SQL 过大时,会触发数据库的最大执行 SQL 限制,导致程序执行报错
        这也是为什么 MP 需要分批执行的原因
    b.业务逻辑层扩展:在 UserServiceImpl 添加 saveBatchByNative 方法
        import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
        import com.example.demo.mapper.UserMapper;
        import com.example.demo.model.User;
        import com.example.demo.service.UserService;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.stereotype.Service;

        import java.util.List;

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

            @Autowired
            private UserMapper userMapper;

            public boolean saveBatchByNative(List<User> list) {
                return userMapper.saveBatchByNative(list);
            }
        }
    c.数据持久层扩展:在 UserMapper 添加 saveBatchByNative 方法
        import com.baomidou.mybatisplus.core.mapper.BaseMapper;
        import com.example.demo.model.User;
        import org.apache.ibatis.annotations.Mapper;

        import java.util.List;

        @Mapper
        public interface UserMapper extends BaseMapper<User> {
            boolean saveBatchByNative(List<User> list);
        }
    d.添加 UserMapper.xml:使用 foreach 标签拼接 SQL
        <?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.example.demo.mapper.UserMapper">
            <insert id="saveBatchByNative">
                INSERT INTO `USER`(`NAME`,`PASSWORD`) VALUES
                <foreach collection="list" separator="," item="item">
                    (#{item.name},#{item.password})
                </foreach>
            </insert>
        </mapper>
    e.测试类
        import com.example.demo.model.User;
        import com.example.demo.service.impl.UserServiceImpl;
        import org.junit.jupiter.api.Test;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.boot.test.context.SpringBootTest;

        import java.util.ArrayList;
        import java.util.List;

        @SpringBootTest
        class UserControllerTest {

            // 最大循环次数
            private static final int MAXCOUNT = 100000;

            @Autowired
            private UserServiceImpl userService;

            /**
             * 原生自己拼接 SQL,批量插入
             */
            @Test
            void saveBatchByNative() {
                long stime = System.currentTimeMillis(); // 统计开始时间
                List<User> list = new ArrayList<>();
                for (int i = 0; i < MAXCOUNT; i++) {
                    User user = new User();
                    user.setName("test:" + i);
                    user.setPassword("123456");
                    list.add(user);
                }
                // 批量插入
                userService.saveBatchByNative(list);
                long etime = System.currentTimeMillis(); // 统计结束时间
                System.out.println("执行时间:" + (etime - stime));
            }
        }
        -----------------------------------------------------------------------------------------------------
        当使用原生方法将 10W 条数据拼接成一个 SQL 执行时
        由于拼接的 SQL 过大(4.56M)导致程序执行报错,因为默认情况下 MySQL 可以执行的最大 SQL(大小)为 4M

02.原生批量插入:INSERT ... ON DUPLICATE KEY UPDATE,允许在插入时,如果遇到主键或唯一键冲突,可以执行更新操作,而不是简单地抛出错误
    a.说明
        插入操作:尝试将数据插入到指定的表中
        冲突检测:如果插入的数据导致主键或唯一键冲突(即表中已经存在具有相同键值的记录),则执行 ON DUPLICATE KEY UPDATE 后面的更新操作
        更新操作:更新冲突记录的指定列为新的值
        -----------------------------------------------------------------------------------------------------
        如果 id 为 1 或 2 的记录不存在,则插入新记录
        如果 id 为 1 或 2 的记录已经存在,则更新 name 和 email 列为新的值
    b.业务层
        // 第3步:创建事务
        // 创建事务定义,设置事务传播行为等
        DefaultTransactionDefinition trans = new DefaultTransactionDefinition();
        // 默认为 REQUIRED
        trans.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        // 开始事务
        TransactionStatus status = transactionManager.getTransaction(trans);
        // 统计操作数量
        int insertSysDepartBatchCount = 0;
        int updateSysDepartBatchCount = 0;
        int bulkDeleteSysDepartCount = 0;
        int bulkDeleteSysUserCount = 0;
        int bulkDeleteSysUserDepartCount = 0;
        int bulkDeleteSysDepartPermissionCount = 0;
        int bulkDeleteSysDepartRoleCount = 0;
        int bulkDeleteSysDepartRolePermissionCount = 0;
        try {
            insertSysDepartBatchCount = handleBatchOperationSysDepart(sysDepartInsertData, "insertSysDepartBatch");
            updateSysDepartBatchCount = handleBatchOperationSysDepart(sysDepartUpdateData, "updateSysDepartBatch");
            bulkDeleteSysDepartCount = handleBatchOperationSysDepart(sysDepartDeleteData, "bulkDeleteSysDepart");
            bulkDeleteSysUserCount = handleBatchOperationSysDepart(sysUserDeleteData, "updateSysUserBatch");
            bulkDeleteSysUserDepartCount = handleBatchOperationSysDepart(sysUserDepartDeleteData, "bulkDeleteSysUserDepartByDepartId");
            bulkDeleteSysDepartPermissionCount = handleBatchOperationSysDepart(sysDepartPermissionDeleteData, "bulkDeleteSysDepartPermission");
            bulkDeleteSysDepartRoleCount = handleBatchOperationSysDepart(sysDepartRoleDeleteData, "bulkDeleteSysDepartRole");
            bulkDeleteSysDepartRolePermissionCount = handleBatchOperationSysDepart(sysDepartRolePermissionDeleteData, "bulkDeleteSysDepartRolePermission");
            // 如果所有操作成功,提交事务
            transactionManager.commit(status);
        } catch (Exception e) {
            // 如果发生异常,回滚事务
            transactionManager.rollback(status);
            throw new RuntimeException("【同步部门】事务回滚,操作失败", e);
        }
    c.业务层-工厂类
        /**
         * 【同步部门信息】:批量插入/更新 SysDepart
         *
         * @param list             操作数据集合
         * @param sqlOperationName sql名称
         * @return 操作数据库条数
         */
        private <T> int handleBatchOperationSysDepart(List<T> list, String sqlOperationName) {
            if (ObjectUtil.isEmpty(list)) {
                return 0;
            }
            int count = 0;
            final int batchSize = 3000;
            for (int i = 0, sizes = list.size(); i < sizes; i += batchSize) {
                List<T> batchList = list.subList(i, Math.min(i + batchSize, sizes));
                try {
                    switch (sqlOperationName) {
                        // SysDepart
                        case "insertSysDepartBatch":
                            if (!(batchList.get(0) instanceof SysDepart)) {
                                throw new IllegalArgumentException("insertSysDepartBatch 批量数据类型不匹配,期望 SysDepart 类型");
                            }
                            count += sysDepartMapper.upsertSysDepartBatch((List<SysDepart>) batchList);
                            break;
                        case "updateSysDepartBatch":
                            if (!(batchList.get(0) instanceof SysDepart)) {
                                throw new IllegalArgumentException("updateSysDepartBatch 批量数据类型不匹配,期望 SysDepart 类型");
                            }
                            count += sysDepartMapper.upsertSysDepartBatch((List<SysDepart>) batchList);
                            break;
                        case "bulkDeleteSysDepart":
                            if (!(batchList.get(0) instanceof String)) {
                                throw new IllegalArgumentException("bulkDeleteSysDepart 批量数据类型不匹配,期望 String 类型");
                            }
                            count += sysDepartMapper.bulkDeleteSysDepart((List<String>) batchList);
                            break;
                        default:
                            throw new IllegalArgumentException("不支持的操作类型: " + sqlOperationName);
                    }
                } catch (Exception e) {
                    // 执行数据库,运行过程抛出异常抛出异常
                    throw new RuntimeException("操作数据库异常:" + sqlOperationName, e);
                }
            }
            return count;
        }
    d.持久层
        <?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="cn.jggroup.system.mapper.SysDepartMapper">
          <delete id="bulkDeleteSysDepart" parameterType="java.util.List">
            DELETE FROM sys_depart
            WHERE id IN
            <foreach close=")" collection="batchList" item="item" open="(" separator=",">
              #{item}
            </foreach>
          </delete>

          <!-- 根据username查询所拥有的部门 -->
          <insert id="upsertSysDepartBatch" parameterType="java.util.List">
            INSERT INTO sys_depart (
            id,
            parent_id,
            depart_name,
            depart_name_en,
            depart_name_abbr,
            depart_order,
            description,
            org_category,
            org_type,
            org_code,
            mobile,
            fax,
            address,
            memo,
            status,
            del_flag,
            qywx_identifier,
            create_by,
            create_time,
            update_by,
            update_time
            ) VALUES
            <foreach collection="batchList" item="item" separator=",">
              (
              #{item.id},
              #{item.parentId},
              #{item.departName},
              #{item.departNameEn},
              #{item.departNameAbbr},
              #{item.departOrder},
              #{item.description},
              #{item.orgCategory},
              #{item.orgType},
              #{item.orgCode},
              #{item.mobile},
              #{item.fax},
              #{item.address},
              #{item.memo},
              #{item.status},
              #{item.delFlag},
              #{item.qywxIdentifier},
              #{item.createBy},
              #{item.createTime},
              #{item.updateBy},
              #{item.updateTime}
              )
            </foreach>
            ON DUPLICATE KEY UPDATE
            parent_id = VALUES(parent_id),
            depart_name = VALUES(depart_name),
            depart_name_en = VALUES(depart_name_en),
            depart_name_abbr = VALUES(depart_name_abbr),
            depart_order = VALUES(depart_order),
            description = VALUES(description),
            org_category = VALUES(org_category),
            org_type = VALUES(org_type),
            org_code = VALUES(org_code),
            mobile = VALUES(mobile),
            fax = VALUES(fax),
            address = VALUES(address),
            memo = VALUES(memo),
            status = VALUES(status),
            del_flag = VALUES(del_flag),
            qywx_identifier = VALUES(qywx_identifier),
            create_by = VALUES(create_by),
            create_time = VALUES(create_time),
            update_by = VALUES(update_by),
            update_time = VALUES(update_time);
          </insert>
        </mapper>

03.MP批量插入:saveBatch,底层是单条插入,通过循环遍历待插入的数据集合,并在每次达到指定的批量大小(默认是 1000 条)时执行一次批量插入操作
    a.说明
        MP 的核心实现代码是 saveBatch 方法
        MP 是将要执行的数据分成 N 份,每份 1000 条,每满 1000 条就会执行一次批量插入
        所以它的性能要比循环单次插入的性能高很多
        -----------------------------------------------------------------------------------------------------
        它会将数据按照指定批次大小拆分(默认 1000 条),然后一批一批执行插入。
        每一批次都会生成类似这样的 SQL:INSERT INTO table (col1, col2) VALUES (val1, val2), (val3, val4), ...;
        如果没有复杂逻辑,单批插入的性能应该很高
    b.依赖
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>mybatis-plus-latest-version</version>
        </dependency>
    c.控制器
        import com.example.demo.model.User;
        import com.example.demo.service.impl.UserServiceImpl;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.web.bind.annotation.RequestMapping;
        import org.springframework.web.bind.annotation.RestController;

        import java.util.ArrayList;
        import java.util.List;

        @RestController
        @RequestMapping("/u")
        public class UserController {

            @Autowired
            private UserServiceImpl userService;

            /**
             * 批量插入(自定义)
             */
            @RequestMapping("/mysavebatch")
            public boolean mySaveBatch(){
                List<User> list = new ArrayList<>();
                // 待添加(用户)数据
                for (int i = 0; i < 1000; i++) {
                    User user = new User();
                    user.setName("test:"+i);
                    user.setPassword("123456");
                    list.add(user);
                }
                return userService.saveBatchCustom(list);
            }
        }
    d.业务层
        import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
        import com.example.demo.mapper.UserMapper;
        import com.example.demo.model.User;
        import com.example.demo.service.UserService;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.stereotype.Service;
        import java.util.List;

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

            @Autowired
            private UserMapper userMapper;

            public boolean saveBatchCustom(List<User> list){
                return userMapper.saveBatchCustom(list);
            }
        }
    e.持久层
        import com.baomidou.mybatisplus.core.mapper.BaseMapper;
        import com.example.demo.model.User;
        import org.apache.ibatis.annotations.Mapper;

        import java.util.List;

        @Mapper
        public interface UserMapper extends BaseMapper<User>{
            boolean saveBatchCustom(List<User> list);
        }
    f.测试类
        import com.example.demo.model.User;
        import com.example.demo.service.impl.UserServiceImpl;
        import org.junit.jupiter.api.Test;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.boot.test.context.SpringBootTest;

        import java.util.ArrayList;
        import java.util.List;

        @SpringBootTest
        class UserControllerTest {

            // 最大循环次数
            private static final int MAXCOUNT = 100000;

            @Autowired
            private UserServiceImpl userService;

            /**
             * MP 批量插入
             */
            @Test
            void saveBatch() {
                long stime = System.currentTimeMillis(); // 统计开始时间
                List<User> list = new ArrayList<>();
                for (int i = 0; i < MAXCOUNT; i++) {
                    User user = new User();
                    user.setName("test:" + i);
                    user.setPassword("123456");
                    list.add(user);
                }
                // MP 批量插入
                userService.saveBatch(list);
                long etime = System.currentTimeMillis(); // 统计结束时间
                System.out.println("执行时间:" + (etime - stime));
            }
        }
        -----------------------------------------------------------------------------------------------------
        使用 MP 的批量插入功能(插入数据 10W 条),性能比循环单次插入的性能提升了 14.5 倍

04.MP批量插入:saveBatch,开启rewriteBatchedStatements=true
    a.说明
        对插入而言,所谓的 rewrite 其实就是将一批插入拼接成 insert into xxx values (a),(b),(c)...这样一条语句的形式然后执行,这样一来跟拼接 sql 的效果是一样的
    b.对比
        批量保存                                   方式数据量(条)                     耗时(ms)
        单条循环插入                                1000                              121011
        mybatis-plus saveBatch                     1000                              59927
        mybatis-plus saveBatch(添加rewtire参数)   1000                              2589
        手动拼接sql                                 1000                              2275
        jdbc executeBatch                           1000                             55663
        jdbc executeBatch(添加rewtire参数)         1000                             324
    c.配置
        a.原来
            master:  # 主数据源
              url: jdbc:mysql://127.0.0.1:3307/ydsz-cloud-parent?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
              username: root
              password: 123456
              driver-class-name: com.mysql.cj.jdbc.Driver
        b.加上rewriteBatchedStatements=true
            master:  # 主数据源
              url: jdbc:mysql://127.0.0.1:3307/ydsz-cloud-parent?rewriteBatchedStatements=true
              username: root
              password: 123456
              driver-class-name: com.mysql.cj.jdbc.Driver
        c.那为什么默认不给这个参数设置为 true 呢?
            如果批量语句中的某些语句失败,则默认重写会导致所有语句都失败
            批量语句的某些语句参数不一样,则默认重写会使得查询缓存未命中
    d.mybatis的saveBatch
        a.源码
            public static <E> boolean executeBatch(SqlSessionFactory sqlSessionFactory, Log log,
                                                   Collection<E> list, int batchSize,
                                                   BiConsumer<SqlSession, E> consumer) {
                // 1. 校验批次大小是否有效
                Assert.isFalse(batchSize < 1, "batchSize must not be less than one");

                // 2. 如果集合为空,直接返回 false
                return !CollectionUtils.isEmpty(list) && executeBatch(sqlSessionFactory, log, sqlSession -> {
                    int size = list.size(); // 数据总量
                    int idxLimit = Math.min(batchSize, size); // 当前批次限制
                    int i = 1;

                    // 3. 遍历数据集合
                    for (E element : list) {
                        // 执行具体的插入逻辑(通过 consumer 定义)
                        consumer.accept(sqlSession, element);

                        // 达到当前批次的限制时,flush 数据并更新批次计数
                        if (i == idxLimit) {
                            sqlSession.flushStatements(); // 刷新批量操作到数据库
                            idxLimit = Math.min(idxLimit + batchSize, size); // 更新下一个批次的限制
                        }
                        i++;
                    }
                });
            }
        b.批次大小校验
            Assert.isFalse(batchSize < 1, "batchSize must not be less than one");
            -------------------------------------------------------------------------------------------------
            首先,它会检查 batchSize 是否有效,确保不能小于 1
            如果你传入 batchSize = 0,saveBatch 就直接让你“扑街”——报错毫不客气
            这是为了保证最基本的逻辑正确性:每个批次至少要插一条数据,不然批量插入还有啥意义?
        c.集合为空的优化处理
            return !CollectionUtils.isEmpty(list) && executeBatch(...);
            -------------------------------------------------------------------------------------------------
            如果要插入的数据集合是空的,saveBatch 也会直接短路,返回 false
            这是一个很实用的优化:空集合没必要再浪费资源走后续逻辑
            实战场景中,这种处理可以避免你“辛辛苦苦遍历到最后发现啥都没干”的尴尬
        d.数据分批核心逻辑
            int idxLimit = Math.min(batchSize, size);
            int i = 1;

            for (E element : list) {
                consumer.accept(sqlSession, element); // 执行插入逻辑
                if (i == idxLimit) {
                    sqlSession.flushStatements(); // 刷新当前批次到数据库
                    idxLimit = Math.min(idxLimit + batchSize, size); // 更新批次限制
                }
                i++;
            }
            -------------------------------------------------------------------------------------------------
            分批装箱:它按照 batchSize(批次大小)把数据分成一批一批的“快递箱”。
                      每次最多装 batchSize 条数据,不超过总数据量 size
            送快递(flushStatements):每装满一个箱子,就通过 flushStatements() 把这批数据送到数据库,类似于“快递员来收件”
                                       flushStatements 的本质就是批量提交,把之前 addBatch() 的操作执行到数据库
            更新下一批次:装完一箱后,计算下一批次的数据范围,并继续“装箱”
        e.插入逻辑的自定义
            consumer.accept(sqlSession, element);
            这里的 consumer 是一个自定义的逻辑,定义了“每一条数据的插入操作”。你可以理解为
            这是 saveBatch 的可扩展部分
            通常情况下,consumer 会调用 MyBatis 的 insert 方法,把数据加入 addBatch 列表
            比如:(sqlSession, element) -> sqlSession.insert("namespace.insert", element);
            这让 saveBatch 变得非常灵活——它本身不关心具体的 SQL,而是把实际的插入逻辑交给用户定义
        f.测试类
            @Test
            void MapperSaveBatch() {
                SqlSession sqlSession = sqlSessionFactory.openSession();
                try {
                    List<OpenTest> openTestList = new ArrayList<>();
                    for (int i = 0; i < 1000; i++) {
                        OpenTest openTest = new OpenTest();
                        openTest.setA("a" + i);
                        openTest.setB("b" + i);
                        openTest.setC("c" + i);
                        openTest.setD("d" + i);
                        openTest.setE("e" + i);
                        openTest.setF("f" + i);
                        openTest.setG("g" + i);
                        openTest.setH("h" + i);
                        openTest.setI("i" + i);
                        openTest.setJ("j" + i);
                        openTest.setK("k" + i);
                        openTestList.add(openTest);
                    }
                    StopWatch stopWatch = new StopWatch();
                    stopWatch.start("mapper save batch");
                    //手动拼接批量插入
                    openTestMapper.saveBatch(openTestList);
                    sqlSession.commit();
                    stopWatch.stop();
                    log.info("mapper save batch:" + stopWatch.getTotalTimeMillis());
                } finally {
                    sqlSession.close();
                }
            }
    e.jdbc的saveBatch
        a.说明
            批量提交是通过 Statement 或 PreparedStatement 的 addBatch() 和 executeBatch() 方法实现的
            它的核心思想是将多条 SQL 操作放入一个批量中,减少与数据库的交互次数,从而提升性能
        b.代码
            try (Connection connection = dataSource.getConnection()) {
                String sql = "INSERT INTO table_name (col1, col2, col3) VALUES (?, ?, ?)";
                try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
                    for (int i = 0; i < 1000; i++) {
                        pstmt.setString(1, "value1-" + i);
                        pstmt.setInt(2, i);
                        pstmt.setString(3, "value3-" + i);
                        pstmt.addBatch();  // 将语句加入批量
                    }
                    pstmt.executeBatch();  // 批量执行
                }
            }
        c.说明
            addBatch():将 SQL 添加到批次中
            executeBatch():一次性执行批次中的所有 SQL
            性能提升:减少网络交互:批量提交的所有 SQL 通过一次网络请求发送给数据库
                     减少事务开销:如果不手动控制事务,默认情况下整个批量操作在一个隐式事务中完成
        d.说明
            也就是说,我们忽略分批的情况下,默认一批次的数据插入,不会一条一条的执行
            而是批量打包为一个 insert 语句包,一次性的丢给数据库去执行
            这个过程只需要一次网络传输,而且一个批次的所有记录共用一条 SQL 模板
        e.测试类
            @Test
            void JDBCSaveBatch() throws SQLException {
                SqlSession sqlSession = sqlSessionFactory.openSession();
                Connection connection = sqlSession.getConnection();
                connection.setAutoCommit(false);

                String sql = "insert into open_test(a,b,c,d,e,f,g,h,i,j,k) values(?,?,?,?,?,?,?,?,?,?,?)";
                PreparedStatement statement = connection.prepareStatement(sql);
                try {
                    for (int i = 0; i < 1000; i++) {
                        statement.setString(1,"a" + i);
                        statement.setString(2,"b" + i);
                        statement.setString(3, "c" + i);
                        statement.setString(4,"d" + i);
                        statement.setString(5,"e" + i);
                        statement.setString(6,"f" + i);
                        statement.setString(7,"g" + i);
                        statement.setString(8,"h" + i);
                        statement.setString(9,"i" + i);
                        statement.setString(10,"j" + i);
                        statement.setString(11,"k" + i);
                        statement.addBatch();
                    }
                    StopWatch stopWatch = new StopWatch();
                    stopWatch.start("JDBC save batch");
                    statement.executeBatch();
                    connection.commit();
                    stopWatch.stop();
                    log.info("JDBC save batch:" + stopWatch.getTotalTimeMillis());
                } finally {
                    statement.close();
                    sqlSession.close();
                }
            }

05.自定义批量1:实现InsertBatchSomeColumn方法批量插入,底层也是拼接sql,但无需手动编写sql语句
    a.自定义SQL注入器实现DefaultSqlInjector,添加InsertBatchSomeColumn方法
        a.MySQL版
            public class MySqlInjector extends DefaultSqlInjector {
                @Override
                public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
                    List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);
                    methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE));
                    return methodList;
                }
            }
        b.Oracle版
            import com.baomidou.mybatisplus.annotation.FieldFill;
            import com.baomidou.mybatisplus.core.injector.AbstractMethod;
            import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;

            import java.util.List;

            public class OracleInjector extends DefaultSqlInjector {
                @Override
                public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
                    List<AbstractMethod> methodList = super.getMethodList(mapperClass);
                    methodList.add(new OracleInsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE));
                    return methodList;
                }
            }
    b.编写配置类,把自定义注入器放入spring容器
        @Configuration
        public class MyBatisConfig {
            @Bean
            public OracleInjector sqlInjector() {
                return new OracleInjector();
            }
        }
    c.编写自定义BaseMapper,加入InsertBatchSomeColumn方法
        public interface MyBaseMapper<T> extends BaseMapper<T> {
            int insertBatchSomeColumn(List<T> entityList);
        }
    d.需要批量插入的Mapper继承自定义BaseMapper
        @Mapper
        public interface UserMapper extends MyBaseMapper<Student> {
        }
    e.修改适配Oracle
        a.Oracle批量插入数据SQL
            INSERT ALL
            INTO TABLE_NAME(COLUMN1,COLUMN2...,COLUMNN)VALUES(VALUE1,VALUE2...,VALUEN)
            INTO TABLE_NAME(COLUMN1,COLUMN2...,COLUMNN)VALUES(VALUE1,VALUE2...,VALUEN)
            INTO TABLE_NAME(COLUMN1,COLUMN2...,COLUMNN)VALUES(VALUE1,VALUE2...,VALUEN)
            SELECT * FROM DUAL
        b.编写SQL组装逻辑
            import com.baomidou.mybatisplus.annotation.IdType;
            import com.baomidou.mybatisplus.core.enums.SqlMethod;
            import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
            import com.baomidou.mybatisplus.core.metadata.TableInfo;
            import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
            import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn;
            import lombok.AllArgsConstructor;
            import lombok.NoArgsConstructor;
            import lombok.Setter;
            import lombok.experimental.Accessors;
            import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
            import org.apache.ibatis.executor.keygen.KeyGenerator;
            import org.apache.ibatis.executor.keygen.NoKeyGenerator;
            import org.apache.ibatis.mapping.MappedStatement;
            import org.apache.ibatis.mapping.SqlSource;

            import java.util.List;
            import java.util.Map;
            import java.util.function.Predicate;

            @NoArgsConstructor
            @AllArgsConstructor
            @SuppressWarnings("serial")
            public class OracleInsertBatchSomeColumn extends InsertBatchSomeColumn {
                @Setter
                @Accessors(chain = true)
                private Predicate<TableFieldInfo> predicate;

                private final String INSERT_BATCH_SQL="<script>\nINSERT ALL \n  %s\n</script>";

                @SuppressWarnings("Duplicates")
                @Override
                public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
                    if (tableInfo.getEntityType().equals(Map.class)) {
                        return null;
                    }
                    KeyGenerator keyGenerator = new NoKeyGenerator();
                    SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
                    List<TableFieldInfo> fieldList = tableInfo.getFieldList();
                    String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(false) +
                            this.filterTableFieldInfo(fieldList, predicate, TableFieldInfo::getInsertSqlColumn, EMPTY);
                    String columns = insertSqlColumn.substring(0, insertSqlColumn.length() - 1) ;
                    String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(ENTITY_DOT, false) +
                            this.filterTableFieldInfo(fieldList, predicate, i -> i.getInsertSqlProperty(ENTITY_DOT), EMPTY);
                    insertSqlProperty = LEFT_BRACKET + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + RIGHT_BRACKET;
                    String valuesScript = convertForeach(insertSqlProperty, "list", tableInfo.getTableName(),columns, ENTITY, NEWLINE);
                    String keyProperty = null;
                    String keyColumn = null;
                    if (tableInfo.havePK()) {
                        if (tableInfo.getIdType() == IdType.AUTO) {
                            keyGenerator = new Jdbc3KeyGenerator();
                            keyProperty = tableInfo.getKeyProperty();
                            keyColumn = tableInfo.getKeyColumn();
                        } else {
                            if (null != tableInfo.getKeySequence()) {
                                keyGenerator = TableInfoHelper.genKeyGenerator(getMethod(sqlMethod), tableInfo, builderAssistant);
                                keyProperty = tableInfo.getKeyProperty();
                                keyColumn = tableInfo.getKeyColumn();
                            }
                        }
                    }
                    String sql = String.format(INSERT_BATCH_SQL, valuesScript);
                    SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
                    return this.addInsertMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource, keyGenerator, keyProperty, keyColumn);
                }
                public static String convertForeach(final String sqlScript, final String collection, final String tableName,final String columns, final String item, final String separator) {
                    StringBuilder sb = new StringBuilder("<foreach");

                    if (StringUtils.isNotBlank(collection)) {
                        sb.append(" collection=\"").append(collection).append("\"");
                    }

                    if (StringUtils.isNotBlank(item)) {
                        sb.append(" item=\"").append(item).append("\"");
                    }

                    if (StringUtils.isNotBlank(separator)) {
                        sb.append(" separator=\"").append(separator).append("\"");
                    }

                    sb.append(">").append("\n");

                    if (StringUtils.isNotBlank(tableName)) {
                        sb.append(" INTO ").append(tableName).append(" ");
                    }

                    if (StringUtils.isNotBlank(columns)) {
                        sb.append(LEFT_BRACKET).append(columns).append(RIGHT_BRACKET).append(" VALUES ");
                    }

                    return sb.append(sqlScript).append("\n").append("</foreach>\n").append(" SELECT ").append("*").append(" FROM dual").toString();
                }
            }
    f.执行批量插入报错解决
        a.错误描述
            Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
            [Request processing failed; nested exception is org.mybatis.spring.MyBatisSystemException:
            nested exception is org.apache.ibatis.type.TypeException: Could not set parameters for mapping:
            ParameterMapping{property='__frch_et_0.serialno', mode=IN, javaType=class java.lang.String,
            jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}.
            Cause: org.apache.ibatis.type.TypeException: Error setting null for parameter #2 with JdbcType
            OTHER . Try setting a different JdbcType for this parameter or a different jdbcTypeForNull
            configuration property. Cause: java.sql.SQLException: 无效的列类型: 1111] with root cause
        b.解决方法
            a.第一种是指定实体所有属性的jdbcType类型
                import com.baomidou.mybatisplus.annotation.TableField;
                import com.baomidou.mybatisplus.extension.activerecord.Model;
                import lombok.Data;
                import org.apache.ibatis.type.JdbcType;

                import java.io.Serializable;
                import java.util.Date;

                @SuppressWarnings("serial")
                @Data
                public class TzVerifyLog extends Model<TzVerifyLog> {
                    private String id;
                    @TableField(value = "serialno",jdbcType = JdbcType.VARCHAR)
                    private String serialno;
                    @TableField(value = "verify_msg",jdbcType = JdbcType.VARCHAR)
                    private String verifyMsg;
                    @TableField(value = "type",jdbcType = JdbcType.VARCHAR)
                    private String type;
                    @TableField(value = "row_num",jdbcType = JdbcType.INTEGER)
                    private Integer rowNum;

                    @TableField(value = "createtime",jdbcType = JdbcType.DATE)
                    private Date createtime;

                    @Override
                    protected Serializable pkVal() {
                        return this.id;
                    }
                }
            b.第二种是设置mybatisplus的jdbc-type-for-null属性值
                mybatis-plus:
                  configuration:
                    jdbc-type-for-null: varchar #空值时设置为varchar类型
    g.service封装InsertBatchSomeColumn方法
        a.新建一个IMyService接口继承IService
            import com.baomidou.mybatisplus.extension.service.IService;

            import java.util.List;

            public interface IMyService <T> extends IService<T> {
                int insertBatchSomeColumn(List<T> entityList);
                int insertBatchSomeColumn(List<T> entityList,int batchSize);
            }
        b.新建一个MyServiceImpl类继承ServiceImpl
            import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

            import java.util.ArrayList;
            import java.util.List;

            public class MyServiceImpl<M extends MyBaseMapper<T>, T>extends ServiceImpl<M,T> implements IMyService<T> {
                @Override
                public int insertBatchSomeColumn(List<T> entityList) {
                    return this.baseMapper.insertBatchSomeColumn(entityList);
                }

                @Override
                public int insertBatchSomeColumn(List<T> entityList, int batchSize) {
                    int size=entityList.size();
                    if(size<batchSize){
                        return this.baseMapper.insertBatchSomeColumn(entityList);
                    }
                    int page=1;
                    if(size % batchSize ==0){
                        page=size/batchSize;
                    }else {
                        page=size/batchSize+1;
                    }
                    for (int i = 0; i < page; i++) {
                        List<T> sub = new ArrayList<>();
                        if(i==page-1){
                            sub=entityList.subList(i*batchSize, entityList.size());
                        }else {
                            sub.subList(i*batchSize,(i+1)*batchSize);
                        }
                        if(sub.size()>0){
                            baseMapper.insertBatchSomeColumn(sub);
                        }
                    }
                    return size;
                }
            }
    h.实体Service接口和接口实现类继承IMyService和MyServiceImpl
        a.代码示例
            public interface ITzVerifyLogService extends IMyService<TzVerifyLog> {
            }
        b.代码示例
            import org.springframework.stereotype.Service;

            @Service
            public class TzVerifyLogServiceImpl extends MyServiceImpl<TzVerifyLogMapper, TzVerifyLog> implements ITzVerifyLogService {
            }

06.自定义批量2:实现InsertBatchSomeColumn方法批量插入,底层也是拼接sql,但无需手动编写sql语句
    a.概况
        a.说明
            a.介绍
                其实数据量不大的时候,批量提交和拼接一条 sql 的效率并不是有很大差异
                但是当数据量巨大的时候,拼接 sql 的效率居然明显下降了,这就更难怪MyBatis-Plus 不选用他啦
                朋友,这就是江湖的复杂之处。当我们看着 insertBatchSomeColumn 的拼接 SQL
                心里已经开始沸腾:“这不就是我想要的批量插入吗?用一条 SQL 搞定一切,岂不是吊打 saveBatch?”
                从设计的角度,MyBatis-Plus 没有默认用 insertBatchSomeColumn 是有原因的
            b.开发者描述
                批量新增数据,自选字段 insert
                不同的数据仓库支持度不一样!!! 只在 mysql 下测试过!!! 只在 mysql 下测试过!!!
                除了主键是 数据库自增的未测试 外理论上都可以使用!!!
                如果你使用自增有报错或者主键值无法回写到entity,就不要跑来问为什么了,因为我也不知道!!!
                自己的通用 mapper 如下使用:
                int insertBatchSomeColumn(List entityList);
                注意:这是自选字段 insert !!,如果个别字段在 entity 里为 null 但是数据库中有配置默认值,
                常用的 Predicate :
                例1:t -> !t.isLogicDelete(),表示不要逻辑删除字段
                例2:t -> !t.getProperty().equals("version"),表示不要字段名为 version 的字段
                例3:t -> t.getFieldFill() != FieldFill.UPDATE,表示不要填充策略为 UPDATE 的字段
                Since: 2018-11-29
                Author: miemie
        b.场景局限性:数据量太大,SQL 太长
            a.说明
                insertBatchSomeColumn 最大的特点是用一条 SQL 插入多行数据
                但是,这种方式有一个致命的短板:数据量太大时,SQL 太长,直接爆掉
            b.问题一:SQL 长度限制
                大部分数据库对 SQL 语句的长度有限制(比如 MySQL 默认是 4MB)
                如果你用 insertBatchSomeColumn 插入 10 万条数据,这条 SQL 很可能直接超出限制,报错了事
            c.问题二:解析压力
                SQL 语句越长,数据库解析 SQL 的时间也越长
                如果你拼了一个超级长的 VALUES,即使没有超过限制,数据库的解析过程也可能拖慢插入效率
            d.对比saveBatch:
                它一次只发送一小批数据到数据库(比如 1000 条),SQL 很短,既不会超限,也不会给数据库解析带来太大压力
        c.适配性问题:不同数据库的行为差异
            a.说明
                ORM 框架最重要的特点是 “通用性”
                它得让你的代码在 MySQL、PostgreSQL、Oracle 这些数据库上都跑得动
                但问题来了,不同数据库对批量插入的支持并不一致
            b.MySQL 的快乐
                在 MySQL 中,insertBatchSomeColumn 是天然适配的
                VALUES 部分可以支持多个记录,性能也很好
            c.Oracle 并不直接支持类似 MySQL 的多行插入语法
                虽然可以通过 INSERT ALL 或 UNION ALL 来模拟多行插入
                但这些方法在语法上较为繁琐,且性能可能不如 MySQL 的原生支持高效
                尤其是 UNION ALL 的方式,在处理大量数据时可能会导致性能下降
                Oracle也提供了批量绑定:在 PL/SQL 中使用 FORALL 语句的方式,但是相较于原生支持复杂度还是较高
            d.PostgreSQL的问题
                PostgreSQL 虽然支持多行 VALUES,但批量插入的性能未必比传统的逐条插入高,具体表现还取决于场景
            e.对比saveBatch
                saveBatch 是基于 JDBC 的 addBatch 和 executeBatch 实现的
                JDBC 是数据库驱动的统一接口,各种数据库都支持,兼容性更好
        d.灵活性:字段动态控制
            a.说明
                insertBatchSomeColumn 的字段是固定的,它会根据 TableInfo 中定义的字段来拼接 SQL
                如果你的某条记录需要动态处理某些字段,比如:
                某些字段需要特定条件下赋值
                某些字段需要根据数据库默认值处理
            c.缺陷
                这时候,insertBatchSomeColumn 就显得“呆板”了
                对比saveBatch:saveBatch 的每条记录是独立的,可以动态调整每条记录的字段内容,更灵活
        e.批量插入的江湖哲学
            a.saveBatch:稳健的“江湖中人”
                a.说明
                    saveBatch 是 MyBatis-Plus 的默认实现
                    它基于 JDBC 的批处理能力,具有稳定性强、兼容性广、实现简单的特点
                    无论你的数据库是 MySQL、Oracle,还是 PostgreSQL,saveBatch 都能满足大多数场景的需求
                    虽然它在性能上可能不如理想中的批量插入快,但它能在数据量大、逻辑复杂的情况下稳稳地完成任务
                b.适合的场景
                    数据量较大(比如超过 10 万条)
                    表结构复杂,字段插入需要动态控制
                    需要在多种数据库之间切换,追求通用性
            b.insertBatchSomeColumn:锋利的“武林奇兵”
                a.说明
                    insertBatchSomeColumn 则是为高性能场景量身定制的“快刀”
                    它通过动态拼接 SQL,将多条数据用一条 SQL 插入数据库,极大地提升了插入效率
                    然而,它的优势是建立在特定场景的基础上
                    比如只支持 MySQL、无法很好处理自增主键、需要开发者精心配置字段等
                b.适合的场景
                    数据库是 MySQL,且对多行 VALUES 优化非常好
                    数据量适中(几千到几万条,不超过 SQL 长度限制)
                    表结构简单,字段固定,不依赖数据库默认值
    b.创建自定义 SQL 注入器:MyBatis-Plus 支持通过扩展 DefaultSqlInjector 来添加自定义 SQL 方法
        public class MySqlInjector extends DefaultSqlInjector {

            @Override
            public List<AbstractMethod> getMethodList(Configuration configuration, Class<?> mapperClass, TableInfo tableInfo) {
                // 从全局配置中获取数据库配置
                GlobalConfig.DbConfig dbConfig = GlobalConfigUtils.getDbConfig(configuration);

                // 构建基础方法列表
                Stream.Builder<AbstractMethod> builder = Stream.<AbstractMethod>builder()
                        .add(new Insert(dbConfig.isInsertIgnoreAutoIncrementColumn())) // 普通插入
                        .add(new Delete())                                            // 删除
                        .add(new Update())                                            // 更新
                        .add(new SelectCount())                                       // 查询总数
                        .add(new SelectMaps())                                        // 查询 Map 集合
                        .add(new SelectObjs())                                        // 查询单个字段集合
                        .add(new SelectList());                                       // 查询列表

                // 如果表有主键,添加 ById 系列方法
                if (tableInfo.havePK()) {
                    builder.add(new DeleteById())
                            .add(new DeleteByIds())
                            .add(new UpdateById())
                            .add(new SelectById())
                            .add(new SelectByIds());
                } else {
                    logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.",
                            tableInfo.getEntityType()));
                }

                // **重点:添加 `insertBatchSomeColumn` 方法**
                // 这里会自动跳过标记为 `FieldFill.UPDATE` 的字段,确保不会插入更新字段
                builder.add(new InsertBatchSomeColumn(column -> column.getFieldFill() != FieldFill.UPDATE));

                return builder.build().collect(Collectors.toList());
            }
        }
    c.配置注入器到 MyBatis-Plus
        @Configuration
        public class MybatisPlusConfig {

            /**
             * 配置分页插件(如果有分页需求)
             */
            @Bean
            public MybatisPlusInterceptor mybatisPlusInterceptor() {
                MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
                interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // MySQL 分页
                return interceptor;
            }

            /**
             * 注册自定义的 SQL 注入器
             */
            @Bean
            public MySqlInjector mySqlInjector() {
                return new MySqlInjector();
            }
        }
    d.在 Mapper 接口中添加 insertBatchSomeColumn 方法
        @Mapper
        public interface UserMapper extends BaseMapper<User> {

            /**
             * 批量插入(使用 `insertBatchSomeColumn`)
             *
             * @param list 数据集合
             * @return 插入条数
             */
            int insertBatchSomeColumn(@Param("list") List<User> list);
        }
    e.在 Service 中调用 insertBatchSomeColumn
        @Resource
        private ImUserDataMapper imUserDataMapper;

        /**
         * 批量插入用户数据
         *
         * @param userList 用户数据列表
         */
        public void batchInsertUsers(List<ImUserData> userList) {
            imUserDataMapper.insertBatchSomeColumn(userList);
        }
    f.测试类
        List<Callable<Void>> tasks = new ArrayList<>();
            for (List<ImportUserData> batch : userDataBatches) {
                tasks.add(() -> {
                    List<ImUserData> imUserDataList = batch.stream()
                            .map(data -> ImUserAdapter.buildImUserData(data, appId))
                            .collect(Collectors.toList());
                    try {
                        // 批量插入
                        imUserDataDao.batchInsertUsers(imUserDataList);
                        imUserDataList.forEach(user -> successId.add(user.getUserId()));
                    } catch (Exception e) {
                        log.error("批量插入失败: {}", e.getMessage(), e);
                        imUserDataList.forEach(user -> failedId.add(user.getUserId()));
                    }
                    return null;
                });
            }

07.原生批量插入:使用update+trim实现case+when的拼接,使用insert时,可最后加上ON DUPLICATE KEY UPDATE
    a.使用update+trim实现case+when的拼接
        <update id="updateForBatch" parameterType="cn.net.susan.entity.sys.UserEntity">
          update sys_user
          <trim prefix="set" suffixOverrides=",">
              <trim prefix="password = case id" suffix="end,">
                  <foreach collection="list" item="item">
                      when #{item.id} then #{item.password}
                  </foreach>
              </trim>
              <trim prefix="update_user_id = case id" suffix="end,">
                  <foreach collection="list" item="item">
                      when #{item.id} then #{item.updateUserId}
                  </foreach>
              </trim>
              <trim prefix="update_user_name = case id" suffix="end">
                  <foreach collection="list" item="item">
                      when #{item.id} then #{item.updateUserName}
                  </foreach>
              </trim>
          </trim>
          <where>
              id in (
              <foreach collection="list" separator="," item="item">
                  #{item.id}
              </foreach>
              )
          </where>
        </update>  
    b.使用insert时,可最后加上ON DUPLICATE KEY UPDATE
         <update id="updateForBatch" parameterType="cn.net.susan.entity.sys.UserEntity">
            insert into sys_user
            (id,username,password) values
            <foreach collection="list" index="index" item="item" separator=",">
                (#{item.id},
                #{item.username},
                #{item.password})
            </foreach>
            ON DUPLICATE KEY UPDATE
             password=values(password)
        </update>

4.13 [3]mybatisplus批量2:4种

00.汇总
    a.数据
        为了保证足够的数据量,每次筛选出5000条数据进行插入,每条数据具有11个字段,大约100个字节,总体数据大小约500KB
    b.4个方案
        1.逐个保存方案:遍历5000条数据,逐个使用save方式进行保存
        2.多线程逐个保存方案:新建线程池,其中线程数量为5个,遍历5000条数据时每次新建一个任务扔到线程池中进行处理,线程使用save的方式进行保存
        3.saveBatch方案:不设置saveBatch的batchSize参数,直接将5000条数据的list放入方法中进行批量保存
        4.多线程saveBatch方案:新建线程池,其中线程数量为5个,遍历5000条数据时将数据均分成5个list,分别放到线程池中进行执行,线程使用saveBatch的方式直接将1000条数据进行批量保存

01.四个方案
    a.逐个保存方案
        a.说明
            遍历5000条数据,逐个使用save方式进行保存
        b.代码
            public String dbDataTest() {
                // 获取能源数据列表
                List<Energy> dataList = getEnergy();
                // 记录开始时间
                long start = System.currentTimeMillis();
                // 遍历数据列表,将每个能源数据转换为能源测试数据并保存到数据库
                for (Energy energy : dataList) {
                    // 创建能源测试数据对象
                    EnergyTest test = EnergyTest.builder().id(energy.getId()).buildId(energy.getBuildId()).buildName(energy.getBuildName())
                    .collectionTime(energy.getCollectionTime()).dataType(energy.getDataType()).usageData(energy.getUsageData())
                    .meterId(energy.getMeterId()).meterName(energy.getMeterName()).meterType(energy.getMeterType())
                    .reading(energy.getReading()).totalSurface(energy.getTotalSurface()).build();
                    // 将能源测试数据保存到数据库
                    energyTestService.save(test);
                }
                // 记录结束时间
                long end = System.currentTimeMillis();
                // 返回执行时间
                return end - start + "ms";
            }
    b.多线程逐个保存方案
        a.说明
            新建线程池,其中线程数量为5个,遍历5000条数据时每次新建一个任务扔到线程池中进行处理,线程使用save的方式进行保存
            -------------------------------------------------------------------------------------------------
            以上多线程的执行方式采用submit有future的返回方式,任务放入线程池后保存future对象
            后续手动进行get请求,保证计时内的任务都执行完毕
            因为如果使用这种异步方式直接保存,计时器只会统计扔到线程池的时间,大概5ms就能结束,不具备参考的意义
        b.代码
            public String dbDataTest2() throws ExecutionException, InterruptedException {
                // 获取能源数据列表
                List<Energy> dataList = getEnergy();
                // 记录开始时间
                long start = System.currentTimeMillis();
                //创建线程池
                ExecutorService executorService = Executors.newFixedThreadPool(5);
                // 创建一个用于存储异步任务执行结果的列表
                List<Future<?>> futures = new ArrayList<>();
                for (Energy energy : dataList) {
                    // 创建能源测试数据对象
                    EnergyTest test = EnergyTest.builder().id(energy.getId()).buildId(energy.getBuildId()).buildName(energy.getBuildName())
                    .collectionTime(energy.getCollectionTime()).dataType(energy.getDataType()).usageData(energy.getUsageData())
                    .meterId(energy.getMeterId()).meterName(energy.getMeterName()).meterType(energy.getMeterType())
                    .reading(energy.getReading()).totalSurface(energy.getTotalSurface()).build();
                    // 将能源测试数据保存到数据库
                    futures.add(executorService.submit(() -> {
                        energyTestService.save(test);
                        return "null";
                    }));
                }
                //获取异步任务执行结果
                for (Future<?> future : futures) {
                    future.get();
                }
                // 记录结束时间
                long end = System.currentTimeMillis();
                // 返回执行时间
                return end - start + "ms";
            }
    c.saveBatch方案
:       a.说明
            不设置saveBatch的batchSize参数,直接将5000条数据的list放入方法中进行批量保存
        b.代码
            public String dbDataTest3() {
                // 获取能源数据列表
                List<Energy> dataList = getEnergy();
                // 记录开始时间
                long start = System.currentTimeMillis();
                // 创建一个用于存储 EnergyTest 对象的列表
                List<EnergyTest> testList = new ArrayList<>();
                // 遍历数据列表,将每个能源数据转换为能源测试数据并添加到 testList 中
                for (Energy energy : dataList) {
                    // 创建能源测试数据对象
                    EnergyTest test = EnergyTest.builder().id(energy.getId()).buildId(energy.getBuildId()).buildName(energy.getBuildName())
                    .collectionTime(energy.getCollectionTime()).dataType(energy.getDataType()).usageData(energy.getUsageData())
                    .meterId(energy.getMeterId()).meterName(energy.getMeterName()).meterType(energy.getMeterType())
                    .reading(energy.getReading()).totalSurface(energy.getTotalSurface()).build();
                    // 将能源测试数据添加到 testList 中
                    testList.add(test);
                }
                // 将 testList 中的所有能源测试数据批量保存到数据库
                energyTestService.saveBatch(testList);
                // 记录结束时间
                long end = System.currentTimeMillis();
                // 返回执行时间
                return end - start + "ms";
            }
    d.多线程saveBatch方案
        a.说明
            新建线程池,其中线程数量为5个,遍历5000条数据时将数据均分成5个list,分别放到线程池中进行执行
            线程使用saveBatch的方式直接将1000条数据进行批量保存
            -------------------------------------------------------------------------------------------------
            以上多线程的执行方式采用submit有future的返回方式,任务放入线程池后保存future对象
            后续手动进行get请求,保证计时内的任务都执行完毕
            因为如果使用这种异步方式直接保存,计时器只会统计扔到线程池的时间,大概5ms就能结束,不具备参考的意义
        b.代码
            public String dbDataTest4() throws ExecutionException, InterruptedException {
                // 获取能源数据列表
                List<Energy> dataList = getEnergy();
                // 记录开始时间
                long start = System.currentTimeMillis();
                //创建线程池
                ExecutorService executorService = Executors.newFixedThreadPool(5);
                // 用于存储分批后的能源测试数据
                Map<String, List<EnergyTest>> testListMap = new HashMap<>(8);
                // 标记当前批次
                int saveFlag = 0;
                // 遍历数据列表,将每个能源数据转换为能源测试数据并添加到对应的批次中
                for (Energy energy : dataList) {
                    EnergyTest test = EnergyTest.builder().id(energy.getId()).buildId(energy.getBuildId()).buildName(energy.getBuildName())
                    .collectionTime(energy.getCollectionTime()).dataType(energy.getDataType()).usageData(energy.getUsageData())
                    .meterId(energy.getMeterId()).meterName(energy.getMeterName()).meterType(energy.getMeterType())
                    .reading(energy.getReading()).totalSurface(energy.getTotalSurface()).build();
                    // 如果当前批次的列表不存在或大小超过1000,则创建新的批次
                    if (!testListMap.containsKey(String.valueOf(saveFlag)) || testListMap.get(String.valueOf(saveFlag)).size() >= 1000) {
                        saveFlag++;
                        testListMap.put(String.valueOf(saveFlag), new ArrayList<>());
                    }
                    // 将能源测试数据添加到当前批次的列表中
                    testListMap.get(String.valueOf(saveFlag)).add(test);
                }
                // 创建一个用于存储异步任务执行结果的列表
                List<Future<?>> futures = new ArrayList<>();
                // 遍历批次列表,将每个批次的能源测试数据批量保存到数据库
                for (Map.Entry<String, List<EnergyTest>> entry : testListMap.entrySet()) {
                    List<EnergyTest> testList = entry.getValue();
                    // 提交异步任务,将当前批次的数据批量保存到数据库
                    futures.add(executorService.submit(() -> {
                        energyTestService.saveBatch(testList);
                        return "null";
                    }));
                }
                // 获取异步任务执行结果
                for (Future<?> future : futures) {
                    future.get();
                }
                // 记录结束时间
                long end = System.currentTimeMillis();
                // 返回执行时间
                return end - start + "ms";
            }

02.第一次测试(不设置rewriteBatchedStatements=true)
    测试批次    耗时逐个保存方案    多线程逐个保存方案  saveBatch方案    多线程saveBatch方案
    1           1461ms            514ms               432ms            167ms
    2           1432ms            544ms               416ms            170ms
    3           1347ms            539ms               428ms            163ms
    4           1288ms            486ms               413ms            184ms
    5           1434ms            560ms               440ms            168ms
    6           1460ms            513ms               462ms            188ms
    7           1453ms            480ms               466ms            194ms
    8           1435ms            477ms               459ms            170ms
    9           1508ms            491ms               408ms            160ms
    10          1437ms            484ms               417ms            178ms
    最大值      1508ms            560ms               466ms            194ms
    最小值      1288ms            477ms               408ms            160ms
    平均值      1425.5ms          508.8ms             434.1ms          174.2ms

03.第二次测试(设置rewriteBatchedStatements=true)
    测试批次    耗时逐个保存方案    多线程逐个保存方案  saveBatch方案    多线程saveBatch方案
    1           1536ms            505ms               244ms            106ms
    2           1591ms            495ms               277ms            89ms
    3           1628ms            510ms               261ms            96ms
    4           1618ms            487ms               281ms            100ms
    5           1581ms            519ms               258ms            111ms
    6           1655ms            515ms               264ms            112ms
    7           1618ms            508ms               271ms            103ms
    8           1507ms            519ms               282ms            98ms
    9           1531ms            509ms               280ms            85ms
    10          1651ms            507ms               287ms            96ms
    最大值      1655ms            519ms               287ms            112ms
    最小值      1507ms            487ms               244ms            85ms
    平均值      1591.6ms          507.4ms             270.5ms          99.6ms

04.总结rewriteBatchedStatements=true的作用
    a.JDBC批处理机制
        a.说明
            JDBC批处理机制是一种优化数据库操作性能的技术,允许将多条SQL语句作为一个批次发送到数据库服务器执行
            从而减少客户端与数据库之间的交互次数,显著提高性能,通常用于批量插入、批量更新和批量删除等场景
        b.代码
            //创建 PreparedStatement 对象,用于定义批处理的 SQL 模板。
            PreparedStatement pstmt = conn.prepareStatement(sql);
            for (Data data : dataList) {
                // 多次调用 addBatch() 方法,每次调用都会将一条 SQL 加入批处理队列。
                pstmt.addBatch();
            }
            //执行批处理,调用 executeBatch() 方法,批量发送 SQL 并执行。
            pstmt.executeBatch();
    b.MySQL JDBC 驱动的默认行为对批处理的影响
        a.未开启重写
            在默认状态下,MySQL JDBC驱动会逐一条目地发送批处理中的SQL语句,未开启重写功能
        b.性能瓶颈
            频繁的网络交互以及数据库解析操作,使得批量操作的性能提升效果有限,形成了性能瓶颈
    c.rewriteBatchedStatements=true
        a.启用批处理重写
            启用批处理重写功能后,驱动能够将多条同类型的SQL语句进行合并,进而发送给数据库执行
        b.减少网络交互
            一次发送多条SQL,可有效降低网络延迟,减少网络交互次数
        c.提高执行效率
            当所有数据都通过一条SQL插入时,MySQL只需要解析一次SQL,降低了解析和执行的开销
        d.减少内存消耗
            虽然批量操作时将数据合并到一条SQL中,理论上会增加内存使用(因为需要构建更大的SQL字符串)
            但相比多次单条插入的网络延迟和处理开销,整体的资源消耗和执行效率是更优的
        e.未开启参数时的批处理SQL
            INSERTINTO question (exam_id, content) VALUES (?, ?);
            INSERT INTO question (exam_id, content) VALUES (?, ?);
            INSERT INTO question (exam_id, content) VALUES (?, ?);
        f.开启参数后的批处理 SQL
            INSERT INTO question (exam_id, content) VALUES (?, ?), (?, ?), (?, ?);

4.14 [3]CompletableFuture批量

01.背景
    a.问题
        我们通常在项目中都会涉及到接收多天的原始数据,然后生成每天的数据报告保存到DB
        如果是循环每天数据顺序的执行生成每天报告保存DB,会有下面的问题:
        1.整个流程变成了串行,耗时较长
        2.写入DB的操作也是一条一条数据写入,没有批量写入效率高
    b.代码
        /**
         * 测试顺序串行生成报告,汇总批量保存DB
         */
        @Test
        public void testSequence() {
            long start = System.currentTimeMillis();
            // 模拟每天的数据
            List<String> days = new ArrayList<>();
            days.add("2024-03-01");
            days.add("2024-03-02");
            days.add("2024-03-03");

            // 循环每天的数据,生成每天报告
            List<DayReport> reportList = new ArrayList<>();
            for(String day: days) {
                DayReport result = generateDayReportTask(day);
                reportList.add(result);
            }

            // 汇总的报告list,批量保存到DB,提高写入的性能
            insertBatch(reportList);
            long execTime = System.currentTimeMillis() - start;
            log.info("执行耗时:{} ms", execTime);
        }
    c.说明
        如果是直接把生成报告的任务提交到线程池处理
        主线程需要借助countDownLatch并发工具类等待线程池里面的任务执行完毕之后执行insertBatch(reportList)操作
        代码实现上稍显复杂,同时还需考虑多个线程保存任务结果到reportList等线程安全问题
        -----------------------------------------------------------------------------------------------------
        所以针对上面的问题,引入CompletableFuture工具,实现并发处理任务,并汇总结果批量保存DB,以此带来效率上的提升
        同时使用更加简单而且也不存在线程安全问题

02.CompletableFuture:并发处理+汇总批量保存
    a.测试类
        /**
         * @ClassName CompletableFutureTest
         * @Description
         * @Author
         **/
        @RunWith(SpringRunner.class)
        @SpringBootTest
        @Slf4j
        public class CompletableFutureTest {


            @Autowired
            @Qualifier("SummaryReportTask")
            ThreadPoolExecutor summaryReportTask;

            @Data
            private class DayReport{
                /**
                 * 报告id
                 */
                private Long reportId;

                /**
                 * 每天的日期
                 */
                private String day;

                /**
                 * 是否执行异常
                 */
                private Boolean ex = false;

                /**
                 * 走路的步数
                 */
                private int stepCount;

                public DayReport(Long reportId, String day, int stepCount) {
                    this.reportId = reportId;
                    this.day = day;
                    this.stepCount = stepCount;
                }

                public DayReport(String day, Boolean ex) {
                    this.day = day;
                    this.ex = ex;
                }
            }

            /**
             * 生成每天报告
             * @param day
             * @return
             */
            private DayReport generateDayReportTask(String day) {
                log.info("模拟生成{}的报告...", day);
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 报告id
                Long reportId = Long.parseLong(day.replace("-", ""));
                // 每天行走的步数
                int stepCount = RandomUtil.randomInt(1, 100);
                return new DayReport(reportId, day, stepCount);
            }

            /**
             * 处理任务执行产生的异常
             * @param e
             * @param day
             * @return
             */
            private DayReport handleException(Throwable e, String day) {
                // 打印异常信息,便于排查问题
                log.error("day: {}的任务执行异常:{}", day, e);
                // 返回异常标记的结果,便于后续判断任务是否出现异常,终止后续的业务流程
                return new DayReport(day, true);
            }


            /**
             * 并发生成报告,汇总批量保存DB
             */
            @Test
            public void testCompletableFuture() {
                long start = System.currentTimeMillis();
                // 模拟每天的数据
                List<String> days = new ArrayList<>();
                days.add("2024-03-01");
                days.add("2024-03-02");
                days.add("2024-03-03");

                List<CompletableFuture<DayReport>> futures = new ArrayList<>();
                // 循环每天的数据,使用CompletableFuture实现并发生成每天报告
                for(String day: days) {
                    CompletableFuture<DayReport> future = CompletableFuture
                            // 提交生成报告任务到指定线程池,异步执行
                            .supplyAsync(() -> generateDayReportTask(day), summaryReportTask)
                            // 任务执行异常时,处理异常
                            .exceptionally(e -> handleException(e, day));
                    // future对象添加到集合中
                    futures.add(future);

                }

                try {
                    // allOf方法等待所有任务执行完毕,最好设置超时时间以免长时间阻塞主线程
                    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(20, TimeUnit.SECONDS);
                } catch (Exception e) {
                    log.error(" CompletableFuture.allOf异常: {}", e);
                    // 出现异常,终止后续逻辑
                    return;
                }
                // 循环获取任务执行返回的结果(即生成的每日报告)
                List<DayReport> reportList = new ArrayList<>();
                for (CompletableFuture<DayReport> future : futures) {
                    DayReport result;
                    try {
                        result = future.get();
                    } catch (Exception e) {
                        log.error("future.get出现异常:{}", e);
                        // 任何一个任务执行异常,则直接return,中断后续业务流程,防止产生的汇总报告不完整
                        return;
                    }
                    // 每日报告汇总
                    if(null != result && !result.getEx()) { // 判断任务执行没有出现异常
                        reportList.add(result);
                    } else {
                        log.error("result为null或者任务执行出现异常");
                        // 任何一个任务执行异常,则直接return,中断后续业务流程,防止产生的汇总报告不完整
                        return;
                    }
                }

                // 汇总的报告list,批量保存到DB,提高写入的性能
                insertBatch(reportList);
                long execTime = System.currentTimeMillis() - start;
                log.info("执行耗时:{} ms", execTime);
            }

            void insertBatch(List<DayReport> reportList) {
                log.info("报告批量保存reportList:{}", JSON.toJSONString(reportList));
            }
    b.线程池配置
        @Configuration
        @Slf4j
        public class ExecutorConfig {


            /**
             * 处理每日汇总报告线程池
             * @return
             */
            @Bean("SummaryReportTask")
            public ThreadPoolExecutor summaryReportTaskExecutor() {
                int corePoolSize = cpuCores();
                int maxPoolSize = corePoolSize * 2;
                ThreadPoolExecutor threadPoolExecutor =  new ThreadPoolExecutor(
                        corePoolSize,
                        maxPoolSize,
                        60L, TimeUnit.SECONDS,
                        new LinkedBlockingQueue<Runnable>(800),
                        // 自定义线程池名称,便于排查问题
                        new CustomizableThreadFactory("summaryReportTaskExecutor"),
                        // 超过最大线程数,拒绝任务并抛出异常
                        new ThreadPoolExecutor.AbortPolicy());
                return threadPoolExecutor;
            }

            private int cpuCores() {
                return Runtime.getRuntime().availableProcessors();
            }
        }
    c.主要流程
        循环每天数据,提交生成报告任务到线程池,并发执行生成每日报告
        任务发生异常处理,主要是打印异常信息,标记结果
        CompletableFuture.allOf方法等待所有任务执行完毕
        获取任务执行结果,进行每日报告汇总为list
        最后汇总的list批量保存DB
    d.耗时:331ms
        比串行处理节省一半的时间
    e.最后需要注意的点
        CompletableFuture需要配置自定义线程池使用,可以做到不同业务线的线程池隔离,避免相互影响
        任务的异常处理打印必要的异常日志便于排查问题
        每个任务出现异常时,记得中断后续逻辑,避免汇总的数据出现不完整

4.15 [4]多租户:4种

00.汇总
    字段隔离方案
    Schema隔离
    独立数据库
    混合架构

01.字段隔离方案
    a.说明
        低成本背后的高风险
        字段隔离方案,是通过统一数据表+租户ID过滤实现逻辑隔离
        初期开发成本极低,但将数据安全的压力完全转移到了代码质量控制上
    b.致命缺陷检查清单
        任意一次DAO层查询漏加tenant_id条件 → 数据跨租户泄露
        索引必须将tenant_id作为最左前缀 → 性能瓶颈风险
        全表扫描类查询(如报表统计)无法避免跨租户干扰
    c.代码防御示范
        a.MyBatis拦截器自动注入租户ID
            @Intercepts({@Signature(type = Executor.class, method = "update")})
            public class TenantInterceptor implements Interceptor {
                public Object intercept(Invocation iv) throws SQLException {
                    MappedStatement ms = (MappedStatement) iv.getArgs()[0];
                    Object param = iv.getArgs()[1];

                    // 实体类自动填充tenant_id
                    if (param instanceof BaseTenantEntity) {
                        Field tenantIdField = param.getClass().getDeclaredField("tenantId");
                        tenantIdField.setAccessible(true);
                        if (tenantIdField.get(param) == null) {
                            tenantIdField.set(param, TenantContext.get());
                        }
                    }
                    return iv.proceed();
                }
            }
        b.SQL防火墙:强制全表扫描必须声明租户范围
            /* 危险操作(可能扫全表) */
            SELECT * FROM orders WHERE status = 'PAID';

            /* 安全写法(强制tenant_id过滤) */
            SELECT * FROM orders
            WHERE tenant_id = 'tenant_01'
              AND status = 'PAID'
              /* 必须添加LIMIT防止全量拉取 */
              LIMIT 1000;
    d.适用场景建议
        初期快速验证的MVP产品,用户量比较少的业务系统
        对数据隔离要求较低的内部管理系统

02.Schema隔离
    a.说明
        数据库层的单元房
        在同一个数据库实例中为每个租户独立Schema,实现库级别隔离
        各租户表结构相同但数据独立,像小区里的不同住户单元
    b.运维警告清单
        百级Schema数量级后,备份与迁移成本陡增
        跨Schema关联查询必须引入中间聚合层
        数据库连接池需按最大租户数配置 → 连接风暴风险
    c.动态路由代码实现
        a.Spring动态数据源配置
            spring:
              datasource:
                dynamic:
                  primary: master
                  strict: true
                  datasource:
                    master:
                      url: jdbc:mysql://主库地址
                    tenant_001:
                      url: jdbc:mysql://从库地址?currentSchema=tenant_001
                    tenant_002:
                      url: jdbc:mysql://从库地址?currentSchema=tenant_002
        b.AOP切面动态切换Schema
            @Aspect
            @Component
            public class SchemaAspect {

                @Before("@annotation(requireTenant)")
                public void switchSchema(JoinPoint joinPoint) {
                    HttpServletRequest request = getCurrentRequest();
                    String tenantId = request.getHeader("X-Tenant-ID");

                    // 验证租户合法性
                    if (!tenantService.isValid(tenantId)) {
                        throw new IllegalTenantException("租户身份异常!");
                    }

                    // 动态切换数据源
                    DynamicDataSourceContextHolder.push(tenantId);
                }

                @After("@annotation(requireTenant)")
                public void clearSchema() {
                    DynamicDataSourceContextHolder.clear();
                }
            }
    d.适用场景建议
        需要中等安全级别的行业(教育、零售)
        租户数<50且数据规模可控的系统

03.独立数据库
    a.说明
        数据隔离的终极形态
        每个租户享有独立数据库实例
        从存储到底层连接完全隔离
        安全性最高但成本呈线性增长
    b.财务预警清单
        每个实例约增加¥3000/月(云RDS基础配置)
        跨租户数据聚合需额外ETL系统支持
        DBA运维成本随租户数量直线上升
    c.数据源动态路由核心代码
        a.抽象路由控制器
            public class TenantDataSourceRouter extends AbstractRoutingDataSource {

                @Override
                protected Object determineCurrentLookupKey() {
                    return TenantContextHolder.get();
                }

                @Override
                protected DataSource determineTargetDataSource() {
                    String tenantId = (String) determineCurrentLookupKey();
                    DataSource ds = dataSourceMap.get(tenantId);
                    if (ds == null) {
                        ds = createNewDataSource(tenantId);  // 动态创建新租户数据源
                        dataSourceMap.put(tenantId, ds);
                    }
                    return ds;
                }
            }
        b.多租户事务同步器(关键!)
            @Bean
            public PlatformTransactionManager transactionManager() {
                return new DataSourceTransactionManager() {
                    @Override
                    protected void doBegin(Object transaction, TransactionDefinition definition) {
                        TenantDataSourceRouter router = (TenantDataSourceRouter) getDataSource();
                        router.initTenantDataSource(TenantContextHolder.get());  // 确保事务绑定正确数据源
                        super.doBegin(transaction, definition);
                    }
                };
            }
    d.适用场景建议
        金融、医疗等强合规行业
        付费能力强且需要独立资源池的KA客户

四、混合架构
    a.说明
        没有银弹的平衡术
        核心原则:按租户等级提供不同隔离方案
        在系统中创建租户时,根据租户的实际情况,给它分配一个等级
        不同的等级,使用不同的隔离方案。
    b.租户等级隔离方案资源配置
        S级独立数据库独占RDS实例+只读副本
        A级Schema隔离共享实例独立Schema
        B级字段过滤共享表
    c.动态策略选择器
        a.针对不同的租户,我们可以使用策略模式,根据不同的等级,选择不同的数据库访问方式
            public class IsolationStrategyFactory {

                public IsolationStrategy getStrategy(String tenantId) {
                    TenantConfig config = configService.getConfig(tenantId);
                    switch(config.getLevel()) {
                        case VIP:
                            return new IndependentDBStrategy();
                        case STANDARD:
                            return new SchemaStrategy();
                        case BASIC:
                        default:
                            return new SharedTableStrategy();
                    }
                }

                // 示例策略接口
                public interface IsolationStrategy {
                    DataSource getDataSource();
                    void executeQuery(String sql);
                }
            }
    d.运维避坑必读
        元数据管理:建立租户-资源映射表,避免配置漂移
        迁移工具链:开发自动化升降级工具(如VIP客户从共享表迁移到独立库)
        监控分层:不同方案的性能指标需独立采集分析

4.16 [4]sql批量:4种

00.汇总
    a.INSERT ... ON DUPLICATE KEY UPDATE
        当表有主键或唯一索引,并且只需要更新部分字段时,这是最好的选择
    b.REPLACE INTO
        当你需要替换整行数据,并且能接受删除旧数据触发的影响时
    c.INSERT IGNORE
        当你需要灵活控制插入时,且不需要更新数据,数据存在则忽略
    d.INSERT IF NOT EXISTS
        当你需要灵活控制插入时,且不需要依赖任何唯一约束的索引

00.前言
    a.创建一张users表,并把name设置为唯一索引。
        CREATE TABLE `users` (
          `id` int NOT NULL AUTO_INCREMENT,
          `name` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL,
          `age` int DEFAULT NULL,
          PRIMARY KEY (`id`),
          UNIQUE KEY `name_key` (`name`) USING BTREE
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
    b.插入一条数据,做测试数据
        INSERT INTO `users` (`name`, `age`) VALUES ('刘备', 50);

01.INSERT ... ON DUPLICATE KEY UPDATE
    a.使用条件
        表中必须存在主键或者唯一索引,用于判断数据是否重复
    b.执行逻辑
        先执行插入,如果不存在,则插入成功;如果唯一索引已存在,则删除刚刚插入的数据,再去更新之前存在的那条记录
    c.示例
        -- 再次插入这个唯一索引存在的数据,如果存在则修改
        INSERT INTO `users` (`name`, `age`)
        VALUES
            ( '刘备', 60 ) ON DUPLICATE KEY UPDATE `name` = '张飞', `age` = 30;
        -------------------------------------------------------------------------------------------------
        你会发现影响了两条数据(删除了SQL执行时新增的数据,并把原来的数据修改了)
        再次查看数据表,数据被修改了
        再去查看自增id
    d.优点
        灵活,可以只更新部分字段,而不是全行替换
        插入和更新在一条SQL语句中完成,效率较高
    e.缺点
        仅适用于存在主键或唯一索引的表
        如果多个字段导致唯一索引冲突,需要提前设计索引结构
        执行更新时,会有额外的开销。并且使用自增id时,会丢失一个id

02.REPLACE INTO
    a.使用条件
        表中必须存在主键或唯一索引,用于判断数据是否重复
    b.执行逻辑
        以唯一键判断数据是否存在。如果不存在,那么新增;如果存在,先删除原来的数据,再新增
    c.示例
        REPLACE INTO `users` (`name`, `age`)
        VALUES ('张飞', '55');
        -----------------------------------------------------------------------------------------------------
        你会发现影响了两条数据(删除了原来的数据,并插入了新数据)。
        再次查看数据表,原来的数据没有了,新的数据id值不一样。
    d.优点
        逻辑简单,直接替换整行数据
        适合替换所有列的场景
    e.缺点
        删除操作会触发外键约束、触发器等,可能导致额外开销
        删除和重新插入会导致主键的id值变化
        对于大表来说,性能不如ON DUPLICATE KEY UPDATE高效

03.INSERT IGNORE
    a.使用条件
        不需要依赖主键或唯一索引来触发冲突行为(但是需要唯一索引来作为是否重复的判断条件)
    b.执行逻辑
        会先执行插入,如果数据不存在,则插入成功;如果存在,则不插入。由于使用了ignore,所以会忽略索引存在错误
    c.示例
        INSERT IGNORE INTO `users` (`name`, `age`)
        VALUES ('张飞', 50);
        -----------------------------------------------------------------------------------------------------
        你会发现没有修改任何数据(因为当前唯一索引的数据已存在)
        再次查看数据表,没有任何变化
    d.优点
        控制更灵活,可以通过条件进行精确的更新操作
        不会触发删除操作,不会影响外键
    e.缺点
        不能更新数据,要想更新数据,需要额外的处理

04.INSERT IF NOT EXISTS
    a.使用条件
        不需要依赖主键或唯一索引来触发冲突行为
    b.执行逻辑
        执行insert前,会先判断条件是否满足。通过not exists判断,如果不存在,则插入;如果存在,则不插入
        注意:新增记录时,select字段from,表名称是dual,并不是当前表
        如果是当前表,那么在新增记录时(表中没有该记录)会报错
        因为第2行的select从当前表查询,已经有了该值。所以必须是dual,或者是其他表也可以
    c.示例
        INSERT INTO `users` ( `name`, `age` )
        SELECT
            '张飞',
            50
        FROM
            DUAL
        WHERE
            NOT EXISTS ( SELECT * FROM `users` WHERE `name` = '张飞' );
        你会发现没有修改任何数据
        再次查看数据表,没有任何变化
    d.优点
        避免重复数据:通过NOT EXISTS过滤掉已存在的数据,保证数据的唯一性。不需要依赖唯一索引和主键
        简洁:将检查与插入操作合并在一条SQL语句中,避免写多条查询
    e.缺点
        性能开销:对于大表,NOT EXISTS的子查询可能性能较差,尤其是在没有索引的情况下
        不适合并发高的场景:在高并发插入时,可能仍然出现重复数据(需要借助唯一索引等约束)
        不能修改数据:修改数据时,依然需要额外处理

05.所有测试SQL语句
    a.片段
        CREATE TABLE `users` (
          `id` int NOT NULL AUTO_INCREMENT,
          `name` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL,
          `age` int DEFAULT NULL,
          PRIMARY KEY (`id`),
          UNIQUE KEY `name_key` (`name`) USING BTREE
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
    b.片段
        INSERT INTO `users` (`name`, `age`) VALUES ('刘备', 50);
    c.片段
        INSERT INTO `users` (`name`, `age`)
        VALUES ( '刘备', 60 ) ON DUPLICATE KEY UPDATE `name` = '张飞', `age` = 30;
    d.片段
        SHOW TABLE STATUS LIKE 'users';
    e.片段
        REPLACE INTO  `users` (`name`, `age`) VALUES ('张飞', 55);
    f.片段
        INSERT IGNORE INTO `users` (`name`, `age`) VALUES ('张飞', 50);
    g.片段
        INSERT INTO `users` ( `name`, `age` )
        SELECT '张飞', 50 FROM DUAL
        WHERE NOT EXISTS ( SELECT * FROM `users` WHERE `name` = '张飞' );

4.17 [4]with语法:公用表表达式,CTE

01.定义与原理
    a.定义
        WITH 语法,也称为公用表表达式(CTE),在 SQL 查询中提供了一种定义临时结果集的方式,可以在主查询中多次引用
    b.原理
        CTE 是一个临时的结果集,数据库优化器可以对其进行优化,视为内联视图或物化视图
    c.优化策略
        a.内联视图优化
            在许多数据库中,非递归 CTE 会被视为内联视图。这意味着 CTE 的定义会被直接插入到主查询中,就像子查询一样
        b.物化视图优化
            在某些情况下,数据库引擎可能会选择将 CTE 物化为临时表,尤其是在 CTE 被多次引用时
        c.递归 CTE 优化
            对于递归 CTE,数据库引擎通常会使用迭代方法来生成结果集
        d.查询计划共享
            如果 CTE 被多次引用,数据库引擎可能会共享查询计划
        e.索引使用
            数据库引擎会尝试在 CTE 中使用索引,以提高数据访问速度
    d.数据库引擎的具体实现
        a.Oracle
            Oracle 会根据查询的复杂性和 CTE 的使用情况,选择内联或物化策略
        b.PostgreSQL
            PostgreSQL 通常将 CTE 视为优化屏障,意味着 CTE 会被完全计算并存储
        c.SQL Server
            SQL Server 会尝试将 CTE 内联到查询中,除非 CTE 被多次引用或递归
        d.MySQL
            MySQL 从 8.0 版本开始支持 CTE,通常会将 CTE 内联到查询中

02.优势对比
    a.可读性和维护性
        WITH 语法:通过将复杂查询分解为多个命名的 CTE,使查询结构更加清晰,易于阅读和维护
        子查询:嵌套子查询可能导致查询结构复杂,难以理解和维护
    b.重用性
        WITH 语法:可以在同一个查询中多次引用同一个 CTE,避免重复编写相同的子查询逻辑
        子查询:每次使用都需要重复编写相同的逻辑
    c.性能
        WITH 语法:在某些数据库中,CTE 可以被优化器视为内联视图,可能会有性能优势,特别是在需要多次引用相同结果集的情况下
        子查询:可能会导致重复计算,尤其是在多个地方使用相同子查询时
    d.递归查询
        WITH 语法:支持递归 CTE,可以用于处理层次结构数据
        子查询:不支持递归,需要其他方法来处理层次结构数据

03.常用API
    a.定义 CTE
        WITH cte_name AS (
            SELECT ...
        )
    b.在主查询中使用 CTE
        SELECT * FROM cte_name;
    c.多个 CTE
        WITH cte1 AS (
            SELECT ...
        ), cte2 AS (
            SELECT ...
        )
        SELECT * FROM cte1 JOIN cte2 ON ...
    d.递归 CTE
        WITH RECURSIVE cte_name AS (
            SELECT ...
            UNION ALL
            SELECT ...
            FROM cte_name
            WHERE ...
        )
        SELECT * FROM cte_name;

04.场景及代码示例
    a.场景1:简化复杂查询
        WITH sales_summary AS (
            SELECT product_id, SUM(sales) AS total_sales
            FROM sales
            GROUP BY product_id
        )
        SELECT product_id, total_sales
        FROM sales_summary
        WHERE total_sales > 1000;
    b.场景2:递归查询
        WITH RECURSIVE employee_hierarchy AS (
            SELECT employee_id, manager_id, 1 AS level
            FROM employees
            WHERE manager_id IS NULL
            UNION ALL
            SELECT e.employee_id, e.manager_id, eh.level + 1
            FROM employees e
            JOIN employee_hierarchy eh ON e.manager_id = eh.employee_id
        )
        SELECT * FROM employee_hierarchy;

4.18 [4]merge语法:根据数据库引擎解析和执行

01.定义
    MERGE INTO 是一种 SQL 语句,用于在数据库中执行合并操作
    它通常用于在目标表中插入、更新或删除数据,具体取决于源数据的存在情况
    MERGE INTO 语句的底层实现并不是简单的字符串拼接,而是由数据库引擎根据 SQL 标准和优化策略来解析和执行的

02.MERGE INTO 的工作原理
    a.匹配条件
        MERGE INTO 语句首先根据指定的条件(通常是主键或唯一键)在目标表和源数据之间进行匹配
    b.操作类型
        a.INSERT
            如果源数据在目标表中不存在,则插入新记录
        b.UPDATE
            如果源数据在目标表中存在,则更新现有记录
        c.DELETE
            在某些数据库中,MERGE 语句也可以配置为删除不匹配的记录
    c.执行计划
        数据库引擎会根据 MERGE INTO 语句生成执行计划,决定如何高效地执行插入、更新或删除操作

03.示例
    MERGE INTO target_table AS t
    USING source_table AS s
    ON t.id = s.id
    WHEN MATCHED THEN
        UPDATE SET t.name = s.name, t.value = s.value
    WHEN NOT MATCHED THEN
        INSERT (id, name, value) VALUES (s.id, s.name, s.value);

04.底层实现
    a.解析和优化
        数据库引擎会解析 MERGE INTO 语句,并根据表的索引和数据分布生成优化的执行计划
    b.事务处理
        MERGE INTO 操作通常在一个事务中执行,以确保数据的一致性
    c.并发控制
        数据库引擎会处理并发访问,确保在多用户环境下的正确性

05.注意事项
    a.性能
        MERGE INTO 语句的性能取决于数据库的实现和优化策略。对于大数据量的操作,可能需要调整索引和执行计划
    b.支持情况
        并非所有数据库都支持 MERGE INTO 语句,具体的语法和功能可能因数据库而异
        例如,Oracle 和 SQL Server 支持,但 MySQL 需要使用替代方案如 INSERT ... ON DUPLICATE KEY UPDATE

4.19 [4]insertOrUpdate语法:INSERT … ON DUPLICATE KEY UPDATE

01.语法
    INSERT INTO table_name (column1, column2, ...)
    VALUES (value1, value2, ...)
    ON DUPLICATE KEY UPDATE
        column1 = value1, column2 = value2, ...;

02.工作原理
    a.插入操作
        尝试将数据插入到指定的表中
    b.冲突检测
        如果插入的数据导致主键或唯一键冲突(即表中已经存在具有相同键值的记录)
        则执行 ON DUPLICATE KEY UPDATE 后面的更新操作
    c.更新操作
        更新冲突记录的指定列为新的值

03.使用场景
    a.批量插入和更新
        在需要批量插入数据的同时,确保如果数据已经存在,则更新现有记录
    b.数据同步
        在数据同步或导入过程中,避免重复数据导致的错误

04.示例
    a.说明
        假设有一个用户表 users,包含 id(主键)、name 和 email 列
        我们希望插入用户数据,如果 id 已存在,则更新用户的 name 和 email
    b.代码
        INSERT INTO users (id, name, email)
        VALUES (1, 'Alice', '[email protected]'),
               (2, 'Bob', '[email protected]')
        ON DUPLICATE KEY UPDATE
            name = VALUES(name),
            email = VALUES(email);
    c.在这个示例中
        如果 id 为 1 或 2 的记录不存在,则插入新记录
        如果 id 为 1 或 2 的记录已经存在,则更新 name 和 email 列为新的值

05.注意事项
    a.性能
        虽然这种语法简化了插入和更新操作,但在处理大量数据时,性能可能会受到影响。需要根据具体情况进行性能测试
    b.触发器
        使用 ON DUPLICATE KEY UPDATE 时,可能会触发表上的 INSERT 和 UPDATE 触发器
    c.自增列
        如果表中有自增列,且插入操作导致更新,自增列的值不会改变

4.20 [5]MP拓展自定义BaseMapper

00.汇总
    通过继承 MyBatis-Plus 提供的 AbstractMethod,可以实现自定义 SQL 模板,如 insertIgnore
    这种方式可以扩展 MyBatis-Plus 的功能,满足特定的业务需求
    你还可以实现其他自定义方法,如 InsertBatch、InsertIgnoreBatch 等

01.编写SQL模板
    a.步骤
        首先,我们需要创建一个类 InsertIgnore,继承自 AbstractMethod,并定义生成 SQL 的模板
    b.代码示例
        import com.baomidou.mybatisplus.annotation.IdType
        import com.baomidou.mybatisplus.core.injector.AbstractMethod
        import com.baomidou.mybatisplus.core.metadata.TableInfo
        import com.baomidou.mybatisplus.core.metadata.TableInfoHelper
        import com.baomidou.mybatisplus.core.toolkit.StringPool
        import com.baomidou.mybatisplus.core.toolkit.StringUtils
        import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils
        import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator
        import org.apache.ibatis.executor.keygen.KeyGenerator
        import org.apache.ibatis.executor.keygen.NoKeyGenerator
        import org.apache.ibatis.mapping.MappedStatement

        class InsertIgnore : AbstractMethod() {

            companion object {
                const val METHOD_NAME = "insertIgnore"
                const val SQL_TEMPLATE = """
                    <script>
                        INSERT IGNORE INTO %s %s VALUES %s
                    </script>
                """
            }

            override fun injectMappedStatement(
                mapperClass: Class<*>?,
                modelClass: Class<*>?,
                tableInfo: TableInfo?
            ): MappedStatement {
                var keyGenerator: KeyGenerator = NoKeyGenerator()
                val columnScript = SqlScriptUtils.convertTrim(
                    tableInfo!!.allInsertSqlColumnMaybeIf,
                    StringPool.LEFT_BRACKET, StringPool.RIGHT_BRACKET, null, StringPool.COMMA
                )
                val valuesScript = SqlScriptUtils.convertTrim(
                    tableInfo.getAllInsertSqlPropertyMaybeIf(null),
                    StringPool.LEFT_BRACKET, StringPool.RIGHT_BRACKET, null, StringPool.COMMA
                )
                var keyProperty: String? = null
                var keyColumn: String? = null

                if (StringUtils.isNotBlank(tableInfo.keyProperty)) {
                    if (tableInfo.idType == IdType.AUTO) {
                        keyGenerator = Jdbc3KeyGenerator()
                        keyProperty = tableInfo.keyProperty
                        keyColumn = tableInfo.keyColumn
                    } else {
                        if (null != tableInfo.keySequence) {
                            keyGenerator = TableInfoHelper.genKeyGenerator(METHOD_NAME, tableInfo, builderAssistant)
                            keyProperty = tableInfo.keyProperty
                            keyColumn = tableInfo.keyColumn
                        }
                    }
                }
                val sql = String.format(SQL_TEMPLATE, tableInfo.tableName, columnScript, valuesScript)
                val sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass)
                return addInsertMappedStatement(
                    mapperClass,
                    modelClass,
                    METHOD_NAME,
                    sqlSource,
                    keyGenerator,
                    keyProperty,
                    keyColumn
                )
            }
        }

02.自定义Mapper接口
    a.步骤
        在自定义的 Mapper 接口中添加 insertIgnore 方法
    b.代码示例
        import com.baomidou.mybatisplus.core.mapper.BaseMapper

        interface UltraBaseMapper<T> : BaseMapper<T> {
            fun insertIgnore(entity: T): Int
        }

03.业务Mapper继承自定义Mapper
    a.步骤
        将业务 Mapper 从继承 BaseMapper 改为继承 UltraBaseMapper
    b.代码示例
        interface UserMapper : UltraBaseMapper<User>

04.配置插件
    a.步骤
        创建一个类 UltraSqlInjector,继承 DefaultSqlInjector,并将 InsertIgnore 方法加入到 MyBatis-Plus 的插件中
    b.代码示例
        import com.baomidou.mybatisplus.core.injector.AbstractMethod
        import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector

        class UltraSqlInjector : DefaultSqlInjector() {
            override fun getMethodList(mapperClass: Class<*>?): MutableList<AbstractMethod> {
                val methodList = super.getMethodList(mapperClass)
                methodList.add(InsertIgnore())
                return methodList
            }
        }

05.Spring配置
    a.步骤
        在 Spring 配置类中注册 UltraSqlInjector
    b.代码示例
        import org.springframework.context.annotation.Bean
        import org.springframework.context.annotation.Configuration

        @Configuration
        class MybatisPlusConfig {
            @Bean
            fun ultraSqlInjector(): UltraSqlInjector {
                return UltraSqlInjector()
            }
        }

4.21 [5]MP接口为什么不需要实现类

00.为什么不需要实现类?
    动态代理 + 反射 + 配置
    MyBatis利用了JDK动态代理和反射机制,在运行时为接口生成代理对象,并通过注解或XML配置提供方法的具体执行逻辑

01.MP源码剖析
    a.朴素的第一步:接口不就是要实现吗?
        a.说明
            在 Java 中,接口定义方法签名,没有具体实现,需要通过实现类提供方法逻辑
        b.代码
            public interface UserService {
                void saveUser(String name);
            }
            public class UserServiceImpl implements UserService {
                @Override
                public void saveUser(String name) {
                    System.out.println("Saving user: " + name);
                }
            }
        c.说明
            调用时,通过 UserServiceImpl 的实例执行 saveUser 方法
    b.初步猜想:MyBatis 帮我们生成了实现类?
        a.说明
            MyBatis 可能在背后生成了实现类,使用了 Java 的动态代理机制。
            动态代理通过 java.lang.reflect.Proxy 类实现,可以为接口生成代理对象,拦截方法调用并执行自定义逻辑
        b.代码
            UserService proxy = (UserService) Proxy.newProxyInstance(
                UserService.class.getClassLoader(),
                new Class<?>[]{UserService.class},
                (proxyObj, method, args) -> {
                    System.out.println("Proxy saving user: " + args[0]);
                    return null;
                }
            );
            proxy.saveUser("Alice");
    c.走进 MyBatis:动态代理的初步验证
        a.说明
            MyBatis 使用 JDK 动态代理
            当调用 SqlSession.getMapper(UserMapper.class) 时,MyBatis 返回一个对象,可以直接调用 getUserById 方法并执行 SQL 查询
            MyBatis 使用 MapperProxy 负责实现动态代理
        b.具体步骤
            检查 UserMapper 是否是接口
            通过 Proxy.newProxyInstance 创建代理对象
            将代理对象的调用转发给内部逻辑处理
    d.复杂的核心:Mapper 的注解与映射机制
        a.注解解析
            MyBatis 扫描接口,通过反射读取方法上的注解,提取 SQL 和参数信息
            例如:
            @Select("SELECT * FROM users WHERE id = #{id}")
            User getUserById(int id);
        b.XML 配置(可选)
            通过 XML 文件定义 SQL
            例如:
            <mapper namespace="com.example.UserMapper">
                <select id="getUserById" resultType="com.example.User">
                    SELECT * FROM users WHERE id = #{id}
                </select>
            </mapper>
        c.动态代理的执行
            当调用 userMapper.getUserById(1) 时:
            代理对象拦截调用
            根据方法名和接口名,找到对应的 SQL
            通过反射获取方法参数,绑定到 SQL
            执行查询,返回结果

4.22 [5]MP如何注入默认的CRUD方法

01.使用Map<String, Object>作为实体类没有意义
    a.说明
        public interface MonitorService extends IService<Map<String, Object>>
        public class MonitorServiceImpl extends ServiceImpl<TestMapper, Map<String, Object>> implements MonitorService {
        public interface MonitorMapper extends BaseMapper<Map<String, Object>> {
    b.问题分析
        a.实体类的缺失
            MyBatis-Plus 的 BaseMapper 和 ServiceImpl 是基于实体类的,它们提供的 CRUD 操作都是针对实体类的字段进行的
        b.使用 Map<String, Object> 的局限性
            使用 Map<String, Object> 作为泛型参数,无法利用 MyBatis-Plus 提供的实体类相关功能,如自动映射、字段验证等
    c.解决方案
        a.定义实体类
            为数据库表定义一个实体类,这样可以充分利用 MyBatis-Plus 的功能
        b.修改 Mapper 和 Service 的泛型参数
            将 MonitorMapper 和 MonitorService 的泛型参数改为实体类

02.MP如何注入默认的CRUD方法
    a.说明
        仅由上面分析,MybatisMapperRegistry 中会初始化 MybatisMapperAnnotationBuilder 类,谜底就在这个类中
        而这个类 只重写了 {@link MapperAnnotationBuilder#parse} 和 #getReturnType 方法
    b.我们来看看 parse 方法里关键的一段
        @Override
        public void parse() {
            //...
            if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
                // 处理默认CRUD注入
                parserInjector();
            }
            //...
        }
    c.说明
        这里判断 这个Mapper 是否继承自某个特定的Mapper 如果是的话,则处理默认CRUD注入
        先看看 GlobalConfigUtils.isSupperMapperChildren(configuration, type) 这里的SupperMapper 到底是什么?
        这里看到 只要 Mapper 继承了 Mapper 这个类,则会进行默认的CRUD注入
    d.默认我们都会继承 BaseMapper ,而 BaseMapper 是 Mapper 的子类
        继承 BaseMapper 后,则默认可以调用CRUD功能,而继承了 Mapper 却什么都没有
        那只继承了 Mapper 就可以注入默认的CRUD 是为什么呢?
    e.说明
        我们来看看 parserInjector(); 处理注入这个方法,有什么奥妙吧!
        parserInjector() 方法最后调用了 AbstractSqlInjector.inspectInject(...)
    f.代码
        @Override
        public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
            Class<?> modelClass = ReflectionKit.getSuperClassGenericType(mapperClass, Mapper.class, 0);
            if (modelClass != null) {
                String className = mapperClass.toString();
                Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
                if (!mapperRegistryCache.contains(className)) {
                    TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
                    List<AbstractMethod> methodList = this.getMethodList(mapperClass, tableInfo);
                    if (CollectionUtils.isNotEmpty(methodList)) {
                            // 循环注入自定义方法
                        methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
                    } else {
                        logger.debug(mapperClass.toString() + ", No effective injection method was found.");
                    }
                    mapperRegistryCache.add(className);
                }
            }
        }
    g.代码
        这里获取了modelClass 也就是实体 class,然后判断是否已经注入
        若没有注入 则进行  initTableInfo  - 根据实体和配置信息初始化表信息对象
        关键方法:this.getMethodList(mapperClass, tableInfo); 获取需要注入的方法列表
    h.这里看看 getMethodList(...) 方法
        public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
            Stream.Builder<AbstractMethod> builder = Stream.<AbstractMethod>builder()
                .add(new Insert())
                .add(new Delete())
                .add(new Update())
                .add(new SelectCount())
                .add(new SelectMaps())
                .add(new SelectObjs())
                .add(new SelectList());
            if (tableInfo.havePK()) {
                builder.add(new DeleteById())
                    .add(new DeleteBatchByIds())
                    .add(new UpdateById())
                    .add(new SelectById())
                    .add(new SelectBatchByIds());
            } else {
                logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.",
                    tableInfo.getEntityType()));
            }
            return builder.build().collect(toList());
        }
        -----------------------------------------------------------------------------------------------------
        这里可以看到就是默认的 CRUD 在这个方法里添加进来了
        我们以 `SelectList` 为例,看看这里面做了什么
        -----------------------------------------------------------------------------------------------------
        public class SelectList extends AbstractMethod {

            public SelectList() {
                this(SqlMethod.SELECT_LIST.getMethod());
            }

            /**
             * @param name 方法名
             * @since 3.5.0
             */
            public SelectList(String name) {
                super(name);
            }

            @Override
            public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
                SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
                String sql = String.format(sqlMethod.getSql(), sqlFirst(), sqlSelectColumns(tableInfo, true), tableInfo.getTableName(),
                    sqlWhereEntityWrapper(true, tableInfo), sqlOrderBy(tableInfo), sqlComment());
                SqlSource sqlSource = super.createSqlSource(configuration, sql, modelClass);
                return this.addSelectMappedStatementForTable(mapperClass, methodName, sqlSource, tableInfo);
            }
        }
    i.说明
        SqlMethod:这个类是 MybatisPlus 的一个 Sql 定义方法 枚举,里面的存有 方法名、注释、方法SQL 三个参数
        这里的 injectMappedStatement 方法组装SQL后调用了 addSelectMappedStatementForTable 方法
        最终调用 MapperBuilderAssistant.addMappedStatement
        也就是通过 Mybaits 官方的构建助手 添加了 MappedStatement

4.23 [5]MP如何实现MyBatis的无侵入式扩展

01.MP如何实现MyBatis的无侵入式扩展
    a.说明
        通过一个简单的启动方法来进行源码调试,以揭示 MP 的工作原理
    b.启动示例
        public class MySimpleTest {
            private static SqlSessionFactory sqlSessionFactory;

            @BeforeAll
            public static void init() throws IOException, SQLException {
                InputStream reader = Resources.getResourceAsStream("mybatis-config.xml");
                sqlSessionFactory = new MybatisSqlSessionFactoryBuilder().build(reader);

                /**
                 *  运行初始化脚本
                 */
                Configuration configuration = sqlSessionFactory.getConfiguration();
                DataSource dataSource = configuration.getEnvironment().getDataSource();
                Connection connection = dataSource.getConnection();
                ScriptRunner scriptRunner = new ScriptRunner(connection);
                scriptRunner.runScript(Resources.getResourceAsReader("h2/user.ddl.sql"));
            }

            @Test
            public void selectList(){
                try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
                   H2UserMapper mapper = sqlSession.getMapper(H2UserMapper.class);
                   List<H2User> user = mapper.selectList(null);
                    System.out.println(user);
                }
            }
        }
    c.说明
        从上面的启动方法中可以看出,MP 的启动流程和 mybatis 是一样的
        但其中的奥妙就是 MP 的启动用的是 MybatisSqlSessionFactoryBuilder
        而不是 mybatis 中的 SqlSessionFactoryBuilder
        这一改变使得 MP 能够在整个初始化流程中使用自己重写或继承自 mybatis 的各种类
    d.首先是使用 MybatisXMLConfigBuilder(copy 的 XMLConfigBuilder) 进行配置文件的解析
        MybatisConfiguration
            MybatisMapperRegistry *(通过 MybatisMapperRegistry 完成 CRUD 方法注入 )
                MybatisMapperProxyFactory
                    MybatisMapperProxy
                        MybatisMapperMethod
                MybatisMapperAnnotationBuilder
    e.这里的重点是在两个位置
        a.入口文件
            MybatisSqlSessionFactoryBuilder 通过重写入口文件进而可以控制整个初始化流程
            在 spring 环境下 会使用 MybatisSqlSessionFactoryBean 但流程是不变的
        b.MapperRegistry
            MybatisMapperRegistry 通过重写了使用自己的 MapperRegistry 达到了封装 Mapper 的作用
            则可以注入默认的 CRUD 方法
    f.总结
        这里的所有的 Mybaits 开头的类,都在 mybatis 源码中有对应的一样的类
        而MP 则是 copy 或者 基础了 mybaits 的类,然后在自己需要的地方做了修改