鳳舞酒天头像
关注
MyBatis 开发入门详细讲义封面图

MyBatis 开发入门详细讲义

MyBatis 开发入门详细讲义


第一章:MyBatis 开发入门

1.1 什么是 MyBatis

MyBatis 是一款基于 Java 的半自动持久层框架(ORM 框架),最初由 Apache 软件基金会开发(原名 iBatis),2010 年迁移至 Google Code 并更名,2013 年转至 GitHub 平台。官方手册链接: mybatis官方手册

MyBatis 的核心优势在于:

  • SQL 与代码分离:通过 XML 或注解配置 SQL 映射,将 SQL 语句从 Java 代码中剥离,便于管理和维护。
  • 灵活的 SQL 控制:开发者可以完全掌控 SQL 语句的编写,适合复杂查询和性能敏感场景。
  • 动态 SQL 支持:提供丰富的动态 SQL 标签,根据条件智能拼接 SQL,避免手动字符串拼接的繁琐与风险。
  • 结果映射强大:支持自动映射(resultType)和手动映射(resultMap),能处理复杂的级联关联。

与 JDBC 相比,MyBatis 封装了繁琐的资源管理和结果集处理,大幅减少样板代码;与 Hibernate 相比,MyBatis 需手动编写 SQL,灵活性更高。


1.2 MyBatis 环境的构建

环境要求
  • JDK 1.8 及以上版本
  • MySQL 8.0+
  • Maven 3.6+
  • IDEA / VSCode 等开发工具
方式一:Maven 引入依赖

在 pom.xml 中添加 MyBatis 和 MySQL 驱动依赖:

<dependencies>
    <!-- MyBatis 核心依赖 -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.9</version>
    </dependency>
    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.28</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>
方式二:手动下载 JAR 包

从 MyBatis GitHub Releases 下载 mybatis-3.x.x.zip,解压后将核心包和 lib 目录中的依赖包添加到类路径中。

创建数据库与表
CREATE DATABASE mybatis_demo;
USE mybatis_demo;

CREATE TABLE user (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL,
    password VARCHAR(50) NOT NULL,
    email VARCHAR(100),
    age INT
);

INSERT INTO user (username, password, email, age) VALUES
('张三', '123456', '[email protected]', 25),
('李四', '123456', '[email protected]', 30),
('王五', '123456', '[email protected]', 28);

1.3 MyBatis 的工作原理

MyBatis 的工作原理分为初始化、代理封装和数据访问三个阶段,架构上分为接口层、核心处理层和基础支撑层。

工作流程图解

mybatis工作流程

核心组件详解
组件职责生命周期
SqlSessionFactoryBuilder根据配置信息构建 SqlSessionFactory方法级别,用完即弃
SqlSessionFactory创建 SqlSession 的工厂,是单个数据库映射关系编译后的内存镜像应用级别,建议单例
SqlSession数据库交互的主入口,执行 SQL、管理事务请求级别,非线程安全,用完需关闭
ExecutorSQL 执行引擎,管理缓存与 Statement 创建策略(SimpleExecutor / ReuseExecutor / BatchExecutor)由 SqlSession 管理
StatementHandler负责 SQL 预编译、参数设置及执行由 Executor 管理
ParameterHandler将 Java 参数转换为 JDBC 参数并设置到 PreparedStatement 中由 Executor 管理
ResultSetHandler将 JDBC 返回的 ResultSet 根据 ResultMap 映射为 Java 对象由 Executor 管理
Mapper 接口与代理定义数据库操作方法,由 MyBatis 动态生成实现类由 SqlSession 管理
详细执行流程
  1. 初始化阶段:读取 mybatis-config.xml 全局配置文件和 Mapper 映射文件(XML 或注解),通过 XMLConfigBuilder 解析配置信息,构建 Configuration 全局配置对象,并将 SQL 配置信息封装为 MappedStatement 对象存储在内存中,最终通过 SqlSessionFactoryBuilder 构建 SqlSessionFactory。
  2. 代理封装阶段:通过 SqlSession.getMapper() 获取 Mapper 接口代理对象。MyBatis 利用 JDK 动态代理机制,由 MapperProxyFactory 创建 MapperProxy 实例,拦截接口方法调用,根据"接口全限定名 + 方法名"定位对应的 MappedStatement,将方法调用转换为 SqlSession 的数据库操作。
  3. 数据访问阶段:SqlSession 将操作委托给 Executor 执行器。Executor 负责 SQL 执行调度及一级缓存维护。执行过程中,StatementHandler 通过 ParameterHandler 将 Java 参数转换为 JDBC 参数;SQL 执行后,ResultSetHandler 将结果集映射为 Java 对象。执行前会先查询一级缓存(SqlSession 级别)和二级缓存(Mapper 级别)。

1.4 MyBatis 的入门程序

第一步:创建核心配置文件 mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 环境配置 -->
    <environments 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/mybatis_demo?serverTimezone=Asia/Shanghai"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <!-- 映射文件 -->
    <mappers>
        <mapper resource="com/example/mapper/UserMapper.xml"/>
    </mappers>
</configuration>
第二步:创建实体类 User.java
package com.example.pojo;

public class User {
    private Integer id;
    private String username;
    private String password;
    private String email;
    private Integer age;

    // 构造方法
    public User() {}

    public User(Integer id, String username, String password, String email, Integer age) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.email = email;
        this.age = age;
    }

    // Getter 和 Setter
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }

    @Override
    public String toString() {
        return "User{id=" + id + ", username='" + username + "', email='" + email + "', age=" + age + "}";
    }
}
第三步:创建 Mapper 接口 UserMapper.java
package com.example.mapper;

import com.example.pojo.User;
import java.util.List;

