当前位置: 首页 > article >正文

Mybatis 框架学习

ORM 思想

什么是 ORM

ORM(Object-Relational Mapping,对象关系映射) 是一种编程技术,旨在通过 面向对象的方式 操作关系型数据库。其核心思想是将 数据库表 映射为 编程语言中的类,将 表中的行 映射为 对象实例,将 表的列 映射为 对象的属性,从而屏蔽底层数据库的细节,让开发者以操作对象的方式操作数据库。

为什么需要 ORM

传统数据库操作的痛点

  • 繁琐的 SQL 硬编码:需手动编写 SQL 语句,维护困难。
  • 重复的结果集处理:需将 ResultSet 逐行解析为对象。
  • 数据库耦合性高:SQL 语法与具体数据库绑定,移植性差。
  • 安全隐患:手动拼接 SQL 易引发 SQL 注入攻击。

ORM 的解决思路

  • 抽象数据库操作:通过面向对象 API 替代原生 SQL。
  • 自动化映射:自动将对象属性与表字段绑定。
  • 统一数据访问层:屏蔽数据库差异,提升代码可移植性。

ORM 的核心思想

表与类的映射

  • 表(Table)类(Class)
  • 行(Row)对象实例(Object)
  • 列(Column)对象属性(Property)

示例

  • 数据库表 user

    CREATE TABLE user (
        id INT PRIMARY KEY,
        name VARCHAR(50),
        age INT
    );
    
  • 对应的 Java 类:

    public class User {
        private Integer id;
        private String name;
        private Integer age;
        // Getter & Setter
    }
    

关系的映射

  • 一对一(如 UserIDCard

    通过外键或共享主键实现。

  • 一对多(如 DepartmentUser

    通过外键在“多”的一方建立关联。

  • 多对多(如 UserRole

    通过中间表(如 user_role)实现。

CRUD 操作的抽象

  • CreateuserDao.save(user)INSERT INTO user ...
  • ReaduserDao.findById(1)SELECT * FROM user WHERE id=1
  • UpdateuserDao.update(user)UPDATE user SET ...
  • DeleteuserDao.delete(1)DELETE FROM user WHERE id=1

ORM 的优势

优势说明
提高开发效率减少 SQL 编写和结果集解析代码。
代码可维护性高数据操作集中在对象层面,逻辑清晰。
数据库无关性更换数据库时只需修改配置,无需重写 SQL。
防止 SQL 注入通过参数化查询(如 PreparedStatement)自动处理。
支持高级特性如事务管理、缓存、延迟加载等。

ORM 的局限性

局限性说明
性能开销自动生成的 SQL 可能不够优化,复杂查询效率低。
学习成本需掌握 ORM 框架的配置和特性(如 Hibernate 的 HQL、Session 管理)。
灵活性受限复杂查询或数据库特性(如存储过程)难以通过 ORM 直接实现。
对象与表的阻抗失配继承、多态等面向对象特性在关系模型中无直接对应。

常见 ORM 框架

框架语言特点
HibernateJava全自动 ORM,支持 JPA 标准,强调对象化操作。
MyBatisJava半自动 ORM,SQL 与代码解耦,灵活性高。
SQLAlchemyPython支持 ORM 和原生 SQL,适用于复杂查询。
Entity Framework.NET微软官方 ORM,集成 LINQ,支持 Code First 开发模式。
Django ORMPython高度集成于 Django 框架,简单易用。

ORM 使用场景

场景是否适合 ORM说明
常规 CRUD 操作✔️ 适合ORM 自动生成高效 SQL,简化开发。
复杂报表查询⚠️ 谨慎使用手动编写 SQL 或结合原生查询更灵活。
高频事务系统⚠️ 需优化配置ORM 缓存和批量操作可提升性能,但需避免 N+1 查询等问题。
微服务架构✔️ 适合ORM 帮助快速实现数据访问层,适配不同数据库。

ORM 最佳实践

  1. 合理设计数据模型

    • 避免过度嵌套对象,优先使用扁平化结构。
    • 合理使用懒加载(Lazy Loading)避免不必要的数据加载。
  2. 优化查询性能

    • 使用批量操作(如 saveAll())减少数据库交互。
    • 避免 SELECT *,按需查询字段。
    • 通过 JOIN FETCH(Hibernate)或 <association>(MyBatis)解决 N+1 查询问题。
  3. 结合原生 SQL

    • 复杂查询直接使用原生 SQL 或存储过程。
    // JPA 原生 SQL 示例
    @Query(value = "SELECT * FROM user WHERE age > :age", nativeQuery = true)
    List<User> findUsersByAge(@Param("age") int age);
    
  4. 事务管理

    • 使用声明式事务(如 @Transactional)确保操作原子性。
    • 控制事务粒度,避免长事务占用连接。

总结

ORM 思想通过 对象与关系的映射,将数据库操作抽象为面向对象的 API,显著提升了开发效率和代码可维护性。尽管存在性能调优和复杂查询的挑战,但在大多数企业应用中,ORM 仍是平衡开发效率与维护成本的最佳选择。选择合适的 ORM 框架(如 Hibernate 的全面性、MyBatis 的灵活性),并结合原生 SQL 补充其不足,是构建高效数据访问层的关键。

Mybatis 简介

MyBatis 是一款半自动化的持久层 ORM 框架(Persistence Framework),它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作,通过 XML 或注解实现 Java 对象与关系型数据库的映射(ORM)。其核心思想是将 SQL 与业务代码解耦,开发者可完全控制 SQL 语句,同时实现参数映射和结果集转换的自动化。

核心定位

  • 轻量级 ORM 框架:比 Hibernate 更轻量,仅关注 SQL 映射,避免全 ORM 的复杂性。
  • SQL 与 Java 解耦:SQL 语句独立于 Java 代码,便于维护和优化。
  • 灵活性与高性能:支持动态 SQL、批量操作和缓存机制,提升开发效率。

核心特性

  1. 半自动化设计

    • 相比全自动化的 Hibernate,需要手动编写 SQL。
    • 提供结果集到对象的自动映射
    • 支持存储过程、复杂联表查询等高级功能
  2. 动态SQL能力

    通过 XML 标签实现条件分支(<if><choose>)、循环(<foreach>)等逻辑处理

    动态 SQL = 传统 JDBC 条件拼接 + 自动防注入处理

  3. 轻量级架构

    核心 JAR 包仅约2.7MB,无第三方依赖。

核心架构组成

组件作用描述
SqlSessionFactory全局单例,通过 mybatis-config.xml 构建
SqlSession线程非安全的数据库会话对象,生命周期与请求绑定
Mapper接口通过动态代理绑定到 XML 映射文件
Executor执行器,处理缓存、事务等底层操作

优点与缺点

优点

  • 灵活控制 SQL,适合复杂查询优化。
  • 学习曲线平缓,易于调试。
  • 与 Spring 等框架集成良好。

缺点

  • 需要手动编写较多 SQL。
  • 多表关联映射配置较复杂。

使用场景

  • 需要高度优化 SQL 性能的项目。
  • 遗留数据库系统(表结构不规范)。
  • 需同时使用存储过程与 ORM 的场景。

持久层框架对比

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

快速开始

  1. 准备数据库
CREATE DATABASE `mybatis-test`;

USE `mybatis-test`;

CREATE TABLE `user` (
  `id` int AUTO_INCREMENT,
  `name` varchar(10),
  `age` int,
  `sex` varchar(2),
  PRIMARY KEY (`id`)
);

INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('张三', 18, '男');
INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('李四', 30, '男');
INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('王五', 6, '女');
INSERT INTO `user` (`name`, `age`, `sex`) VALUES ('赵六', 99, '女');
  1. 创建项目,导入依赖
<dependencies>
  <!-- mybatis依赖 -->
  <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.11</version>
	</dependency>
<!-- MySQL驱动 mybatis底层依赖jdbc驱动实现,本次不需要导入连接池,mybatis自带! -->
	<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.25</version>
	</dependency>
<!--junit5测试-->
	<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.3.1</version>
    <scope>test</scope>
  </dependency>
  <!-- Lombok 依赖 -->
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
    <scope>provided</scope>
  </dependency>
</dependencies>
  1. 准备实体类
@Data
public class User {

	private Integer id;
	private String name;
	private Integer age;
	private String sex;

}
  1. 准备 Mapper 接口和 Mapper XML 文件。

    MyBatis 框架下,SQL 语句编写位置发生改变,从原来的 Java 类,改成 XML 或者注解定义。

    推荐在 XML 文件中编写 SQL 语句,让用户能更专注于 SQL 代码,不用关注其他的 JDBC 代码。

    如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。

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

    Mybatis 中的 Mapper 接口相当于以前的 Dao。但是区别在于,Mapper 仅仅只是建接口即可,我们不需要提供实现类,具体的 SQL 写到对应的 Mapper 文件,该用法的思路如下图所示:

在这里插入图片描述

mapper 接口:

public interface UserMapper {
	
	// 根据id查询用户
	User getUserById(Integer id);
	
}

编写 mapper xml 文件:

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

    <!-- 查询使用 select标签
            id = 方法名
            resultType = 返回值类型
            标签内编写SQL语句
     -->
    <select id="getUserById" resultType="com.yigongsui.pojo.User">
        <!-- #{id}代表动态传入的参数,id为方法的形参名,并且进行赋值!后面详细讲解 -->
        select id, name, age, sex from user where id = #{id};
    </select>

</mapper>

注意:

  • 方法名和 SQL 的 id 一致。
  • 方法返回值和 resultType 一致。
  • 方法的参数和 SQL 的参数一致。
  • 接口的全类名和映射配置文件的名称空间一致。
  1. 编写 Mybatis 配置文件

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

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

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

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

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

</configuration>
  1. 测试
public class MyBatisTest {

    @Test
    public void testSelectEmployee() throws IOException {

        // 1.创建SqlSessionFactory对象
        // ①声明Mybatis全局配置文件的路径
        String mybatisConfigFilePath = "mybatis-config.xml";

        // ②以输入流的形式加载Mybatis配置文件
        InputStream inputStream = Resources.getResourceAsStream(mybatisConfigFilePath);

        // ③基于读取Mybatis配置文件的输入流创建SqlSessionFactory对象
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

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

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

        // 4. 调用代理类方法既可以触发对应的SQL语句
        Employee employee = employeeMapper.selectEmployee(1);

        System.out.println("employee = " + employee);

        // 4.关闭SqlSession
        session.commit(); //提交事务 [DQL不需要,其他需要]
        session.close(); //关闭会话

    }
}

说明:

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

Mybatis 基本使用

Mybatis 配置文件常用属性

Mybatis 配置文件就是上面的 mybatis-config.xml 文件。

结果如下:

  • configuration(配置)
    • properties(属性)
    • settings(设置)
    • typeAliases(类型别名)
    • typeHandlers(类型处理器)
    • objectFactory(对象工厂)
    • plugins(插件)
    • environments(环境配置)
      • environment(环境变量)
        • transactionManager(事务管理器)
        • dataSource(数据源)
    • databaseIdProvider(数据库厂商标识)
    • mappers(映射器)

配置文件的元素配置时需要严格按照上面的顺序来配置,否则报错。

我们上面的配置文件配置了 environmentsmappers 元素,environments 元素必须要在 mappers 上面

environments 元素

environments 属性是用于配置数据库环境的核心部分,可以配置多个数据库环境,例如实际开发中会有生产环境和测试环境。

<environments default="生产环境">
    <environment id="生产环境">
    </environment>
    <environment id="测试环境">
    </environment>
</environments>

不同的环境分别使用 environment 标签指定,最后在 environments 标签的 default 指定要使用哪个环境。

environment 子元素
<environment id="development">
    <!-- Mybatis的内置的事务管理器 -->
    <transactionManager type="JDBC"/>
    <!-- 配置数据源 -->
    <dataSource type="POOLED">
        <!-- 建立数据库连接的具体信息 -->
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mybatis-test"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </dataSource>
</environment>

id 属性:唯一标识一个环境(如 developmenttestproduction)。

transactionManager 标签:定义事务管理器,可选类型:

  • JDBC:使用 JDBC 的事务管理(默认),依赖 Connection 的提交和回滚。
  • MANAGED:由容器(如 Spring)管理事务,MyBatis 不干预事务生命周期。

dataSource 标签:配置数据库连接池,管理数据库连接的获取和释放,可选类型:

  • UNPOOLED:每次请求时新建连接,适合简单场景(无连接池)。
  • POOLED:使用连接池(默认),适合高并发场景。
  • JNDI:从容器(如 Tomcat)的 JNDI 资源中获取数据源。(不常用)

mappers 元素

用于 注册 SQL 映射文件或 Mapper 接口。它告诉 MyBatis 框架到哪里加载 SQL 映射规则,从而将 Java 接口方法与对应的 SQL 语句绑定。

<mappers>
    <mapper resource="mappers/UserMapper.xml"/>
</mappers>
注册 Mapper 的四种方式
  1. 通过 XML 文件路径注册

使用 <mapper resource> 直接指定 Mapper XML 文件的类路径(推荐方式)。

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

要求:XML 文件需放在类路径(resources 目录)对应的位置。

  1. 通过 mapper 接口全限定名注册

使用 <mapper class> 直接指定 Mapper 接口的完全限定名(需配合注解或 XML 文件)。

<mapper class="com.yigongsui.mapper.UserMapper"/>

要求

  • 如果使用 XML 文件,XML 必须与接口同名且在同一包下(如 ProductMapper.javaProductMapper.xml)。
  • 如果使用注解(如 @Select),则无需 XML 文件。
  1. 通过包名批量注册

使用 <package> 扫描指定包下的所有 Mapper 接口或 XML 文件。

<package name="com.yigongsui.mapper"/>

要求

  • 接口与 XML 文件需同名且在同一包下(如 UserMapper.javaUserMapper.xml)。

在这里插入图片描述

包名结构必须完全相同。

  • 适用于大量 Mapper 的场景,避免逐个注册。
  1. 通过绝对路径文件注册

使用 <mapper url> 指定 XML 文件的绝对磁盘路径(不常用,适用于特殊场景)。

<mapper url="file:///D:/project/mappers/UserMapper.xml"/>
常见问题及注意事项
  1. 路径问题
    • XML 文件路径需与 <mapper resource><package> 配置严格匹配。
    • 若使用 Maven 项目,需确保 XML 文件位于 resources 目录的对应包路径下。
  2. 同名冲突
    • 同一 SQL 语句在 XML 和注解中重复定义时,XML 优先级更高。
  3. 包扫描限制
    • <package> 仅扫描接口,不自动加载 XML 文件,需保证 XML 与接口位置一致。

其余元素用到时再介绍。

Mybatis 日志输出

如果想要输出 sql 语句的日志,需要使用 Mybatis 配置文件的 settings 元素。

settings 元素

用于 调整框架的全局运行时行为。通过它,可以启用或禁用缓存、控制延迟加载、配置日志实现等,从而优化性能和定制化功能。

常用配置项及说明

完整列表参考官方文档

配置项默认值说明
cacheEnabledtrue是否开启二级缓存(Mapper 级别缓存)。
lazyLoadingEnabledfalse是否启用延迟加载(关联对象的延迟初始化)。
aggressiveLazyLoadingfalse是否“激进”延迟加载。若为 true,访问任意属性会立即加载所有延迟对象;若为 false,按需加载。
mapUnderscoreToCamelCasefalse是否自动将数据库字段的下划线命名(如 user_name)映射为 Java 属性的驼峰命名(如 userName)。
logImpl未指定指定 MyBatis 使用的日志框架(如 SLF4JLOG4J2)。
jdbcTypeForNullOTHER当参数为 null 时,指定 JDBC 类型(某些数据库要求明确类型,可设为 NULL)。
useGeneratedKeysfalse是否允许 JDBC 支持自动生成主键(需数据库驱动支持)。
defaultStatementTimeout未设置设置数据库操作默认超时时间(秒)。

使用 name 属性指定配置项,value 属性指定属性值。

配置示例
  1. 开启驼峰命名自动映射

在实际开放中,由于表非常多且复杂,需要名字进行区分,各个字段名都要加上业务类型。

例如 User 用户类,它的字段名通常是 userIduserNameuserAge 等。

在 java 中,实体类的属性命名规则是驼峰命名法(多个单词组合时,首字母小写,其它单词首字母大写),例如:userIduserNameupdateTime 等。

在数据库中,字段名是单词_单词的形式命名,例如:user_iduser_name

这样两者名对不上了,如果名字严格符合上面要求,我们就可以配置驼峰命名自动映射去匹配。

<settings>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
  • 效果:无需手动编写 <resultMap>,自动将 user_name 映射到 userName 属性。
  1. 配置延迟加载
<settings>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>
  • 效果:关联对象(如 Order 关联的 User)仅在访问时加载,避免不必要的查询。
  1. 指定日志框架
<settings>
    <setting name="logImpl" value="SLF4J"/>
</settings>
  • 支持的值SLF4JLOG4J2LOG4JSTDOUT_LOGGING(控制台输出)等。

我们测试配置日志使用 STDOUT_LOGGINGSLF4JLOG4J2 等需要导入依赖才能使用。

在快速开始的测试代码配置文件 environments 元素上面配置:

<settings>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

查看输出:

Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Opening JDBC Connection
Created connection 1866229258.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6f3c660a]
==>  Preparing: select id, name, age, sex from user where id = ?;
==> Parameters: 1(Integer)
<==    Columns: id, name, age, sex
<==        Row: 1, 张三, 18, 男
<==      Total: 1
User(id=1, name=张三, age=18, sex=男)
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6f3c660a]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6f3c660a]
Returned connection 1866229258 to pool.