public interface UserMapper {
    // 根据 ID 查询用户
    User selectUserById(Integer id);
    // 查询所有用户
    List<User> selectAllUsers();
    // 插入用户
    int insertUser(User user);
    // 更新用户
    int updateUser(User user);
    // 删除用户
    int deleteUserById(Integer id);
}
第四步:创建 SQL 映射文件 UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
    <!-- 根据 ID 查询用户 -->
    <select id="selectUserById" parameterType="int" resultType="com.example.pojo.User">
        SELECT * FROM user WHERE id = #{id}
    </select>
    <!-- 查询所有用户 -->
    <select id="selectAllUsers" resultType="com.example.pojo.User">
        SELECT * FROM user
    </select>
    <!-- 插入用户 -->
    <insert id="insertUser" parameterType="com.example.pojo.User">
        INSERT INTO user (username, password, email, age)
        VALUES (#{username}, #{password}, #{email}, #{age})
    </insert>
    <!-- 更新用户 -->
    <update id="updateUser" parameterType="com.example.pojo.User">
        UPDATE user
        SET username = #{username}, password = #{password}, email = #{email}, age = #{age}
        WHERE id = #{id}
    </update>
    <!-- 删除用户 -->
    <delete id="deleteUserById" parameterType="int">
        DELETE FROM user WHERE id = #{id}
    </delete>
</mapper>
第五步:编写工具类 MybatisUtils.java
package com.example.utils;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;

public class MybatisUtils {
    private static SqlSessionFactory sqlSessionFactory;

    static {
        try {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static SqlSession getSession() {
        return sqlSessionFactory.openSession();
    }
}
第六步:编写测试类
package com.example.test;

import com.example.mapper.UserMapper;
import com.example.pojo.User;
import com.example.utils.MybatisUtils;
import org.apache.ibatis.session.SqlSession;
import java.util.List;

public class UserTest {
    public static void main(String[] args) {
        SqlSession session = MybatisUtils.getSession();
        try {
            UserMapper mapper = session.getMapper(UserMapper.class);
            // 测试查询所有用户
            List<User> users = mapper.selectAllUsers();
            for (User user : users) {
                System.out.println(user);
            }
            // 测试根据 ID 查询
            User user = mapper.selectUserById(1);
            System.out.println("查询结果:" + user);
            session.commit();
        } finally {
            session.close();
        }
    }
}

1.5 Mybatis generator自动生成工具去生成实体类和mapper文件

  1. 导入jar包 mybatis-generator-core-1.4.1.jar 到lib文件夹
  2. 在项目根目录下新建generator.xml, 写入
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <!-- 确定数据库驱动包位置 采用绝对路径 -->
    <classPathEntry location="D:\Spring Boot\learnMybatis\lib\mysql-connector-java-8.0.29.jar" />
    <context id="mysqlTables" targetRuntime="MyBatis3">
        <commentGenerator>
            <property name="suppressAllComments" value="true" />
        </commentGenerator>
        <!-- 数据库链接URL、用户名、密码(确认数据库是否正确,用户名和密码是否正确) -->
        <jdbcConnection
                driverClass="com.mysql.cj.jdbc.Driver"
                connectionURL="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true&amp;characterEncoding=UTF-8&amp;allowMultiQueries=true&amp;serverTimezone=GMT%2B8"
                userId="root" password="123456">
                <!--            只查mybatis数据库里的表 -->
<!--            <property name="nullCatalogMeansCurrent" value="true"/>-->
        </jdbcConnection>
        <javaTypeResolver>
            <property name="forceBigDecimals" value="true" />
        </javaTypeResolver>
        <!-- 生成模型(MyBatis里面用到实体类)的从src下开始写的包名和src包的绝对路径-->
        <javaModelGenerator targetPackage="com.giit.entity" targetProject="D:\Spring Boot\learnMybatis\src">
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>
        <!-- 生成的映射文件(MyBatis的SQL语句xml文件)从src下开始写的包名和src包的绝对路径 -->
        <sqlMapGenerator targetPackage="com.giit.dao" targetProject="D:\Spring Boot\learnMybatis\src">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>
        <!-- 生成DAO/Mapper接口的从src下开始写的包名和src包的绝对路径 -->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.giit.dao" targetProject="D:\Spring Boot\learnMybatis\src">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>
        <!-- 要生成那些表(更改tableName和domainObjectName就可以,前提数据库中的user表已创建) 注意domainObjectName的首字母大写
               catalog主要是找指定数据库下的表 -->
        <table tableName="user" domainObjectName="User" catalog="mybatis" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false" />
    </context>
</generatorConfiguration>
  1. 在项目根目录下打开terminal, 输入
java -jar .\lib\mybatis-generator-core-1.4.1.jar -configfile .\generator.xml -overwrite

1.6 MyBatis 与 Spring 框架的整合开发

为什么要整合 Spring?

MyBatis 原生开发存在以下痛点:

  • 组件创建繁琐:需手动加载配置文件、创建 SqlSessionFactory、获取 SqlSession、生成 Mapper 代理对象,代码冗余且耦合度高;
  • 事务管理混乱:需手动控制 SqlSession 的提交/回滚,多数据源场景下一致性难以保障;
  • 资源管理低效:数据库连接需手动维护,无连接池复用机制。

Spring 的解决方案:

  • 组件托管:通过 IOC 容器接管 DataSource、SqlSessionFactory、Mapper 代理对象,自动管理对象创建、依赖注入与销毁;
  • 事务统一管控:基于 AOP 实现声明式事务,通过注解或 XML 配置即可完成事务规则定义;
  • 资源池化:整合第三方连接池(Druid/C3P0),优化数据库连接的创建与复用。
整合核心组件与依赖关系
组件核心职责MyBatis 原生方式Spring 整合方式
DataSource管理数据库连接手动配置 environments 标签Spring IOC 容器管理
SqlSessionFactory创建 SqlSession手动加载 mybatis-config.xml通过 SqlSessionFactoryBean 自动创建
SqlSession数据库操作会话手动调用 openSession()Spring 通过 SqlSessionTemplate 管理
Mapper 代理对象映射 SQL 到接口方法手动通过 getMapper() 获取MapperScannerConfigurer 自动扫描
PlatformTransactionManager事务管理器无原生支持DataSourceTransactionManager

整合的关键桥梁是 mybatis-spring.jar,其核心类 SqlSessionFactoryBean 替代了 MyBatis 原生的 SqlSessionFactoryBuilder。

Spring 整合 MyBatis 步骤

第一步:引入依赖
需要引入spring相关jar包和mybatis相关jar包以及mybatis-spring-3.x.x.jar包

第二步:在项目src下或者resources包下配置 application.xml 或者采用java配置类形式

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">

<!--    加载数据库配置文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
<!--    配置数据源-->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
<!--        最大活跃连接数-->
        <property name="maxTotal" value="${jdbc.maxTotal}"/>
<!--        最小空闲连接数-->
        <property name="minIdle" value="${jdbc.minIdle}"/>
<!--        初始连接数-->
        <property name="initialSize" value="${jdbc.initialSize} "/>
    </bean>
<!--    配置事务管理器-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
<!--    启用注解驱动的事务管理-->
    <tx:annotation-driven transaction-manager="txManager"/>
<!--    配置mybaitis工厂,同时指定数据源,并于mybatis完美配合-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
<!--        configLocation的属性值为mybatis-config.xml文件的位置-->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>
<!--    Mapper代理开发,MapperScannerConfigurer将包中所有接口自动装配为mybatis映射接口Mapperd的实现类
的实例(映射器),所有映射器都被自动注入SqlSessionFactory实例,同时扫描包中的SQL映射文件,Mybatis配置文件不在加载SQL映射文件-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--        mybatis-Spring组件扫描器,basePackage指定Mapper接口所在的包,多个包用逗号分隔-->
        <property name="basePackage" value="com.giit.learnmybatisspring.dao"/>
<!--        sqlSessionFactoryBeanName 传 bean 的名称(字符串)而非引用(官方推荐),避免 MapperScannerConfigurer
在占位符解析之前就提前实例化 sqlSessionFactory,导致 ${jdbc.driver} 等占位符无法替换-->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>

<!--        避免使用ref指向sqlSessionFactory, 这个 ref 引用会让 sqlSessionFactory(连带 dataSource)在占位符解析之前就被提前实例化,于是 ${jdbc.driver} 原样进了数据源。-->
<!--        <property name="sqlSessionFactory" ref="sqlSessionFactory"/>-->
    </bean>
<!--    扫描service包-->
    <context:component-scan base-package="com.giit.learnmybatisspring.service"/>
</beans>

或者在com.giit.learnmybatisspring下新建一个config包,并且新建一个配置类

package com.giit.learnmybatisspring.config;

import javax.sql.DataSource;

import org.apache.commons.dbcp2.BasicDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * 与原 applicationContext.xml 完全等价的 Java 配置类
 */
@Configuration                                        // <beans>
@PropertySource("classpath:jdbc.properties")          // <context:property-placeholder>
@ComponentScan("com.giit.learnmybatisspring.service") // <context:component-scan>
@MapperScan(basePackages = "com.giit.learnmybatisspring.dao",sqlSessionFactoryRef = "sqlSessionFactory")        // <MapperScannerConfigurer>
@EnableTransactionManagement                          // <tx:annotation-driven>
public class SpringConfig {

    // <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
    @Bean
    public DataSource dataSource(
            @Value("${jdbc.driver}") String driver,
            @Value("${jdbc.url}") String url,
            @Value("${jdbc.username}") String username,
            @Value("${jdbc.password}") String password,
            @Value("${jdbc.maxTotal}") int maxTotal,
            @Value("${jdbc.minIdle}") int minIdle,
            @Value("${jdbc.initialSize}") int initialSize) {

        BasicDataSource ds = new BasicDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(username);
        ds.setPassword(password);
        ds.setMaxTotal(maxTotal);       // 最大活跃连接数
        ds.setMinIdle(minIdle);         // 最小空闲连接数
        ds.setInitialSize(initialSize); // 初始连接数
        return ds;
    }

    // <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    @Bean
    public DataSourceTransactionManager txManager(DataSource dataSource) {
        DataSourceTransactionManager txManager = new DataSourceTransactionManager();
        txManager.setDataSource(dataSource);
        return txManager;
    }

    // <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        // configLocation:指定 mybatis-config.xml 的位置
        factoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
        return factoryBean;
    }
}

第三步:在项目src下或者resources包下配置 jdbc.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/students?useSSL=false&serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=123456
jdbc.maxTotal=30
jdbc.minIdle=10
jdbc.initialSize=5

第四步:在项目src下或者resources包下配置 log4j.properties

# 全局日志配置
log4j.rootLogger=ERROR, stdout
# mybatis 日志配置
log4j.logger.com.mybatis.mapper=DEBUG
# console 输出配置
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
# console 输出格式配置
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p  [%t] - %m%n

第五步:在项目src下或者resources包下配置 mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="logImpl" value="LOG4J"/>
    </settings>
    <typeAliases>
        <package name="com.giit.learnmybatisspring.entity"/>
    </typeAliases>
</configuration

第六步:在项目src下新建com.giit.learnmybatisspring.entity包

该包主要存放javabean文件

数据库文件如下:

create database if not exists students;
use students;
create table user(
    id       int auto_increment
        primary key,
    username varchar(255) null,
    password varchar(255) null,
    email    varchar(255) null
);

INSERT INTO students.user (username, password, email) VALUES ('admin', '123456', '[email protected]');
INSERT INTO students.user (username, password, email) VALUES ('user1', 'userOne', '[email protected]');

Javabean如下,采用mybatis-x插件生成

package com.giit.learnmybatisspring.entity;


public class User {

    private Integer id;

    private String username;
    
    private String password;

    private String email;
    
    public Integer getId() {
        return id;
    }
    
    public void setId(Integer id) {
        this.id = id;
    }
    
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public boolean equals(Object that) {
        if (this == that) {
            return true;
        }
        if (that == null) {
            return false;
        }
        if (getClass() != that.getClass()) {
            return false;
        }
        User other = (User) that;
        return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
            && (this.getUsername() == null ? other.getUsername() == null : this.getUsername().equals(other.getUsername()))
            && (this.getPassword() == null ? other.getPassword() == null : this.getPassword().equals(other.getPassword()))
            && (this.getEmail() == null ? other.getEmail() == null : this.getEmail().equals(other.getEmail()));
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
        result = prime * result + ((getUsername() == null) ? 0 : getUsername().hashCode());
        result = prime * result + ((getPassword() == null) ? 0 : getPassword().hashCode());
        result = prime * result + ((getEmail() == null) ? 0 : getEmail().hashCode());
        return result;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getSimpleName());
        sb.append(" [");
        sb.append("Hash = ").append(hashCode());
        sb.append(", id=").append(id);
        sb.append(", username=").append(username);
        sb.append(", password=").append(password);
        sb.append(", email=").append(email);
        sb.append("]");
        return sb.toString();
    }
}

第七步:在项目src下新建com.giit.learnmybatisspring.dao包

用mybatis-x插件生成UserMapper.java和UserMapper.xml

import com.giit.learnmybatisspring.entity.User;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface UserMapper {

    int deleteByPrimaryKey(Long id);

    int insert(User record);

    int insertSelective(User record);

    User selectByPrimaryKey(Long id);

    int updateByPrimaryKeySelective(User record);

    int updateByPrimaryKey(User record);

    List<User> selectAllUsers();

}
<?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.giit.learnmybatisspring.dao.UserMapper">

    <resultMap id="BaseResultMap" type="com.giit.learnmybatisspring.entity.User">
            <id property="id" column="id" jdbcType="INTEGER"/>
            <result property="username" column="username" jdbcType="VARCHAR"/>
            <result property="password" column="password" jdbcType="VARCHAR"/>
            <result property="email" column="email" jdbcType="VARCHAR"/>
    </resultMap>

    <sql id="Base_Column_List">
        id,username,password,email
    </sql>

    <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from user
        where  id = #{id,jdbcType=INTEGER} 
    </select>
    <select id="selectAllUsers" resultType="com.giit.learnmybatisspring.entity.User">
        select
        <include refid="Base_Column_List" />
        from user
    </select>

    <delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
        delete from user
        where  id = #{id,jdbcType=INTEGER} 
    </delete>
    <insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.giit.learnmybatisspring.entity.User" useGeneratedKeys="true">
        insert into user
        ( id,username,password,email)
        values (#{id,jdbcType=INTEGER},#{username,jdbcType=VARCHAR},#{password,jdbcType=VARCHAR},#{email,jdbcType=VARCHAR})
    </insert>
    <insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.giit.learnmybatisspring.entity.User" useGeneratedKeys="true">
        insert into user
        <trim prefix="(" suffix=")" suffixOverrides=",">
                <if test="id != null">id,</if>
                <if test="username != null">username,</if>
                <if test="password != null">password,</if>
                <if test="email != null">email,</if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
                <if test="id != null">#{id,jdbcType=INTEGER},</if>
                <if test="username != null">#{username,jdbcType=VARCHAR},</if>
                <if test="password != null">#{password,jdbcType=VARCHAR},</if>
                <if test="email != null">#{email,jdbcType=VARCHAR},</if>
        </trim>
    </insert>
    <update id="updateByPrimaryKeySelective" parameterType="com.giit.learnmybatisspring.entity.User">
        update user
        <set>
                <if test="username != null">
                    username = #{username,jdbcType=VARCHAR},
                </if>
                <if test="password != null">
                    password = #{password,jdbcType=VARCHAR},
                </if>
                <if test="email != null">
                    email = #{email,jdbcType=VARCHAR},
                </if>
        </set>
        where   id = #{id,jdbcType=INTEGER} 
    </update>
    <update id="updateByPrimaryKey" parameterType="com.giit.learnmybatisspring.entity.User">
        update user
        set 
            username =  #{username,jdbcType=VARCHAR},
            password =  #{password,jdbcType=VARCHAR},
            email =  #{email,jdbcType=VARCHAR}
        where  id = #{id,jdbcType=INTEGER}
    </update>
</mapper>

第八步:在项目src下新建其他包

新建service层,负责对dao层进行访问

// service接口
import com.giit.learnmybatisspring.entity.User;

import java.util.List;

public interface UserService {
    public User getUserById(Long id);

    public List<User> getAllUsers();

    public int addUser(User user);

    public int modifyUser(User user);

    public int removeUser(Long id);
}

// service接口实现类
import com.giit.learnmybatisspring.dao.UserMapper;
import com.giit.learnmybatisspring.entity.User;
import com.giit.learnmybatisspring.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    public User getUserById(Long id) {
        return userMapper.selectByPrimaryKey(id);
    }

    public List<User> getAllUsers() {
        return userMapper.selectAllUsers();
    }

    @Transactional
    public int addUser(User user) {
        return userMapper.insert(user);
    }

    @Transactional
    public int modifyUser(User user) {
        return userMapper.updateByPrimaryKey(user);
    }

    @Transactional
    public int removeUser(Long id) {
        return userMapper.deleteByPrimaryKey(id);
    }
}

第九步:在项目src下新建DemoTest.java进行测试

当然可以新建Controller层,负责控制转发,后期springMVC会具体讲解

package com.giit.learnmybatisspring;

import com.giit.learnmybatisspring.dao.UserMapper;
import com.giit.learnmybatisspring.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class DemoTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
        UserService userService = context.getBean(UserService.class);
        System.out.println(userService.getUserById(1L));
    }
}
Spring Boot 整合 MyBatis 步骤(推荐)