#{} 和 ${}

它们的用途都是为 sql 语句传入参数值,下面是对比:

特性#{}(井号占位符)${}(美元占位符)
处理方式预编译参数(PreparedStatement)直接字符串替换(字符串拼接)
防 SQL 注入✅ 自动防止❌ 存在风险(需手动过滤)
参数类型转换自动匹配 JDBC 类型按原样插入(可能需手动加引号)
适用场景动态的传递(如 WHERE 条件值)动态SQL 片段(如表名、列名、ORDER BY)

典型使用场景示例

  1. 必须用 #{} 的场景(安全优先)
<!-- 动态条件值 -->
<select id="findUser">
  SELECT * FROM user 
  WHERE username = #{name} AND age > #{minAge}
</select>
  1. 必须用 ${} 的场景(动态结构)
<!-- 动态表名(需确保 tableName 值可信) -->
<select id="selectFromTable">
  SELECT * FROM ${tableName}
</select>

<!-- 动态排序字段 -->
<select id="getUsers">
  SELECT * FROM user ORDER BY ${orderColumn}
</select>

Mybatis 数据输入

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

  • 简单类型:只包含一个值的数据类型
    • 基本数据类型:intbyteshortdouble、……
    • 基本数据类型的包装类型:IntegerCharacterDouble、……
    • 字符串类型:String
  • 复杂类型:包含多个值的数据类型
    • 实体类类型:UserStudent、……
    • 集合类型:ListSetMap、……
    • 数组类型:int[]、String[]、……
    • 复合类型:List<User>、实体类中包含集合……

单个简单类型参数