第一步:引入依赖

<dependencies>
    <!-- Spring Boot MyBatis Starter -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.3.1</version>
    </dependency>
    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

Spring Boot 2.x 对应 mybatis-spring-boot-starter 2.x 版本,Spring Boot 3.x 对应 3.x 版本。

第二步:配置 application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mybatis_demo?serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*.xml    # 指定 XML 映射文件路径
  type-aliases-package: com.example.pojo      # 指定实体类包路径
  configuration:
    map-underscore-to-camel-case: true        # 开启驼峰映射

第三步:创建 Mapper 接口

package com.example.mapper;

import com.example.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper  // 标记为 MyBatis Mapper
public interface UserMapper {
    User selectUserById(Integer id);
    List<User> selectAllUsers();
    int insertUser(User user);
    int updateUser(User user);
    int deleteUserById(Integer id);
}

或者在启动类上使用 @MapperScan 指定扫描路径:

@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

第四步:创建 Service 层

package com.example.service;

import com.example.mapper.UserMapper;
import com.example.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User getUserById(Integer id) {
        return userMapper.selectUserById(id);
    }

    public List<User> getAllUsers() {
        return userMapper.selectAllUsers();
    }

    @Transactional
    public int addUser(User user) {
        return userMapper.insertUser(user);
    }

    @Transactional
    public int modifyUser(User user) {
        return userMapper.updateUser(user);
    }

    @Transactional
    public int removeUser(Integer id) {
        return userMapper.deleteUserById(id);
    }
}

第五步:创建 Controller 层

package com.example.controller;

import com.example.pojo.User;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

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

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User getUser(@PathVariable Integer id) {
        return userService.getUserById(id);
    }

    @GetMapping
    public List<User> getAllUsers() {
        return userService.getAllUsers();
    }

    @PostMapping
    public String addUser(@RequestBody User user) {
        int result = userService.addUser(user);
        return result > 0 ? "添加成功" : "添加失败";
    }

    @PutMapping
    public String modifyUser(@RequestBody User user) {
        int result = userService.modifyUser(user);
        return result > 0 ? "更新成功" : "更新失败";
    }

    @DeleteMapping("/{id}")
    public String removeUser(@PathVariable Integer id) {
        int result = userService.removeUser(id);
        return result > 0 ? "删除成功" : "删除失败";
    }
}

第二章:程序映射器

2.1 MyBatis 核心配置文件

mybatis-config.xml 是 MyBatis 的全局配置文件,包含了 MyBatis 运行所需的所有配置信息。

完整配置结构
<?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>
    <!-- 1. properties:引入外部属性文件 -->
    <properties resource="db.properties"/>
    <!-- 2. settings:全局配置 -->
    <settings>
        <!-- 开启驼峰命名自动映射 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 开启二级缓存 -->
        <setting name="cacheEnabled" value="true"/>
        <!-- 延迟加载 -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 日志实现 -->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
    <!-- 3. typeAliases:类型别名 -->
    <typeAliases>
        <!-- 单个类型别名 -->
        <typeAlias alias="User" type="com.example.pojo.User"/>
        <!-- 批量扫描包,别名为类名(首字母小写) -->
        <package name="com.example.pojo"/>
    </typeAliases>
    <!-- 4. typeHandlers:类型处理器(自定义类型转换) -->
    <typeHandlers>
        <typeHandler handler="com.example.handler.MyTypeHandler"/>
    </typeHandlers>
    <!-- 5. plugins:插件配置 -->
    <plugins>
        <plugin interceptor="com.example.plugin.MyPlugin"/>
    </plugins>
    <!-- 6. environments:环境配置 -->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
        <environment id="production">
            <transactionManager type="MANAGED"/>
            <dataSource type="JNDI">
                <property name="data_source" value="java:comp/env/jdbc/mybatis"/>
            </dataSource>
        </environment>
    </environments>
    <!-- 7. mappers:映射器配置 -->
    <mappers>
        <!-- 方式一:使用 resource 指定 XML 文件 -->
        <mapper resource="com/example/mapper/UserMapper.xml"/>
        <!-- 方式二:使用 class 指定 Mapper 接口(需同名 XML 在同一包下) -->
        <mapper class="com.example.mapper.OrderMapper"/>
        <!-- 方式三:批量扫描包 -->
        <package name="com.example.mapper"/>
    </mappers>
</configuration>
配置项说明
配置项说明常用值
properties引入外部属性文件resource/classpath 指定位置
settings全局行为配置mapUnderscoreToCamelCase, cacheEnabled 等
typeAliases类型别名简化 resultType/parameterType 的全限定名
typeHandlers类型处理器自定义 Java 类型与 JDBC 类型的转换逻辑
plugins插件如分页插件 PageHelper
environments多环境配置开发/测试/生产环境切换
mappers映射器注册注册 SQL 映射文件

2.2 SQL 映射文件

SQL 映射文件是 MyBatis 的核心,用于定义 SQL 语句与 Java 方法之间的映射关系。

基本结构
<?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.mapper.UserMapper">
    <!-- SQL 语句定义 -->
</mapper>
常用 SQL 元素
元素作用常用属性
<select>定义查询语句id, parameterType, resultType, resultMap
<insert>定义插入语句id, parameterType, useGeneratedKeys, keyProperty
<update>定义更新语句id, parameterType
<delete>定义删除语句id, parameterType
<sql>定义可重用的 SQL 片段id
<include>引用 <sql> 定义的片段refid
参数占位符
  • #{}:预编译占位符,使用 PreparedStatement,自动进行类型转换和 SQL 注入防护,适用于数据值绑定。
  • ${}:字符串直接替换,使用 Statement,不进行预编译,存在 SQL 注入风险,仅适用于 SQL 结构部分(如表名、列名、ORDER BY 字段)。
参数传递方式
方式示例说明
单参数#{任意名称}直接使用 #{任意名称} 或 #{param1}/#{arg0}
多参数@Param("name") String name推荐使用 @Param 注解显式命名
对象参数#{属性名}MyBatis 调用 getter 方法获取值
Map 参数#{key}使用 #{key} 引用 Map 中的值
主键回填

方式一:使用 useGeneratedKeys(适用于 MySQL 自增主键)