我们快速开始的代码就是单个简单类型参数(Integer id

<select id="getUserById" resultType="com.yigongsui.pojo.User">
    select id, name, age, sex from user where id = #{id};
</select>

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

单个复杂类型参数

插入一条数据,参数就是实体类 User

接口:

int addUser(User user);

mapper.xml

<insert id="addUser">
    insert into user (name, age, sex) VALUE (#{name},#{age},#{sex});
</insert>

测试:

User user = new User();
user.setName("田七");
user.setAge(44);
user.setSex("男");

int result = mapper.addUser(user);
if (result == 1) {
    System.out.println("插入成功");
}
session.commit();

单个复杂类型参数,在 #{} 中必须为实体类的属性名。

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

因为默认是开启事务的,所以插入语句需要提交事务。

多个简单类型参数

场景:根据用户的姓名,年龄查找用户

接口:

User getUserByNameAndAge(String name, int age);

mapper.xml

<select id="getUserByNameAndAge" resultType="com.yigongsui.pojo.User">
    select * from user where name = #{name} and age = #{6}
</select>

#{} 不能任意命名,也不能单纯得使用参数名,而是必要要在接口方法的参数上使用 @Param 注解指定名称

User getUserByNameAndAge(@Param("name") String name, @Param("age") int age);

两个名称对应。

map 类型参数

场景:插入数据,参数为 map 类型

接口:

int addUserMap(Map<String, Object> map);

mapper.xml

<insert id="addUserMap">
    insert into user (name, age, sex) VALUE (#{name},#{age},#{sex});
</insert>

测试

HashMap<String, Object> map = new HashMap<>();
map.put("name","田七");
map.put("age",77);
map.put("sex","男");
int result = mapper.addUserMap(map);
if (result > 0) {
    System.out.println("插入成功");
}
session.commit();

#{} 为 map 的 key 名字

Mybatis 数据输出

输出概述

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

  • 增删改操作返回的受影响行数:直接使用 intlong 类型接收即可。
  • 查询操作的查询结果,可能为各种类型。

我们需要指定查询的输出数据类型。

并且插入场景下,实现主键数据回显示。

对于 select 语句,需要使用 resultType 指明返回值类型,其它 dml 语句(insert、update、delete)不需要指定 resultType,因为都是 int 类型。

resultType 属性

  1. :类的全限定名或者别名。例如 Integer 类型的全限定名是 java.lang.Integer ,Mybatis 设置了很多别名 int Integer integer Int INT INTEGER。基本数据类型的别名是名称前加 _,例如 int 类型的别名是 _int
  2. 作用:指定 SQL 返回结果的映射类型,可以是简单类型、POJO 对象或 Map。
  3. 使用场景
  • 基本类型:返回单个字段值(如 int, String)。
  • POJO 对象:返回结果自动映射到对象的属性(字段名与属性名一致)。
  • Map:以键值对形式返回结果(键为列名,值为数据)。
  1. 注意事项
  • 必须显式指定resultType 不可省略(除非使用 resultMap)。
  • 自动映射规则
    • 默认按列名与属性名匹配(可开启 mapUnderscoreToCamelCase 支持驼峰转换)。
    • 若列名与属性名不一致,需使用 <resultMap> 或别名。
  • 集合处理
    • 返回多行数据时,resultType 仍指定单条记录的类型(MyBatis 自动包装为 List<T>)。

返回单个简单类型

场景:查询数据总数

接口:

int getCount();

mapper.xml

<select id="getCount" resultType="int">
    select count(*) from user;
</select>

使用 Integer 类型的别名 int

返回单个复杂类型

根据 id 查询用户,最开始的例子。

<select id="getUserById" resultType="com.yigongsui.pojo.User">
    <!-- #{id}代表动态传入的参数,id为方法的形参名,并且进行赋值!后面详细讲解 -->
    select id, name, age, sex from user where id = #{id};
</select>

这里 resultType 为实体类的全限定名 com.yigongsui.pojo.User ,为了简化,我们可以使用配置别名,用到配置文件的 typeAliases 元素。

typeAliases 元素

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

typeAliases 元素在 settings 的下面,environments 的上面

配置别名有两种方式。

  1. typeAlias 将类的全限定名与包名做映射。
<typeAliases>
    <typeAlias type="com.yigongsui.pojo.User" alias="user"/>
</typeAliases>

当这样配置时,user 可以用在任何使用 com.yigongsui.pojo.User 的地方。

  1. 配置包名
<typeAliases>
    <package name="com.yigongsui.pojo"/>
</typeAliases>

配置包名后,包名下的每个实体类都会以类名首字母小写的名字作为别名。

例如 com.yigongsui.pojo 包下的 User 实体类,它的别名是 user

由于这样每个实体类别名都默认为这个,如果想用其它别名,可以在类上设置注解 @Alias 指定别名。

@Data
@Alias("author")
public class User {

	private Integer id;
	private String name;
	private Integer age;
	private String sex;

}

返回 map 类型

接口

Map<String, Object> getUserMapById(Integer id);

mapper.xml

<select id="getUserMapById" resultType="map">
    select * from user where id = #{id};
</select>

测试

Map<String, Object> resultMap = mapper.getUserMapById(1);
for (Map.Entry<String, Object> entry : resultMap.entrySet()) {
    System.out.println(entry.getKey() + "=" + entry.getValue());
}

返回 list 类型

接口

List<User> getAll();

mapper.xml

<select id="getAll" resultType="user">
    select * from user 
</select>

测试

List<User> userList = mapper.getAll();
userList.forEach(System.out::println);

resultType 指定的是集合的泛型类型,而不是 list

返回主键值

自增长主键

场景:插入一条数据,获取它的主键值。

mapper.xml<insert> 标签指定3个属性 useGeneratedKeyskeyColumnkeyProperty

属性作用
useGeneratedKeys是否启用数据库自动生成的主键(如自增 ID、序列等),默认为 false
keyColumn指定数据库表中主键列的列名(用于多主键或列名与属性名不一致的场景)。
keyProperty指定将生成的主键值赋值给 Java 对象的哪个属性(对象的字段名)。
<insert id="addUser" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
    insert into user (name, age, sex) VALUE (#{name},#{age},#{sex});
</insert>

测试

User user = new User();
user.setAge(18);
user.setName("孙八");
user.setName("男");
System.out.println(user.getId());
int rows = mapper.addUser(user);
if (rows > 0) {
    System.out.println("插入成功");
}
System.out.println(user.getId());
session.commit();

可以看到,插入前 idnull ,插入后 id 就变成了当前的主键值。

非自增类型主键

而对于不支持自增型主键的数据库(例如 Oracle)或者字符串类型主键,则可以使用 selectKey 子元素:selectKey 元素将会首先运行,id 会被设置,然后插入语句会被调用!

使用 selectKey 帮助插入 UUID 作为字符串类型主键示例:

<insert id="addUser" parameterType="User">
    <selectKey keyProperty="uId" resultType="java.lang.String"
        order="BEFORE">
        SELECT UUID() as id
    </selectKey>
    INSERT INTO user (id, name, age, sex) VALUE (#{id},#{name},#{age},#{sex});
</insert>

在上例中,我们定义了一个 addUser 的插入语句来将 User 对象插入到 user 表中。我们使用 selectKey 来查询 UUID 并设置到 uId 字段中,uIdString 类型。

通过 keyProperty 属性来指定查询到的 UUID 赋值给对象中的 uId 属性,而 resultType 属性指定了 UUID 的类型为 java.lang.String

需要注意的是,我们将 selectKey 放在了插入语句的前面,这是因为 MySQL 在 insert 语句中只支持一个 select 子句,而 selectKey 中查询 UUID 的语句就是一个 select 子句,因此我们需要将其放在前面。

最后,在将 User 对象插入到 user 表中时,我们直接使用对象中的 id 属性来插入主键值。

使用这种方式,我们可以方便地插入 UUID 作为字符串类型主键。当然,还有其他插入方式可以使用,如使用 Java 代码生成 UUID 并在类中显式设置值等。需要根据具体应用场景和需求选择合适的插入方式。

实体类属性和数据库字段映射

在实际开发中,经常会遇到实体类名和字段名不匹配的情况。

有3种解决方案:

  1. sql 语句使用别名:
select user_id as userID,user_name as userName from user;
  1. 开启驼峰命名规则

在 Mybatis 配置文件 settings 元素配置驼峰命名,上面展示过了。

这两种方法均使用 resultType 属性自动配置,适用于单表情况。

对于多表情况,最好使用第3种方法:

  1. 使用 resultMap 自定义映射

resultMap

resultMap 是 MyBatis 中用于自定义结果集映射的核心属性,适用于以下场景:

  • 字段名与属性名不一致:数据库列名与 Java 对象属性名不匹配时。
  • 复杂对象映射:如关联对象(一对一)、集合(一对多)、嵌套查询等。
  • 需要显式控制映射规则:例如指定主键列、类型转换器或级联映射。
resultMap 的核心子标签
子标签作用
<id>映射主键字段,优化性能(标识对象的唯一性)。
<result>映射普通字段,指定列名与属性名的对应关系。
<association>映射 一对一 关联对象(如 User 关联 Department)。
<collection>映射 一对多 集合对象(如 User 包含多个 Order)。
<constructor>通过构造方法注入结果(替代 setter)。
resultMap vs resultType
对比项resultMapresultType
灵活性高(支持复杂映射)低(仅支持简单自动映射)
配置方式需显式定义 <resultMap>直接指定 Java 类或别名
适用场景字段名不一致、关联对象、集合、嵌套查询字段名与属性名一致的简单映射
resultMap 示例
<select id="getAll" resultMap="myMap">
    select * from user
</select>

<!-- id的值与select标签的resultMap的值对应 -->
<resultMap id="myMap" type="user">
    <!-- id配置主键 -->
    <id column="user_id" property="userId"/>
    <!-- result配置其它字段 -->
    <result column="user_name" property="userName"/>
    <result column="user_age" property="userAge"/>
    <result column="user_sex" property="userSex"/>
</resultMap>
高级特性
继承其他 resultMap

通过 extends 属性复用已有的映射配置:

<resultMap id="baseUserMap" type="User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
</resultMap>

<resultMap id="userWithDeptMap" extends="baseUserMap" type="User">
    <association property="dept" resultMap="deptResultMap"/>
</resultMap>
自动映射

resultMap 中开启 autoMapping="true",自动映射未显式配置的字段:

<resultMap id="userResultMap" type="User" autoMapping="true">
    <id property="id" column="id"/>
    <!-- 其他字段自动映射 -->
</resultMap>
类型处理器

通过 typeHandler 指定自定义类型转换器:

<result property="createTime" column="create_time" 
        typeHandler="org.apache.ibatis.type.DateTypeHandler"/>

Mybatis 多表映射

多表映射指通过 MyBatis 实现关系型数据库中多个关联表的数据映射,主要处理三种关系:

  1. 一对一(1:1)
  2. 一对多(1:N)
  3. 多对多(M:N)

MyBatis 处理多表关联的两种核心方法:

  • 联表查询(JOIN):通过一条 SQL 查询多表数据,映射到嵌套对象。
  • 嵌套查询(分步查询):先查主表数据,再根据主表结果查询关联表数据(支持延迟加载)。

一对一

一只猫对应一个用户。

环境准备

数据库,用户表,猫表

CREATE TABLE `user` (
    `user_id` int AUTO_INCREMENT,
    `user_name` varchar(10),
    PRIMARY KEY (`user_id`)
);

CREATE TABLE `cat` (
    `cat_id` int AUTO_INCREMENT,
    `cat_name` varchar(10),
  	`user_id` int
    PRIMARY KEY (`cat_id`)
);

INSERT INTO `user` (`user_name`) VALUES ('张三');
INSERT INTO `user` (`user_name`) VALUES ('李四');
INSERT INTO `cat` (`cat_name`,`user_id`) VALUES ('小花',1);
INSERT INTO `cat` (`cat_name`,`user_id`) VALUES ('小红',2);

用户表实体类 User.java

@Data
public class User {

	private Integer userId;
	private String userName;

}

猫实体类

@Data
public class Cat {

	private Integer catId;
	private String catName;
	private Integer userId;

	// 一对一
	private User user;

}

环境准备完成。

场景:根据 id 查询猫及所属用户的信息。

联表查询

联表查询本质是1条 sql 语句,例如:

select *
from cat c
left join user u on c.cat_id = u.user_id
where c.cat_id = 1

下面使用代码实现:

接口:

Cat getCatById(int id);

mapper.xml

<resultMap id="catMap" type="com.yigongsui.pojo.Cat">
    <id property="catId" column="cat_id" />
    <result property="catName" column="cat_name" />
    <result property="userId" column="user_id" />
    <!-- 一对一配置 -->
    <association property="user" javaType="com.yigongsui.pojo.User">
        <id property="userId" column="user_id" />
        <result property="userName" column="user_name" />
    </association>
</resultMap>

<select id="getCatById" resultMap="catMap">
    select *
    from cat c
    left join user u on c.cat_id = u.user_id
    where c.cat_id = #{id}
</select>

使用 resultMap 配置多表映射,使用 association 给对象属性赋值,配置一对一映射,javaType 是类的全限定名。

嵌套查询(分步查询)

本质是2条 sql 语句,先根据 id 查询猫,获取用户的 id,再根据用户 id 查询用户信息。

select *
from user u
where u.user_id = (
    select c.user_id
    from cat c
    where c.cat_id = 1
);

使用代码实现:

mapper.xml

<resultMap id="catMap" type="com.yigongsui.pojo.Cat">
    <id property="catId" column="cat_id" />
    <result property="catName" column="cat_name" />
    <result property="userId" column="user_id" />
    <association property="user" column="user_id" select="getUserById" />
</resultMap>

<!-- 主查询 --
<select id="getCatById" resultMap="catMap">
    select * from cat c where c.cat_id = #{catId};
</select>

<!-- 关联查询 -->
<select id="getUserById" resultType="com.yigongsui.pojo.User">
    select * from user u where u.user_id = #{userId}
</select>

需要在 mybatis 配置文件开启驼峰命名,否则 user 字段无法映射

测试结果

==>  Preparing: select * from cat c where c.cat_id = ?;
==> Parameters: 1(Integer)
<==    Columns: cat_id, cat_name, user_id
<==        Row: 1, 小花, 1
====>  Preparing: select * from user u where u.user_id = ?
====> Parameters: 1(Integer)
<====    Columns: user_id, user_name
<====        Row: 1, 张三
<====      Total: 1
<==      Total: 1
Cat(catId=1, catName=小花, userId=1, user=User(userId=1, userName=张三))

一对多和多对一

场景:一个用户拥有多只猫

数据库添加两条数据

INSERT INTO `cat` (`cat_name`,`user_id`) VALUES ('小黄',1);
INSERT INTO `cat` (`cat_name`,`user_id`) VALUES ('小绿',1);

用户实体类新增属性

@Data
public class User {

	private Integer userId;
	private String userName;

	private List<Cat> catList;

}

联表查询

sql 语句

select *
from user u
join cat c on u.user_id = c.user_id
where u.user_id = 1

代码实现:

接口:

User getUserById(int id);

mapper.xml

<resultMap id="userMap" type="com.yigongsui.pojo.User">
    <id property="userId" column="user_id" />
    <result property="userName" column="user_name" />
    <collection property="catList" ofType="com.yigongsui.pojo.Cat">
        <id property="catId" column="cat_id" />
        <result property="catName" column="cat_name" />
    </collection>
</resultMap>

<select id="getUserById" resultMap="userMap">
    select *
    from user u
    join cat c on u.user_id = c.user_id
    where u.user_id = #{userId}
</select>

使用 collection 配置集合,ofType 为集合的泛型

结果:

==>  Preparing: select * from user u join cat c on u.user_id = c.user_id where u.user_id = ?
==> Parameters: 1(Integer)
<==    Columns: user_id, user_name, cat_id, cat_name, user_id
<==        Row: 1, 张三, 1, 小花, 1
<==        Row: 1, 张三, 3, 小黄, 1
<==        Row: 1, 张三, 4, 小绿, 1
<==      Total: 3
User(userId=1, userName=张三, catList=[Cat(catId=1, catName=小花, userId=null, user=null), Cat(catId=3, catName=小黄, userId=null, user=null), Cat(catId=4, catName=小绿, userId=null, user=null)])

嵌套查询

先根据 id 查询用户信息,再根据用户 id 获取所有猫的信息。

<resultMap id="userMap" type="com.yigongsui.pojo.User">
    <id property="userId" column="user_id" />
    <result property="userName" column="user_name" />
    <collection property="catList" column="user_id" select="getCatByUserId" />
</resultMap>

<!-- 主查询 -->
<select id="getUserById" resultMap="userMap">
    select * from user u where u.user_id = #{userId};
</select>

<!-- 嵌套查询 -->
<select id="getCatByUserId" resultType="com.yigongsui.pojo.Cat">
    select * from cat c where c.user_id = #{userId}
</select>

结果:

==>  Preparing: select * from user u where u.user_id = ?;
==> Parameters: 1(Integer)
<==    Columns: user_id, user_name
<==        Row: 1, 张三
====>  Preparing: select * from cat c where c.user_id = ?
====> Parameters: 1(Integer)
<====    Columns: cat_id, cat_name, user_id
<====        Row: 1, 小花, 1
<====        Row: 3, 小黄, 1
<====        Row: 4, 小绿, 1
<====      Total: 3
<==      Total: 1
User(userId=1, userName=张三, catList=[Cat(catId=1, catName=小花, userId=1, user=null), Cat(catId=3, catName=小黄, userId=1, user=null), Cat(catId=4, catName=小绿, userId=1, user=null)])

多对多

多对多一般发生在3个表之间,步骤同一对多。

多表映射优化

settings 配置中有一个 autoMappingBehavior 可以处理多表复杂映射。

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

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

总结

关联关系配置项关键词所在配置文件和具体位置
对一association标签/javaType属性/property属性Mapper配置文件中的resultMap标签内
对多collection标签/ofType属性/property属性Mapper配置文件中的resultMap标签内

Mybatis 动态 sql

MyBatis 动态 SQL 允许根据不同条件动态生成 SQL 语句,常用于处理复杂的查询、更新或插入逻辑。

核心标签

标签作用
<if>条件判断,根据表达式结果决定是否包含 SQL 片段。
<choose>/<when>/<otherwise>多分支选择(类似 Java 的 switch-case)。
<trim>智能去除 SQL 片段中的多余前缀/后缀(如 ANDOR、逗号)。
<where>自动处理 WHERE 子句,避免条件为空时生成无效 WHERE 或多余的 AND/OR
<set>自动处理 UPDATE 语句中的 SET 子句,去除多余的逗号。
<foreach>遍历集合参数,生成 IN 查询或批量操作的 SQL 片段。
<bind>定义变量,简化复杂表达式或重复逻辑。

where 和 if 标签

两个标签通常要搭配使用,根据条件动态查询数据。

场景:根据用户和姓名和年龄查询数据,如果条件不存在就查询所有数据。

实体类:

@Data
public class User {

	private Integer userId;
	private String userName;
	private Integer userAge;

}

接口:

List<User> getUserByNameAndAge(@Param("name") String name, @Param("age") Integer age);

mapper.xml

<select id="getUserByNameAndAge" resultType="com.yigongsui.pojo.User">
    select * from user
    <where>
        <if test="name != null and name != ''">
            user_name = #{name}
        </if>
        <if test="age != null">
            and user_age = #{age}
        </if>
    </where>
</select>

需要开启驼峰自动映射

if 标签的 test 属性用于判断查询是否存在,如果满足条件才会拼接语句。

拼接的语句从第2条开始,要在前面加上 and 或 or 关键字进行拼接(也可以在第1条语句前也加上),即时第1条语句不满足条件,Mybatis 会自动省略多余的 and、or 避免 sql 语句出错。

如果条件均不满足,where 也会省略。

set 标签

set 标签用于更新语句。

接口:

int updateUserById(User user);

mapper.xml

<update id="updateUserById">
    update user
    <set>
        <if test="userName != null and userName != ''">
            user_name = #{userName},
        </if>
        <if test="userAge != null">
            user_age = #{userAge}
        </if>
    </set>
    where user_id = #{userId}
</update>

使用 set 标签时,if 下语句需要加上 ,set 标签会自动删除不需要的 , (最后一条语句可加可不加)。

至少要有一个条件满足,否则会报错,毕竟修改至少要修改一条字段。

trim 标签

作用:自定义去除 SQL 片段的前缀/后缀。

属性

  • prefix:添加前缀。
  • suffix:添加后缀。
  • prefixOverrides:去除前缀。
  • suffixOverrides:去除后缀。

场景:替代 where 标签。

choose/when/otherwise 标签

作用:多条件分支选择,类似于 java 中的 switch-case

场景:根据优先级查询用户。

foreach 标签

作用:遍历集合参数(如 List、数组、Map)。

属性

  • collection:集合参数名。
  • item:遍历时的元素别名。
  • index:遍历时的索引别名。
  • open/close:循环体开始/结束时的字符。
  • separator:元素间的分隔符。

示例 1IN 查询:

需要设置 @Param 属性名。

List<User> getUserByIds(@Param("ids") List<Integer> ids);
<select id="getUserByIds" resultType="User">
  SELECT * FROM user
  WHERE user_id IN
  <foreach collection="ids" item="userId" open="(" separator="," close=")">
    #{userId}
  </foreach>
</select>

item 指定的属性名与 #{} 内名相同。

示例 2:批量插入

<insert id="batchInsertUsers">
  INSERT INTO user (user_name, user_age) VALUES
  <foreach collection="users" item="user" separator=",">
    (#{user.userName}, #{user.userAge})
  </foreach>
</insert>

sql 片段

当一条 sql 语句出现多次时,可以将这条 sql 语句提取出来进行复用。

提取 sql:

<!-- 使用sql标签抽取重复出现的SQL片段 -->
<sql id="mySelectSql">
    select user_id,user_name,user_age from user
</sql>

引用 sql:

<!-- 使用include标签引用声明的SQL片段 -->
<include refid="mySelectSql"/>

Mybatis 插件机制

原理

MyBatis 的插件机制基于 动态代理责任链模式,允许开发者拦截以下四大核心接口的方法:

  • Executor:执行 SQL 的顶层对象,负责增删改查操作。
  • StatementHandler:处理 SQL 预编译和结果集。
  • ParameterHandler:处理 SQL 参数映射。
  • ResultSetHandler:处理结果集映射。

插件工作流程

  1. 定义拦截器:实现 Interceptor 接口,指定拦截的方法。
  2. 创建代理对象:MyBatis 通过动态代理生成目标对象的代理。
  3. 责任链调用:多个插件按配置顺序形成拦截链,依次执行前置处理、原方法、后置处理。

实现步骤

@Intercepts({
    @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    )
})
public class MyPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 拦截逻辑
        return invocation.proceed();
    }
}

在MyBatis配置文件中注册:

<plugins>
    <plugin interceptor="com.example.MyPlugin"/>
</plugins>

分页插件 PageHelper

PageHelper 是基于 MyBatis 插件机制的分页工具,通过拦截 Executor 的查询方法,自动添加分页 SQL(如 MySQL 的 LIMIT),并计算总记录数。

核心特性

  • 零侵入:无需修改原有 SQL,通过简单 API 触发分页。
  • 支持多种数据库:自动识别数据库类型,生成适配的分页语句。
  • 性能优化:通过 COUNT 查询获取总记录数,避免全表扫描。

使用步骤

  1. 添加依赖
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.3.2</version>
</dependency>
  1. 配置插件

在配置文件配置:

<configuration>
    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!-- 配置数据库方言(可选,PageHelper 会自动检测) -->
            <property name="helperDialect" value="mysql"/>
            <!-- 合理化分页参数(页码超出范围时自动修正) -->
            <property name="reasonable" value="true"/>
        </plugin>
    </plugins>
</configuration>
  1. 使用
// 1. 开启分页:pageNum - 页码,pageSize - 每页数量
PageHelper.startPage(1, 10);

// 2. 执行查询(紧跟 startPage 后的第一个查询会自动分页)
List<User> users = userMapper.selectAll();

// 3. 封装分页结果
PageInfo<User> pageInfo = new PageInfo<>(users);

// 输出分页信息
System.out.println("总记录数:" + pageInfo.getTotal());
System.out.println("当前页数据:" + pageInfo.getList());

核心原理

  1. 拦截 Executor 的查询方法

    PageInterceptor 拦截 Executorquery() 方法,判断是否需要分页。

  2. 改写原始 SQL

    • 添加分页语句(如 MySQL 的 LIMIT)。
    • 执行 COUNT 查询获取总记录数。
  3. 封装分页结果

    将分页后的数据与总记录数封装到 PagePageInfo 对象中。

高级功能与配置

分页参数传递
  • 直接传递参数

    PageHelper.startPage(pageNum, pageSize);
    
  • 通过方法参数传递(需配置 supportMethodsArguments=true):

    public PageInfo<User> findUsers(@Param("pageNum") int pageNum, 
                                    @Param("pageSize") int pageSize) {
        // 无需手动调用 startPage
        return PageHelper.startPage(pageNum, pageSize)
            .doSelectPageInfo(() -> userMapper.selectAll());
    }
    
排序支持
PageHelper.startPage(1, 10, "name desc, age asc");
List<User> users = userMapper.selectAll();
物理分页 vs 内存分页
  • 物理分页:通过 SQL 分页(默认),性能高。

  • 内存分页:先查全部数据,再在内存中分页(不推荐):

    PageHelper.startPage(1, 10, false);
    

与其他分页方案对比

方案优点缺点
PageHelper无侵入、支持多数据库、使用简单依赖 MyBatis 插件机制
手动 LIMIT完全控制 SQL需手动编写分页逻辑和 COUNT 查询
Spring Data JPA与 Spring 生态集成好仅适用于 JPA,灵活性较低

Mybatis 的缓存机制

MyBatis 提供了一套灵活的缓存机制,用于减少数据库访问次数,提升查询性能。其缓存分为 一级缓存二级缓存,两者的作用范围、配置方式及生命周期不同。以下是详细解析:

一级缓存(本地缓存)

作用域

  • SqlSession 级别:同一个 SqlSession 内有效(默认开启)。
  • 生命周期:与 SqlSession 绑定,会话关闭或执行 commit()rollback()clearCache() 时缓存失效。

工作机制

  • 缓存范围
    同一 SqlSession 多次执行相同的查询(完全相同的 SQL 和参数),会直接返回缓存的结果,不访问数据库。
  • 失效条件
    • 执行 INSERT/UPDATE/DELETE 操作(无论是否修改缓存数据)。
    • 手动调用 sqlSession.clearCache()
    • 配置 flushCache="true" 的查询(如 <select flushCache="true">)。

示例验证

SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);

// 第一次查询,访问数据库
User user1 = mapper.findUserById(1); 
// 第二次查询,命中一级缓存
User user2 = mapper.findUserById(1); 

System.out.println(user1 == user2); // true(同一对象引用)

session.commit(); // 提交后缓存失效

优缺点

优点缺点
自动开启,无需配置。作用域小,仅限同一 SqlSession。
减少重复查询开销。多个 SqlSession 无法共享缓存数据。

二级缓存(全局缓存)

作用域

  • Mapper 级别:跨 SqlSession 共享(需手动开启)。
  • 生命周期:与应用程序的生命周期一致,除非显式清除或配置过期时间。

工作机制

  • 缓存共享

    不同 SqlSession 访问同一 Mapper 的查询,可以共享缓存数据。

  • 存储方式

    默认使用内存存储(可集成 Redis、Ehcache 等第三方缓存)。

配置步骤

全局启用二级缓存

mybatis-config.xml 中配置:

<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>
Mapper 文件中声明缓存

在对应的 Mapper.xml 中添加 <cache> 标签:

<mapper namespace="com.example.mapper.UserMapper">
    <!-- 开启二级缓存 -->
    <cache 
        eviction="LRU"              <!-- 淘汰策略(默认LRU) -->
        flushInterval="60000"       <!-- 自动刷新间隔(毫秒) -->
        size="1024"                 <!-- 缓存最大对象数 -->
        readOnly="true"/>           <!-- 是否只读(默认false) -->
    
    <select id="findUserById" resultType="User">
        SELECT * FROM user WHERE id = #{id}
    </select>
</mapper>
POJO 类实现序列化

若使用默认缓存,需确保返回的对象实现 Serializable 接口:

public class User implements Serializable {
    private Integer id;
    private String name;
    // Getter & Setter
}

缓存淘汰策略(eviction

策略说明
LRU最近最少使用(默认),移除最长时间未被使用的对象。
FIFO先进先出,按对象进入缓存的顺序移除。
SOFT软引用,基于垃圾回收器状态和软引用规则移除对象。
WEAK弱引用,更积极地移除对象。
缓存失效条件
  • 执行 <insert><update><delete> 操作时,同一 Mapper 的缓存自动清空。
  • 手动调用 sqlSession.clearCache() 或配置 flushCache="true"
示例验证
// 第一个 SqlSession
SqlSession session1 = sqlSessionFactory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.findUserById(1); // 查询数据库
session1.close(); // 提交并关闭,数据存入二级缓存

// 第二个 SqlSession
SqlSession session2 = sqlSessionFactory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.findUserById(1); // 命中二级缓存
session2.close();

System.out.println(user1 == user2); // false(不同会话,但数据相同)
优缺点
优点缺点
跨会话共享,减少数据库压力。配置复杂,需处理序列化和并发问题。
支持第三方缓存扩展。数据实时性差,可能读到旧数据。

缓存使用注意事项

避免脏读

  • 场景:多个 Mapper 操作同一张表时,二级缓存可能未及时更新。

  • 解决:在关联的 Mapper 配置中引用同一缓存命名空间:

    <cache-ref namespace="com.example.mapper.UserMapper"/>
    

细粒度缓存控制

  • 关闭单条语句的缓存

    <select id="findUser" resultType="User" useCache="false">
        SELECT * FROM user
    </select>
    
  • 强制刷新缓存

    <select id="findUser" resultType="User" flushCache="true">
        SELECT * FROM user
    </select>
    

集成第三方缓存

Ehcache 为例:

  1. 添加依赖

    <dependency>
        <groupId>org.mybatis.caches</groupId>
        <artifactId>mybatis-ehcache</artifactId>
        <version>1.2.2</version>
    </dependency>
    
  2. 配置 Mapper 缓存

    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
    

缓存的适用场景

场景推荐缓存说明
频繁读取,极少修改二级缓存如配置表、商品分类等静态数据。
会话内重复查询一级缓存同一事务中多次查询相同数据。
实时性要求高禁用缓存如订单状态、库存数量等需实时获取的数据。

总结

  • 一级缓存:默认开启,会话级别,适合短周期、高重复查询。
  • 二级缓存:需手动配置,全局共享,适合读多写少的静态数据。
  • 缓存选择:根据业务场景权衡性能与数据一致性,必要时结合第三方缓存(如 Redis)提升扩展性。
  • 避坑指南:注意事务提交、缓存刷新策略,避免脏读和内存溢出。

通过合理配置 MyBatis 缓存,可显著减少数据库负载,但在高并发或分布式场景中,建议结合分布式缓存(如 Redis)替代默认的本地二级缓存。


http://www.kler.cn/a/585207.html

相关文章:

  • C# Type类中Name、FullName、Namespace、AssemblyQualifiedName的区别
  • 了解一下HTTP的短连接和长连接
  • 从波士顿动力到Figure AI:探寻人工智能驱动的机器人智能化
  • UdpClient
  • 什么是 MyBatis?
  • 基于卡尔曼滤波的雷达光电多目标航迹融合算法matlab仿真
  • 基于ssm的宠物医院信息管理系统(全套)
  • 线程池参数调优
  • 游戏引擎学习第152天
  • AI时代研究卷积神经网络(CNN)工具与方法
  • 【即插即用涨点模块】CAA上下文锚点注意力机制:有效捕捉全局信息,助力高效涨点【附源码+注释】
  • 21天 - 说说 TCP 的四次挥手?TCP 的粘包和拆包能说说吗?说说 TCP 拥塞控制的步骤?
  • 谷歌 Gemini 2.0 Flash实测:1条指令自动出图+配故事!
  • el-table 插槽踩过的坑 :slot-scope 和#default的区别
  • 代码随想录-回溯
  • 如何优雅地将Collection转为Map?
  • 平安养老险广西分公司2025年“3∙15”金融消费者权益教育宣传活动暨南湖公园健步行活动
  • 【C语言】编译和链接详解
  • Redis的缓存雪崩、缓存击穿、缓存穿透与缓存预热、缓存降级
  • 2025-03-15 学习记录--C/C++-PTA 练习3-4 统计字符