<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user (username, password, email, age)
    VALUES (#{username}, #{password}, #{email}, #{age})
</insert>

方式二:使用 <selectKey>(适用于 Oracle 序列或复杂主键策略)

<insert id="insertUser" parameterType="User">
    <selectKey keyProperty="id" resultType="int" order="BEFORE">
        SELECT SEQ_USER.NEXTVAL FROM DUAL
    </selectKey>
    INSERT INTO user (id, username, password, email, age)
    VALUES (#{id}, #{username}, #{password}, #{email}, #{age})
</insert>

order="BEFORE" 表示先查后插(如 Oracle 序列),order="AFTER" 表示先插后查(如 MySQL LAST_INSERT_ID())。

可重用 SQL 片段
<!-- 定义可重用的列名片段 -->
<sql id="userColumns">
    id, username, password, email, age
</sql>
<!-- 引用片段 -->
<select id="selectAllUsers" resultType="User">
    SELECT <include refid="userColumns"/> FROM user
</select>

2.3 级联查询

级联查询用于处理多表关联关系,将查询结果映射为包含嵌套对象的复杂数据结构。

关联关系类型
关系类型标签说明示例
一对一<association>一个对象包含另一个对象的引用用户与身份证
一对多<collection>一个对象包含另一个对象的集合订单与订单项
多对多分解为两个一对多通过中间表维护学生与课程
实体类准备
// 用户实体
public class User {
    private Integer id;
    private String username;
    private String email;
    private Role role;              // 一对一关联
    private List<Order> orders;     // 一对多关联
    // getter/setter 省略
}

// 角色实体
public class Role {
    private Integer id;
    private String roleName;
    private String description;
    // getter/setter 省略
}

// 订单实体
public class Order {
    private Integer id;
    private String orderNo;
    private BigDecimal amount;
    private Date createTime;
    // getter/setter 省略
}
数据库表结构
-- 用户表
CREATE TABLE user (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50),
    email VARCHAR(100),
    role_id INT
);
-- 角色表
CREATE TABLE role (
    id INT PRIMARY KEY AUTO_INCREMENT,
    role_name VARCHAR(50),
    description VARCHAR(200)
);
-- 订单表
CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(50),
    amount DECIMAL(10, 2),
    create_time DATETIME,
    user_id INT
);
方式一:嵌套结果映射(推荐)

通过一次 JOIN 查询获取所有数据,性能好,是生产环境推荐的方式。

<resultMap id="userResultMap" type="com.example.pojo.User">
    <id property="id" column="user_id"/>
    <result property="username" column="username"/>
    <result property="email" column="email"/>
    <!-- 一对一:使用 association -->
    <association property="role" javaType="com.example.pojo.Role">
        <id property="id" column="role_id"/>
        <result property="roleName" column="role_name"/>
        <result property="description" column="description"/>
    </association>
    <!-- 一对多:使用 collection -->
    <collection property="orders" ofType="com.example.pojo.Order">
        <id property="id" column="order_id"/>
        <result property="orderNo" column="order_no"/>
        <result property="amount" column="amount"/>
        <result property="createTime" column="create_time"/>
    </collection>
</resultMap>
<select id="selectUserWithDetails" resultMap="userResultMap">
    SELECT
        u.id AS user_id, u.username, u.email,
        r.id AS role_id, r.role_name, r.description,
        o.id AS order_id, o.order_no, o.amount, o.create_time
    FROM user u
    LEFT JOIN role r ON u.role_id = r.id
    LEFT JOIN orders o ON u.id = o.user_id
    WHERE u.id = #{id}
</select>
方式二:嵌套查询(分步查询)

将关联查询拆分为多个独立的 SQL 语句,支持延迟加载,但会产生 N+1 查询问题。

<!-- 主查询:查询用户基本信息 -->
<resultMap id="userResultMap2" type="com.example.pojo.User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="email" column="email"/>
    <!-- 一对一:通过 select 引用其他查询 -->
    <association property="role" column="role_id"
                 select="com.example.mapper.RoleMapper.selectRoleById"/>
    <!-- 一对多:通过 select 引用其他查询 -->
    <collection property="orders" column="id"
                select="com.example.mapper.OrderMapper.selectOrdersByUserId"/>
</resultMap>
<select id="selectUserWithDetails2" resultMap="userResultMap2">
    SELECT id, username, email, role_id FROM user WHERE id = #{id}
</select>
<!-- RoleMapper.xml -->
<mapper namespace="com.example.mapper.RoleMapper">
    <select id="selectRoleById" resultType="com.example.pojo.Role">
        SELECT id, role_name, description FROM role WHERE id = #{id}
    </select>
</mapper>
<!-- OrderMapper.xml -->
<mapper namespace="com.example.mapper.OrderMapper">
    <select id="selectOrdersByUserId" resultType="com.example.pojo.Order">
        SELECT id, order_no, amount, create_time FROM orders WHERE user_id = #{userId}
    </select>
</mapper>
延迟加载配置

嵌套查询会产生 N+1 问题(查询 1 条主记录 + N 条关联记录),可通过延迟加载优化:

<!-- mybatis-config.xml -->
<settings>
    <!-- 开启延迟加载 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 按需加载(false 表示访问任意属性时加载所有关联对象) -->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>
性能对比与选择建议
方式优点缺点适用场景
嵌套结果映射一次查询,性能好SQL 较复杂,列名需别名区分数据量适中、关联层级少
嵌套查询查询逻辑解耦,支持延迟加载N+1 问题,大数据量性能差关联数据访问频率低、层级深

选择建议:简单联查用 JOIN + Map;需要返回嵌套对象用 ResultMap;小数据量且层级深用嵌套查询;微服务/复杂业务用 DTO + JOIN。


第三章:动态 SQL

动态 SQL 是 MyBatis 的"灵魂功能",能根据运行时参数值动态拼接、裁剪、组合 SQL 语句,避免手动拼接的冗余与错误。

3.1 <if> 元素

<if> 是最常用的条件判断标签,基于 test 属性中的 OGNL 表达式进行判断,满足条件时才拼接标签内的 SQL 片段。

基础用法
<select id="selectUserByCondition" resultType="User">
    SELECT id, username, email, age
    FROM user
    WHERE 1=1
    <if test="username != null and username != ''">
        AND username LIKE CONCAT('%', #{username}, '%')
    </if>
    <if test="age != null">
        AND age > #{age}
    </if>
    <if test="email != null and email != ''">
        AND email LIKE CONCAT('%', #{email}, '%')
    </if>
</select>

为什么加 WHERE 1=1? 如果所有 <if> 都不满足,SQL 会变成 SELECT ... FROM user WHERE,产生语法错误。加了 1=1,不满足时就是 WHERE 1=1,语法合法。

对应的 Mapper 接口
List<User> selectUserByCondition(
    @Param("username") String username,
    @Param("age") Integer age,
    @Param("email") String email
);

3.2 <choose>、<when>、<otherwise> 元素

这三个标签组合使用,实现类似 Java switch-case 的互斥分支逻辑:只执行第一个满足条件的 <when> 分支,全部不满足则执行 <otherwise>。

使用场景

适用于"多选一"的查询场景,例如订单查询中"按订单号查"和"按用户 ID 查"只能选一个。

代码示例
<select id="selectOrder" resultType="Order">
    SELECT id, order_no, user_id, amount, create_time
    FROM orders
    <where>
        <choose>
            <!-- 优先级最高:有订单号则按订单号查 -->
            <when test="orderNo != null and orderNo != ''">
                AND order_no = #{orderNo}
            </when>
            <!-- 次优先级:按用户 ID 查 -->
            <when test="userId != null">
                AND user_id = #{userId}
            </when>
            <!-- 默认条件:查最近 7 天的订单 -->
            <otherwise>
                AND create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
            </otherwise>
        </choose>
    </where>
</select>
与 <if> 的区别
特性<if><choose>/<when>
逻辑关系并列,多个可同时生效互斥,只执行第一个满足的
适用场景多条件组合查询多条件单选查询
默认处理无可通过 <otherwise> 提供默认分支

3.3 <trim>、<where>、<set> 元素

<where> 元素

<where> 是智能的 WHERE 子句生成器,自动处理以下问题:

  1. 如果至少有一个条件成立,自动添加 WHERE 关键字。
  2. 自动去除第一个条件前多余的 AND 或 OR。
  3. 如果没有任何条件满足,不生成 WHERE 关键字。
<select id="selectUserByCondition" resultType="User">
    SELECT id, username, email, age
    FROM user
    <where>
        <if test="username != null and username != ''">
            AND username LIKE CONCAT('%', #{username}, '%')
        </if>
        <if test="age != null">
            AND age > #{age}
        </if>
    </where>
</select>

当只传入 age=25 时,生成的 SQL 为 SELECT ... FROM user WHERE age > 25,开头的 AND 被自动去除。

<set> 元素

<set> 用于 UPDATE 语句,自动添加 SET 关键字并去除末尾多余的逗号,支持动态更新非空字段。

<update id="updateUser" parameterType="User">
    UPDATE user
    <set>
        <if test="username != null and username != ''">
            username = #{username},
        </if>
        <if test="email != null and email != ''">
            email = #{email},
        </if>
        <if test="age != null">
            age = #{age},
        </if>
    </set>
    WHERE id = #{id}
</update>

如果只传入 username 和 email,生成的 SQL 为 UPDATE user SET username = ?, email = ? WHERE id = ?,末尾多余的逗号被自动去除。

<trim> 元素

<trim> 是通用的字符串修剪标签,<where> 和 <set> 本质上都是 <trim> 的特例。

属性作用
prefix在内容前添加前缀
suffix在内容后添加后缀
prefixOverrides去除内容开头指定的字符串
suffixOverrides去除内容末尾指定的字符串

用 <trim> 实现 <where> 的效果:

<trim prefix="WHERE" prefixOverrides="AND |OR ">
    <if test="username != null and username != ''">
        AND username LIKE CONCAT('%', #{username}, '%')
    </if>
    <if test="age != null">
        AND age > #{age}
    </if>
</trim>

用 <trim> 实现 <set> 的效果:

<trim prefix="SET" suffixOverrides=",">
    <if test="username != null and username != ''">
        username = #{username},
    </if>
    <if test="email != null and email != ''">
        email = #{email},
    </if>
</trim>

3.4 <foreach> 元素

<foreach> 用于遍历集合(List、Array、Map),生成批量 SQL,常用于 IN 查询、批量插入和批量删除。

核心属性
属性说明
collection要遍历的集合。单参数 List 默认为 list,数组默认为 array,多参数需用 @Param 指定
item当前元素的别名
index当前元素的索引(Map 中为 key)
open遍历开始前添加的字符串
close遍历结束后添加的字符串
separator元素之间的分隔符
场景一:批量删除(IN 查询)
// Mapper 接口
int deleteUsersByIds(@Param("ids") List<Integer> ids);
<delete id="deleteUsersByIds">
    DELETE FROM user WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</delete>

传入 ids = [1, 2, 3],生成的 SQL 为 DELETE FROM user WHERE id IN (1, 2, 3)。

场景二:批量插入
// Mapper 接口
int batchInsertUsers(@Param("users") List<User> users);
<insert id="batchInsertUsers">
    INSERT INTO user (username, password, email, age) VALUES
    <foreach collection="users" item="user" separator=",">
        (#{user.username}, #{user.password}, #{user.email}, #{user.age})
    </foreach>
</insert>

生成的 SQL 为 INSERT INTO user (...) VALUES (?, ?, ?, ?), (?, ?, ?, ?), ...。

场景三:Map 集合遍历
// Mapper 接口
List<User> selectUsersByMap(@Param("params") Map<String, Object> params);
<select id="selectUsersByMap" resultType="User">
    SELECT * FROM user
    <where>
        <foreach collection="params" item="value" index="key" separator="AND">
            ${key} = #{value}
        </foreach>
    </where>
</select>

注意:${key} 是字符串替换,存在 SQL 注入风险,仅适用于可信的列名。


3.5 <bind> 元素

<bind> 用于在上下文中绑定自定义变量,常用于模糊查询的参数预处理,简化 OGNL 表达式并增强数据库兼容性。

使用场景

不同数据库的字符串拼接语法不同:MySQL 用 CONCAT(),Oracle 用 ||。使用 <bind> 可以统一处理。

代码示例
<select id="selectUserByName" resultType="User">
    <!-- 预处理:将参数拼接为模糊查询格式 -->
    <bind name="pattern" value="'%' + username + '%'"/>
    SELECT id, username, email, age
    FROM user
    WHERE username LIKE #{pattern}
</select>

传入 username = "张",pattern 的值为 %张%,最终 SQL 为 WHERE username LIKE '%张%'。

对比传统写法
<!-- 传统写法:依赖数据库函数 -->
WHERE username LIKE CONCAT('%', #{username}, '%')
<!-- bind 写法:与数据库无关 -->
<bind name="pattern" value="'%' + username + '%'"/>
WHERE username LIKE #{pattern}

第四章:综合实战案例

案例:图书管理系统

数据库设计
CREATE DATABASE library;
USE library;

-- 图书分类表
CREATE TABLE category (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    description VARCHAR(200)
);

-- 图书表
CREATE TABLE book (
    id INT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(100) NOT NULL,
    author VARCHAR(50),
    price DECIMAL(10, 2),
    stock INT DEFAULT 0,
    category_id INT,
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (category_id) REFERENCES category(id)
);

-- 读者表
CREATE TABLE reader (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    phone VARCHAR(20),
    email VARCHAR(100)
);

-- 借阅记录表
CREATE TABLE borrow_record (
    id INT PRIMARY KEY AUTO_INCREMENT,
    book_id INT,
    reader_id INT,
    borrow_date DATE,
    return_date DATE,
    status TINYINT DEFAULT 0 COMMENT '0:借阅中 1:已归还',
    FOREIGN KEY (book_id) REFERENCES book(id),
    FOREIGN KEY (reader_id) REFERENCES reader(id)
);

-- 插入测试数据
INSERT INTO category (name, description) VALUES
('计算机科学', '计算机科学与技术相关图书'),
('文学小说', '中外文学小说作品'),
('经济管理', '经济学与管理学相关图书'),
('自然科学', '数理化生等自然科学图书');

INSERT INTO book (title, author, price, stock, category_id) VALUES
('Java核心技术', 'Cay Horstmann', 128.00, 5, 1),
('深入理解Java虚拟机', '周志明', 89.00, 3, 1),
('三体', '刘慈欣', 35.00, 10, 2),
('百年孤独', '马尔克斯', 45.00, 4, 2),
('经济学原理', '曼昆', 68.00, 6, 3),
('算法导论', 'Thomas H. Cormen', 128.00, 2, 1),
('红楼梦', '曹雪芹', 55.00, 8, 2),
('高等数学', '同济大学', 42.00, 7, 4);

INSERT INTO reader (name, phone, email) VALUES
('张三', '13800138001', '[email protected]'),
('李四', '13800138002', '[email protected]'),
('王五', '13800138003', '[email protected]');

INSERT INTO borrow_record (book_id, reader_id, borrow_date, return_date, status) VALUES
(1, 1, '2025-06-01', NULL, 0),
(3, 2, '2025-06-05', '2025-06-20', 1),
(5, 1, '2025-06-10', NULL, 0),
(2, 3, '2025-06-12', NULL, 0);
实体类设计

Category.java(图书分类)

import java.util.List;

public class Category {
    private Integer id;
    private String name;
    private String description;
    // 一对多:一个分类下有多个图书
    private List<Book> books;

    public Category() {}

    public Category(Integer id, String name, String description) {
        this.id = id;
        this.name = name;
        this.description = description;
    }

    // getter 和 setter
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
    public List<Book> getBooks() { return books; }
    public void setBooks(List<Book> books) { this.books = books; }

    @Override
    public String toString() {
        return "Category{id=" + id + ", name='" + name + "', description='" + description + "'}";
    }
}

Book.java(图书)

import java.math.BigDecimal;
import java.util.Date;
import java.util.List;

public class Book {
    private Integer id;
    private String title;
    private String author;
    private BigDecimal price;
    private Integer stock;
    private Integer categoryId;
    private Date createTime;
    // 多对一:每本图书属于一个分类
    private Category category;
    // 一对多:每本图书可被多次借阅
    private List<BorrowRecord> borrowRecords;

    public Book() {}

    public Book(Integer id, String title, String author, BigDecimal price,
                Integer stock, Integer categoryId, Date createTime) {
        this.id = id;
        this.title = title;
        this.author = author;
        this.price = price;
        this.stock = stock;
        this.categoryId = categoryId;
        this.createTime = createTime;
    }

    // getter 和 setter
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getAuthor() { return author; }
    public void setAuthor(String author) { this.author = author; }
    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }
    public Integer getStock() { return stock; }
    public void setStock(Integer stock) { this.stock = stock; }
    public Integer getCategoryId() { return categoryId; }
    public void setCategoryId(Integer categoryId) { this.categoryId = categoryId; }
    public Date getCreateTime() { return createTime; }
    public void setCreateTime(Date createTime) { this.createTime = createTime; }
    public Category getCategory() { return category; }
    public void setCategory(Category category) { this.category = category; }
    public List<BorrowRecord> getBorrowRecords() { return borrowRecords; }
    public void setBorrowRecords(List<BorrowRecord> borrowRecords) { this.borrowRecords = borrowRecords; }

    @Override
    public String toString() {
        return "Book{id=" + id + ", title='" + title + "', author='" + author +
               "', price=" + price + ", stock=" + stock + "}";
    }
}

Reader.java(读者)

import java.util.List;

public class Reader {
    private Integer id;
    private String name;
    private String phone;
    private String email;
    // 一对多:一个读者有多条借阅记录
    private List<BorrowRecord> borrowRecords;

    public Reader() {}

    public Reader(Integer id, String name, String phone, String email) {
        this.id = id;
        this.name = name;
        this.phone = phone;
        this.email = email;
    }

    // getter 和 setter
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getPhone() { return phone; }
    public void setPhone(String phone) { this.phone = phone; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public List<BorrowRecord> getBorrowRecords() { return borrowRecords; }
    public void setBorrowRecords(List<BorrowRecord> borrowRecords) { this.borrowRecords = borrowRecords; }

    @Override
    public String toString() {
        return "Reader{id=" + id + ", name='" + name + "', phone='" + phone + "', email='" + email + "'}";
    }
}

BorrowRecord.java(借阅记录)

import java.util.Date;

public class BorrowRecord {
    private Integer id;
    private Integer bookId;
    private Integer readerId;
    private Date borrowDate;
    private Date returnDate;
    private Integer status;  // 0:借阅中 1:已归还
    // 关联对象
    private Book book;
    private Reader reader;

    public BorrowRecord() {}

    public BorrowRecord(Integer id, Integer bookId, Integer readerId,
                        Date borrowDate, Date returnDate, Integer status) {
        this.id = id;
        this.bookId = bookId;
        this.readerId = readerId;
        this.borrowDate = borrowDate;
        this.returnDate = returnDate;
        this.status = status;
    }

    // getter 和 setter
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public Integer getBookId() { return bookId; }
    public void setBookId(Integer bookId) { this.bookId = bookId; }
    public Integer getReaderId() { return readerId; }
    public void setReaderId(Integer readerId) { this.readerId = readerId; }
    public Date getBorrowDate() { return borrowDate; }
    public void setBorrowDate(Date borrowDate) { this.borrowDate = borrowDate; }
    public Date getReturnDate() { return returnDate; }
    public void setReturnDate(Date returnDate) { this.returnDate = returnDate; }
    public Integer getStatus() { return status; }
    public void setStatus(Integer status) { this.status = status; }
    public Book getBook() { return book; }
    public void setBook(Book book) { this.book = book; }
    public Reader getReader() { return reader; }
    public void setReader(Reader reader) { this.reader = reader; }

    @Override
    public String toString() {
        return "BorrowRecord{id=" + id + ", bookId=" + bookId +
               ", readerId=" + readerId + ", borrowDate=" + borrowDate +
               ", status=" + status + "}";
    }
}
Mapper 接口与 XML 映射文件

BookMapper.java

import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;

public interface BookMapper {
    // 基础 CRUD
    Book selectById(Integer id);
    List<Book> selectAll();
    int insert(Book book);
    int update(Book book);
    int deleteById(Integer id);
    // 级联查询:根据ID查询图书(含分类信息)
    Book selectBookWithCategory(Integer id);
    // 动态SQL:多条件组合查询图书
    List<Book> selectByCondition(@Param("title") String title,
                                 @Param("author") String author,
                                 @Param("categoryId") Integer categoryId,
                                 @Param("minPrice") Double minPrice,
                                 @Param("maxPrice") Double maxPrice);
    // 动态SQL:批量删除图书
    int deleteBatch(@Param("ids") List<Integer> ids);
    // 动态SQL:批量更新库存
    int batchUpdateStock(@Param("bookIds") List<Integer> bookIds,
                         @Param("stock") Integer stock);
    // 动态SQL:使用 bind 实现模糊查询
    List<Book> selectByTitleLike(@Param("title") String title);
    // 综合查询:查询借阅记录(含图书和读者信息)
    List<BorrowRecord> selectBorrowRecords();
}

BookMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.BookMapper">
    <!-- 可重用 SQL 片段 -->
    <sql id="bookColumns">
        b.id, b.title, b.author, b.price, b.stock, b.category_id, b.create_time
    </sql>
    <!-- 基础查询:根据ID -->
    <select id="selectById" resultType="Book">
        SELECT <include refid="bookColumns"/> FROM book b WHERE b.id = #{id}
    </select>
    <!-- 基础查询:全部 -->
    <select id="selectAll" resultType="Book">
        SELECT <include refid="bookColumns"/> FROM book b
    </select>
    <!-- 基础插入 -->
    <insert id="insert" parameterType="Book" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO book (title, author, price, stock, category_id)
        VALUES (#{title}, #{author}, #{price}, #{stock}, #{categoryId})
    </insert>
    <!-- 基础更新 -->
    <update id="update" parameterType="Book">
        UPDATE book
        <set>
            <if test="title != null and title != ''">
                title = #{title},
            </if>
            <if test="author != null and author != ''">
                author = #{author},
            </if>
            <if test="price != null">
                price = #{price},
            </if>
            <if test="stock != null">
                stock = #{stock},
            </if>
            <if test="categoryId != null">
                category_id = #{categoryId},
            </if>
        </set>
        WHERE id = #{id}
    </update>
    <!-- 基础删除 -->
    <delete id="deleteById" parameterType="int">
        DELETE FROM book WHERE id = #{id}
    </delete>
    <!-- 级联查询:根据ID查询图书(含分类信息)- 嵌套结果映射 -->
    <resultMap id="bookWithCategoryMap" type="Book">
        <id property="id" column="book_id"/>
        <result property="title" column="title"/>
        <result property="author" column="author"/>
        <result property="price" column="price"/>
        <result property="stock" column="stock"/>
        <result property="categoryId" column="category_id"/>
        <result property="createTime" column="create_time"/>
        <association property="category" javaType="Category">
            <id property="id" column="cat_id"/>
            <result property="name" column="cat_name"/>
            <result property="description" column="cat_description"/>
        </association>
    </resultMap>
    <select id="selectBookWithCategory" resultMap="bookWithCategoryMap">
        SELECT
            b.id AS book_id, b.title, b.author, b.price, b.stock, b.category_id, b.create_time,
            c.id AS cat_id, c.name AS cat_name, c.description AS cat_description
        FROM book b
        LEFT JOIN category c ON b.category_id = c.id
        WHERE b.id = #{id}
    </select>
    <!-- 动态SQL:多条件组合查询图书 -->
    <select id="selectByCondition" resultType="Book">
        SELECT <include refid="bookColumns"/>
        FROM book b
        <where>
            <if test="title != null and title != ''">
                AND b.title LIKE CONCAT('%', #{title}, '%')
            </if>
            <if test="author != null and author != ''">
                AND b.author LIKE CONCAT('%', #{author}, '%')
            </if>
            <if test="categoryId != null">
                AND b.category_id = #{categoryId}
            </if>
            <if test="minPrice != null">
                AND b.price >= #{minPrice}
            </if>
            <if test="maxPrice != null">
                AND b.price &lt;= #{maxPrice}
            </if>
        </where>
    </select>
    <!-- 动态SQL:批量删除图书(IN 查询) -->
    <delete id="deleteBatch">
        DELETE FROM book WHERE id IN
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>
    <!-- 动态SQL:批量更新库存 -->
    <update id="batchUpdateStock">
        <foreach collection="bookIds" item="bookId" separator=";">
            UPDATE book SET stock = #{stock} WHERE id = #{bookId}
        </foreach>
    </update>
    <!-- 动态SQL:使用 bind 实现模糊查询 -->
    <select id="selectByTitleLike" resultType="Book">
        <bind name="pattern" value="'%' + title + '%'"/>
        SELECT <include refid="bookColumns"/>
        FROM book b
        WHERE b.title LIKE #{pattern}
    </select>
    <!-- 综合查询:借阅记录(含图书和读者信息) -->
    <resultMap id="borrowRecordMap" type="BorrowRecord">
        <id property="id" column="br_id"/>
        <result property="bookId" column="book_id"/>
        <result property="readerId" column="reader_id"/>
        <result property="borrowDate" column="borrow_date"/>
        <result property="returnDate" column="return_date"/>
        <result property="status" column="status"/>
        <!-- 关联图书 -->
        <association property="book" javaType="Book">
            <id property="id" column="book_id"/>
            <result property="title" column="book_title"/>
            <result property="author" column="book_author"/>
            <result property="price" column="book_price"/>
        </association>
        <!-- 关联读者 -->
        <association property="reader" javaType="Reader">
            <id property="id" column="reader_id"/>
            <result property="name" column="reader_name"/>
            <result property="phone" column="reader_phone"/>
        </association>
    </resultMap>
    <select id="selectBorrowRecords" resultMap="borrowRecordMap">
        SELECT
            br.id AS br_id, br.book_id, br.reader_id, br.borrow_date, br.return_date, br.status,
            b.title AS book_title, b.author AS book_author, b.price AS book_price,
            r.name AS reader_name, r.phone AS reader_phone
        FROM borrow_record br
        LEFT JOIN book b ON br.book_id = b.id
        LEFT JOIN reader r ON br.reader_id = r.id
    </select>
</mapper>
Service 层代码
package com.example.service;
import com.example.pojo.Book;
import com.example.pojo.BorrowRecord;
import com.example.mapper.BookMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
public class BookService {

    @Autowired
    private BookMapper bookMapper;

    // 根据ID查询图书
    public Book getById(Integer id) {
        return bookMapper.selectById(id);
    }
    // 查询所有图书
    public List<Book> listAll() {
        return bookMapper.selectAll();
    }
    // 添加图书
    @Transactional
    public int addBook(Book book) {
        return bookMapper.insert(book);
    }
    // 更新图书
    @Transactional
    public int updateBook(Book book) {
        return bookMapper.update(book);
    }
    // 删除图书
    @Transactional
    public int deleteBook(Integer id) {
        return bookMapper.deleteById(id);
    }
    // 级联查询:图书+分类
    public Book getBookWithCategory(Integer id) {
        return bookMapper.selectBookWithCategory(id);
    }
    // 动态SQL:多条件查询
    public List<Book> searchBooks(String title, String author, Integer categoryId,
                                   Double minPrice, Double maxPrice) {
        return bookMapper.selectByCondition(title, author, categoryId, minPrice, maxPrice);
    }
    // 批量删除
    @Transactional
    public int batchDelete(List<Integer> ids) {
        return bookMapper.deleteBatch(ids);
    }
    // 批量更新库存
    @Transactional
    public int batchUpdateStock(List<Integer> bookIds, Integer stock) {
        return bookMapper.batchUpdateStock(bookIds, stock);
    }
    // 模糊查询(使用 bind)
    public List<Book> searchByTitle(String title) {
        return bookMapper.selectByTitleLike(title);
    }
    // 查询所有借阅记录
    public List<BorrowRecord> listBorrowRecords() {
        return bookMapper.selectBorrowRecords();
    }
}
Controller 层代码
package com.example.controller;
import com.example.pojo.Book;
import com.example.pojo.BorrowRecord;
import com.example.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/books")
public class BookController {

    @Autowired
    private BookService bookService;

    // 根据ID查询
    @GetMapping("/{id}")
    public Book getById(@PathVariable Integer id) {
        return bookService.getById(id);
    }
    // 查询所有
    @GetMapping
    public List<Book> listAll() {
        return bookService.listAll();
    }
    // 添加图书
    @PostMapping
    public String addBook(@RequestBody Book book) {
        int result = bookService.addBook(book);
        return result > 0 ? "添加成功" : "添加失败";
    }
    // 更新图书
    @PutMapping
    public String updateBook(@RequestBody Book book) {
        int result = bookService.updateBook(book);
        return result > 0 ? "更新成功" : "更新失败";
    }
    // 删除图书
    @DeleteMapping("/{id}")
    public String deleteBook(@PathVariable Integer id) {
        int result = bookService.deleteBook(id);
        return result > 0 ? "删除成功" : "删除失败";
    }
    // 级联查询:图书+分类
    @GetMapping("/detail/{id}")
    public Book getBookWithCategory(@PathVariable Integer id) {
        return bookService.getBookWithCategory(id);
    }
    // 动态SQL:多条件查询
    @GetMapping("/search")
    public List<Book> searchBooks(
            @RequestParam(required = false) String title,
            @RequestParam(required = false) String author,
            @RequestParam(required = false) Integer categoryId,
            @RequestParam(required = false) Double minPrice,
            @RequestParam(required = false) Double maxPrice) {
        return bookService.searchBooks(title, author, categoryId, minPrice, maxPrice);
    }
    // 批量删除
    @DeleteMapping("/batch")
    public String batchDelete(@RequestBody List<Integer> ids) {
        int result = bookService.batchDelete(ids);
        return result > 0 ? "批量删除成功" : "批量删除失败";
    }
    // 批量更新库存
    @PutMapping("/stock")
    public String batchUpdateStock(@RequestBody List<Integer> bookIds) {
        int result = bookService.batchUpdateStock(bookIds, 0);
        return result > 0 ? "库存更新成功" : "库存更新失败";
    }
    // 模糊查询(bind)
    @GetMapping("/like/{title}")
    public List<Book> searchByTitle(@PathVariable String title) {
        return bookService.searchByTitle(title);
    }
    // 查询借阅记录
    @GetMapping("/borrows")
    public List<BorrowRecord> listBorrowRecords() {
        return bookService.listBorrowRecords();
    }
}
综合练习

练习一:实现读者借阅图书功能

要求:1. 在 ReaderMapper 中新增借阅方法,向 borrow_record 表插入一条借阅记录 2. 同时减少 book 表中对应图书的库存(stock - 1) 3. 使用 @Transactional 保证事务一致性

练习二:实现还书功能

要求:1. 更新 borrow_record 表中对应记录的 return_date 和 status 2. 同时增加 book 表中对应图书的库存(stock + 1)

练习三:统计每个分类下的图书数量

要求:1. 使用 GROUP BY 统计每个分类的图书数量 2. 返回包含分类信息和图书数量的 DTO 对象

练习四:动态 SQL 综合查询

要求:1. 实现一个综合查询接口,支持按书名、作者、分类、价格范围、借阅状态等多个条件动态组合查询 2. 使用 <choose>/<when> 实现默认排序策略(有价格条件时按价格排序,否则按创建时间排序)


第五章:练习与答案

一、基础题(5 道)

第 1 题:MyBatis 环境搭建

题目:请写出完整的 mybatis-config.xml 核心配置文件,要求配置 MySQL 数据源(数据库名 mybatis_test,用户名 root,密码 123456),并注册 com.example.mapper 包下的所有 Mapper 接口。

参考答案:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis_test?serverTimezone=Asia/Shanghai"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <package name="com.example.mapper"/>
    </mappers>
</configuration>

第 2 题:基本 CRUD 实现

题目:编写 UserMapper 接口的 selectById 方法和对应的 UserMapper.xml 映射文件,实现根据用户 ID 查询用户信息。

参考答案:

// UserMapper.java
public interface UserMapper {
    User selectById(Integer id);
}
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
    <select id="selectById" resultType="User" parameterType="int">
        SELECT id, username, password, email, age
        FROM user WHERE id = #{id}
    </select>
</mapper>

第 3 题:核心配置文件属性

题目:在 mybatis-config.xml 中,如何开启驼峰命名自动映射?如何开启二级缓存?请写出对应的 settings 配置。

参考答案:

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

第 4 题:主键回填

题目:在插入用户时,需要自动获取数据库自增主键并回填到 User 对象的 id 属性中,请写出对应的 insert 语句配置。

参考答案:

<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user (username, password, email, age)
    VALUES (#{username}, #{password}, #{email}, #{age})
</insert>

第 5 题:类型别名配置

题目:如何配置类型别名,使得 com.example.pojo.User 可以使用 “User” 作为别名?请写出两种配置方式。

参考答案:

方式一:单个配置

<typeAliases>
    <typeAlias alias="User" type="com.example.pojo.User"/>
</typeAliases>

方式二:批量扫描(别名自动为类名首字母小写)

<typeAliases>
    <package name="com.example.pojo"/>
</typeAliases>

二、进阶题(5 道)

第 6 题:级联查询 - 一对多

题目:设计一个 ResultMap,实现查询订单时同时获取订单下的所有订单项(Order 包含 List),写出完整的 resultMap 配置和对应的 SQL 语句。

参考答案:

<collection property="orderItems" ofType="OrderItem">
    <id property="id" column="item_id"/>
    <result property="productId" column="product_id"/>
    <result property="quantity" column="quantity"/>
    <result property="price" column="item_price"/>
</collection>
SELECT o.id AS order_id, o.order_no, o.amount, o.create_time,
       oi.id AS item_id, oi.product_id, oi.quantity, oi.price AS item_price
FROM orders o
LEFT JOIN order_item oi ON o.id = oi.order_id

第六章:MyBatis-Plus 增强开发

MyBatis-Plus(简称 MP)是在 MyBatis 基础上的增强工具,遵循"只做增强不做改变"的设计理念。学习本章前,请确保已掌握前五章 MyBatis 的 CRUD、映射文件与动态 SQL。

6.1 什么是 MyBatis-Plus

MyBatis-Plus 是国内开源的 MyBatis 增强框架,它在不改变 MyBatis 原有用法的前提下,内置了通用 CRUD、条件构造器、分页插件、代码生成器等大量功能,目标是"节省开发时间、提高效率"。官方口号:只增强,不改变(Better ORM)。

与 MyBatis、MyBatis-Spring-Boot-Starter 的对比
维度原生 MyBatisMyBatis-Spring-Boot-StarterMyBatis-Plus
单表 CRUD需手写 SQL 与映射需手写 SQL 与映射继承 BaseMapper 自动拥有
条件查询手写动态 SQL手写动态 SQL条件构造器链式 API
分页需 PageHelper 等插件需 PageHelper 等插件内置分页插件
逻辑删除手写 UPDATE 语句手写 UPDATE 语句@TableLogic 一行配置
乐观锁手写 version 判断手写 version 判断@Version + 插件
代码生成无无内置代码生成器
学习成本中中低(在 MyBatis 之上)
复杂多表联查完全可控完全可控完全可控(仍写 XML)

结论:MyBatis-Plus 并不是替代 MyBatis,而是"加了一层脚手架"。单表操作交给 MP,复杂多表 SQL 仍然可以用前五章学过的 XML 与 resultMap 手写,二者无缝共存。

核心优势
  • 无侵入:引入 MP 后原有 MyBatis 工程无需改动即可运行。
  • 通用 CRUD:Mapper 继承 BaseMapper<T> 即获得多个通用方法。
  • 条件构造器:用 Java 链式 API 代替 XML 动态 SQL,类型安全、IDE 可提示。
  • 插件丰富:分页、乐观锁、防全表更新删除、性能分析等一键开启。

6.2 环境搭建(Spring Boot)

MyBatis-Plus 官方推荐使用 mybatis-plus-boot-starter。注意版本需与 Spring Boot 匹配:Spring Boot 3.x 需使用 mybatis-plus-spring-boot3-starter。

Maven 依赖
<dependencies>
    <!-- MyBatis-Plus Starter(内含 MyBatis,无需再单独引入 mybatis) -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.5</version>
    </dependency>
    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- Lombok(可选,简化实体类) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
application.yml 配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mybatis_demo?serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml   # XML 映射文件位置
  type-aliases-package: com.example.pojo          # 实体类别名包
  configuration:
    map-underscore-to-camel-case: true            # 开启驼峰映射
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 打印 SQL
  global-config:
    db-config:
      id-type: auto                               # 主键策略:自增
      logic-delete-field: deleted                 # 全局逻辑删除字段
      logic-delete-value: 1                       # 已删除标记值
      logic-not-delete-value: 0                   # 未删除标记值
启动类扫描 Mapper
@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

6.3 快速入门:BaseMapper 通用 CRUD

MP 最核心的能力:Mapper 接口只需继承 BaseMapper<T>,无需编写任何 SQL 即可拥有单表增删改查。

实体类
package com.example.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("user")           // 对应数据库表名
public class User {
    @TableId(type = IdType.AUTO)   // 主键,自增
    private Integer id;
    private String username;
    private String password;
    private String email;
    private Integer age;
}
Mapper 接口
package com.example.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.pojo.User;

// 只需继承 BaseMapper,即可获得全部通用 CRUD 方法
public interface UserMapper extends BaseMapper<User> {
}
通用方法演示
@SpringBootTest
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void testInsert() {
        User user = new User();
        user.setUsername("赵六");
        user.setPassword("123456");
        user.setEmail("[email protected]");
        user.setAge(26);
        int rows = userMapper.insert(user);
        System.out.println("影响行数:" + rows + ",回填主键:" + user.getId());
    }

    @Test
    void testSelect() {
        User user = userMapper.selectById(1);            // 按主键查
        System.out.println(user);
    }

    @Test
    void testUpdate() {
        User user = new User();
        user.setId(1);
        user.setAge(99);                                 // 只更新非 null 字段
        userMapper.updateById(user);
    }

    @Test
    void testDelete() {
        userMapper.deleteById(2);                        // 按主键删除
        userMapper.deleteBatchIds(Arrays.asList(3, 4));  // 批量删除
    }
}
BaseMapper 常用方法速查
方法说明
insert(T)插入一条记录
deleteById(id) / deleteBatchIds(list)按主键删除 / 批量删除
updateById(T)按主键更新(只更新非 null 字段)
selectById(id)按主键查询
selectBatchIds(list)批量主键查询
selectList(Wrapper)条件查询列表
selectOne(Wrapper)条件查询单条
selectCount(Wrapper)条件统计数量
selectPage(page, Wrapper)分页查询

6.4 实体类常用注解

注解作用说明
@TableName指定表名类名与表名不一致时使用
@TableId标记主键type 指定主键策略(AUTO/ASSIGN_ID/ASSIGN_UUID/INPUT)
@TableField标记普通字段exist=false 表示该属性不属于表字段
@TableLogic逻辑删除标记查询自动追加未删除条件,删除转 UPDATE
@Version乐观锁版本字段更新时自动进行版本号比对
@TableField(fill=...)字段自动填充配合元对象处理器自动填充创建/更新时间
主键策略(IdType)
策略说明
AUTO数据库自增(MySQL 常用)
ASSIGN_ID雪花算法生成分布式 ID(默认)
ASSIGN_UUID生成 32 位 UUID
INPUT手动输入主键
逻辑删除示例
@Data
@TableName("user")
public class User {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String username;
    private Integer age;

    @TableLogic                 // 逻辑删除标志字段
    private Integer deleted;    // 0 未删除,1 已删除
}

配置 @TableLogic 后:

  • deleteById(1) 实际执行 UPDATE user SET deleted=1 WHERE id=1;
  • 所有查询自动追加 AND deleted=0,无需手写。

6.5 条件构造器 Wrapper

条件构造器用 Java 链式代码替代 XML 动态 SQL。推荐优先使用 Lambda 版本(LambdaQueryWrapper),通过方法引用避免字段名字符串写错。

QueryWrapper 基本用法
// 查询 age >= 25 且 username 包含 "张",按 age 倒序
List<User> list = userMapper.selectList(
    new QueryWrapper<User>()
        .ge("age", 25)
        .like("username", "张")
        .orderByDesc("age")
);
LambdaQueryWrapper(推荐)
// 使用实体方法的引用,编译期即可检查字段是否存在
List<User> list = userMapper.selectList(
    new LambdaQueryWrapper<User>()
        .ge(User::getAge, 25)
        .like(User::getUsername, "张")
        .eq(User::getEmail, "[email protected]")
        .orderByDesc(User::getAge)
);
常用条件方法
方法SQL 片段方法SQL 片段
eq=ne<>
gt / ge> / >=lt / le< / <=
likeLIKE '%值%'likeLeftLIKE '%值'
betweenBETWEENinIN (...)
isNullIS NULLorderByDescORDER BY ... DESC
and / or拼接括号逻辑nested嵌套条件
条件动态拼接(对应 MyBatis 的 if)

Wrapper 的多数方法可传入第一个 boolean condition 参数,条件为 true 时才拼接,等价于 MyBatis 的 <if>:

String name = "张";        // 可能为 null
Integer minAge = null;

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
    .like(StringUtils.isNotBlank(name), User::getUsername, name)   // name 非空才拼
    .ge(minAge != null, User::getAge, minAge);                     // minAge 非空才拼

List<User> list = userMapper.selectList(wrapper);
UpdateWrapper 更新示例
// 将所有 age 大于 30 的用户 email 置空
userMapper.update(null,
    new UpdateWrapper<User>()
        .gt("age", 30)
        .set("email", null)
);

6.6 分页插件

MP 内置分页功能,只需注册一个拦截器 Bean,即可使用 Page<T> 进行物理分页。

配置分页拦截器
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件,指定数据库类型
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
分页查询
@Test
void testPage() {
    // 当前页码,每页条数
    Page<User> page = new Page<>(1, 10);
    Page<User> result = userMapper.selectPage(page,
        new LambdaQueryWrapper<User>().ge(User::getAge, 18)
            .orderByDesc(User::getId));

    System.out.println("当前页数据:" + result.getRecords());
    System.out.println("总记录数:" + result.getTotal());
    System.out.println("总页数:" + result.getPages());
    System.out.println("当前页:" + result.getCurrent());
}

若不需要查询总条数(性能优化),可 page.setSearchCount(false);分页插件底层会自动改写 SQL 追加 LIMIT,并生成 COUNT 语句。


6.7 常用增强插件与自动填充

乐观锁

在实体字段上加 @Version,并注册 OptimisticLockerInnerInterceptor:

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 乐观锁
    return interceptor;
}

实体字段:

@Version
private Integer version;

更新时 MP 会自动执行 UPDATE ... SET version=version+1 WHERE id=? AND version=?,避免并发覆盖。

字段自动填充

实现 MetaObjectHandler,自动填充创建时间、更新时间:

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

实体字段声明填充策略:

@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

6.8 Service 层:IService 与 ServiceImpl

MP 提供了 Service 层的通用接口 IService<T> 与实现类 ServiceImpl,包含 save/updateById/removeById/list/getById 等业务级方法。

// Service 接口
public interface UserService extends IService<User> {
}

// Service 实现
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
@Autowired
private UserService userService;

@Test
void testService() {
    User u = new User();
    u.setUsername("孙七");
    u.setAge(22);
    userService.save(u);                                // 新增
    List<User> all = userService.list();                // 查询全部
    boolean ok = userService.lambdaQuery()              // 链式业务查询
        .ge(User::getAge, 20).list().size() > 0;
    userService.removeById(1);                          // 删除
}

6.9 代码生成器

MP 官方提供 AutoGenerator / FastAutoGenerator,可根据数据库表一键生成 entity、mapper、service、controller 与 XML,大幅减少样板代码。

<!-- 代码生成器依赖 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.5</version>
</dependency>
<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.3</version>
</dependency>
public class CodeGenerator {
    public static void main(String[] args) {
        FastAutoGenerator.create(
                "jdbc:mysql://localhost:3306/mybatis_demo?serverTimezone=Asia/Shanghai",
                "root", "root")
            .globalConfig(builder -> builder
                .author("teacher")
                .outputDir("D:/code/src/main/java"))
            .packageConfig(builder -> builder
                .parent("com.example"))
            .strategyConfig(builder -> builder
                .addInclude("user", "book")       // 需要生成的表
                .entityBuilder().enableLombok())
            .templateEngine(new VelocityTemplateEngine())
            .execute();
    }
}

6.10 综合案例:用户模块(MP 版)

将第五章图书管理系统的用户模块用 MP 重写,体验开发效率的提升。

需求
  1. 新增用户,自动填充创建时间;
  2. 分页查询:按年龄区间 + 姓名模糊,按创建时间倒序;
  3. 逻辑删除用户;
  4. 修改用户时启用乐观锁。
实体类
@Data
@TableName("user")
public class User {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String username;
    private String email;
    private Integer age;

    @Version
    private Integer version;                 // 乐观锁

    @TableLogic
    private Integer deleted;                 // 逻辑删除

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}
Controller
@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping
    public String add(@RequestBody User user) {
        return userService.save(user) ? "添加成功" : "添加失败";
    }

    // 分页 + 条件查询
    @GetMapping("/page")
    public Page<User> page(@RequestParam(defaultValue = "1") long current,
                           @RequestParam(defaultValue = "10") long size,
                           @RequestParam(required = false) String name,
                           @RequestParam(required = false) Integer minAge,
                           @RequestParam(required = false) Integer maxAge) {
        return userService.page(new Page<>(current, size),
            new LambdaQueryWrapper<User>()
                .like(StringUtils.isNotBlank(name), User::getUsername, name)
                .ge(minAge != null, User::getAge, minAge)
                .le(maxAge != null, User::getAge, maxAge)
                .orderByDesc(User::getCreateTime));
    }

    @PutMapping
    public String update(@RequestBody User user) {
        return userService.updateById(user) ? "更新成功" : "更新失败(可能版本冲突)";
    }

    @DeleteMapping("/{id}")
    public String remove(@PathVariable Integer id) {
        return userService.removeById(id) ? "删除成功" : "删除失败";
    }
}

对比第五章:同样的需求在原生 MyBatis 中需要编写 Mapper 接口、XML 映射、动态 SQL;在 MP 中几乎全部由 IService 与 LambdaQueryWrapper 完成,仅在需要多表联查时才回退到手写 XML。


6.11 练习与答案

第 1 题:MP 环境搭建

题目:在 Spring Boot 项目中引入 MyBatis-Plus 需要添加哪个 Starter 依赖?如果项目使用的是 Spring Boot 3.x,应注意什么?

参考答案:

需要添加 com.baomidou:mybatis-plus-boot-starter。Spring Boot 3.x 需改用 mybatis-plus-spring-boot3-starter,并确保 MyBatis-Plus 版本为 3.5.3+ 以适配 Jakarta 命名空间。

第 2 题:通用 CRUD

题目:一个 Mapper 接口如何在不写任何 SQL 的情况下获得单表增删改查能力?写出接口定义。

参考答案:

public interface UserMapper extends BaseMapper<User> {
}

继承 BaseMapper<User> 后自动获得 insert / deleteById / updateById / selectById / selectList / selectPage 等通用方法。

第 3 题:条件构造器

题目:用 LambdaQueryWrapper 查询年龄大于等于 30、邮箱包含 “@example.com”、按 id 升序的所有用户。

参考答案:

List<User> list = userMapper.selectList(
    new LambdaQueryWrapper<User>()
        .ge(User::getAge, 30)
        .like(User::getEmail, "@example.com")
        .orderByAsc(User::getId)
);

第 4 题:分页插件

题目:MP 的分页功能需要额外引入第三方 PageHelper 吗?如何开启?

参考答案:

不需要。MP 内置分页,只需注册拦截器:

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
}

再使用 Page<User> 调用 selectPage 即可。

第 5 题:逻辑删除

题目:如何配置逻辑删除?配置后 deleteById 与查询语句会发生什么变化?

参考答案:

在实体删除标志字段上加 @TableLogic,并在配置文件设置 logic-delete-field / logic-delete-value / logic-not-delete-value。配置后 deleteById(1) 实际执行 UPDATE user SET deleted=1 WHERE id=1;所有查询自动追加 AND deleted=0,逻辑删除的记录不会被查出。


附录 B:MyBatis-Plus 快速上手清单

步骤关键代码/配置
1. 引入依赖mybatis-plus-boot-starter(Spring Boot 3.x 用 spring-boot3 版)
2. 数据源配置spring.datasource.*
3. MP 配置mapper-locations、type-aliases-package、logic-delete-*
4. Mapperinterface XxxMapper extends BaseMapper<Xxx>
5. 实体注解@TableName/@TableId/@TableField/@TableLogic/@Version
6. 分页插件注册 MybatisPlusInterceptor + PaginationInnerInterceptor
7. 乐观锁/自动填充OptimisticLockerInnerInterceptor + MetaObjectHandler
8. Serviceinterface XxxService extends IService<Xxx>,ServiceImpl 实现
9. 条件查询LambdaQueryWrapper / UpdateWrapper
10. 代码生成mybatis-plus-generator + 模板引擎

学习建议:先用 MP 把单表 CRUD、分页、条件构造器跑通,再逐步引入逻辑删除、乐观锁、自动填充与代码生成器;遇到多表复杂查询时,回退到前五章的 XML + resultMap 写法,二者可在同一工程中共存。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_40229790/article/details/166900009

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--