【MyBatis】XML方式实现CRUD

加入 Maven 依赖:MySQL 驱动 + MyBatis

在 Spring Boot 项目中,使用 MyBatis 操作 MySQL 数据库,首先需要在 pom.xml 文件中引入相关依赖。以下是核心依赖配置:

xml 复制代码
<!-- MySQL 驱动 -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- MyBatis Spring Boot Starter -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>

<!-- Spring Boot Test(单元测试需要) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

依赖说明:

  • mysql-connector-j :MySQL 官方 JDBC 驱动,scope 设为 runtime,仅在运行时需要。
  • mybatis-spring-boot-starter :MyBatis 官方的 Spring Boot 起步依赖,自动配置 SqlSessionFactorySqlSessionTemplate 等核心组件。
  • spring-boot-starter-test:Spring Boot 测试框架,包含 JUnit 5、Mockito 等,用于编写单元测试。

配置数据库连接

yml 配置

application.yml 中配置 MySQL 数据源连接信息。XML 方式与注解方式的最大区别在于 必须配置 mapper-locations,指定 XML 映射文件的位置:

yml 复制代码
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
    username: 数据库账号
    password: 数据库密码
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
  # 配置 mybatis xml 的文件路径,在 resources/mapper 创建所有表的 xml 文件
  mapper-locations: classpath:mapper/**Mapper.xml
  configuration: # 配置打印 MyBatis日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true #配置驼峰自动转换

全局驼峰命名自动转换

准备数据库表

在 MySQL 中执行以下 SQL,创建测试数据库和用户表:

sql 复制代码
-- 创建数据库
DROP DATABASE IF EXISTS mybatis_test;
CREATE DATABASE mybatis_test DEFAULT CHARACTER SET utf8mb4;

-- 使用数据库
USE mybatis_test;

-- =============================================
-- 表名: user_info
-- 描述: 用户基础信息表,存储系统用户的核心数据
-- 注意事项:
--   1. 密码应存储加密后的密文,VARCHAR(50)根据加密算法调整
--   2. username 建议添加唯一索引,保证用户唯一
--   3. 性别采用 TINYINT 存储,节省空间且含义清晰
--   4. 默认字符集 utf8mb4,兼容emoji等特殊字符
--   5. 自动维护 create_time 和 update_time,减少应用层负担
-- =============================================
DROP TABLE IF EXISTS user_info;
CREATE TABLE `user_info` (
    `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID,自增整数,作为用户唯一物理标识',
    `username` VARCHAR(50) NOT NULL COMMENT '用户名,登录凭证,实际使用时应添加唯一索引',
    `password` VARCHAR(50) NOT NULL COMMENT '加密后的密码,根据加密算法调整长度(如使用bcrypt建议VARCHAR(255))',
    `age` INT DEFAULT NULL COMMENT '年龄,允许空值,便于隐私保护或未知场景',
    `gender` TINYINT DEFAULT '0' COMMENT '性别:1-男,2-女,0-未知。使用TINYINT节省存储',
    `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号,格式如13800138000,允许空值',
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间,由数据库自动填充',
    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间,每次修改行时自动更新',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';
-- 添加测试数据
INSERT INTO user_info (username, `password`, age, gender, phone) VALUES
('admin', 'admin', 18, 1, '18612340001'),
('zhangsan', 'zhangsan', 20, 1, '18612340002'),
('lisi', 'lisi', 22, 2, '18612340003');

创建实体类 UserInfo

java 复制代码
@Data
public class UserInfo {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private Integer gender;
    private String phone;
    private Date createTime;
    private Date updateTime;
}

XML 方式核心:三层组件

XML 方式实现 CRUD 需要三层组件协同工作:

层级 文件 位置 作用
Mapper 接口 UserInfoMapperXML.java src/main/java/.../mapper/ 定义方法签名,与 XML 绑定
Mapper XML UserInfoMapperXML.xml src/main/resources/mapper/ 编写 SQL 语句
实体类 UserInfo.java src/main/java/.../entity/ 数据载体(已创建)

绑定规则:

  1. Mapper 接口的 全限定类名 = XML 的 namespace
  2. Mapper 接口的 方法名 = XML 中 <select>/<insert>/<update>/<delete>id
  3. 接口方法 参数类型 = XML 中 SQL 标签的 parameterType(可选,MyBatis 可自动推断)
  4. 接口方法 返回类型 = XML 中 SQL 标签的 resultType / resultMap

创建 Mapper 接口

src/main/java/.../mapper/ 路径下创建 UserInfoMapperXML.java

java 复制代码
package com.example.mapper;

import com.example.entity.UserInfo;
import org.apache.ibatis.annotations.Param;
import java.util.List;

/**
 * UserInfo 的 Mapper 接口(XML 方式)
 * ⚠️ 接口方法与 XML 中的 id 必须一一对应
 */
public interface UserInfoMapperXML {

    // ========== 查询 ==========
    
    /** 1. 按主键查询 */
    UserInfo selectById(Integer id);

    /** 2. 查询全部 */
    List<UserInfo> selectAll();

    /** 3. 多条件查询(按用户名和年龄) */
    List<UserInfo> selectByCondition(
            @Param("username") String username,
            @Param("minAge") Integer minAge);

    /** 4. 模糊查询(LIKE) */
    List<UserInfo> selectByKeyword(@Param("keyword") String keyword);

    /** 5. 排序查询 */
    List<UserInfo> selectAllOrdered(
            @Param("orderColumn") String orderColumn,
            @Param("orderDirection") String orderDirection);

    /** 6. 分页查询 */
    List<UserInfo> selectByPage(
            @Param("offset") Integer offset,
            @Param("limit") Integer limit);

    /** 7. 聚合查询 - 统计总数 */
    Integer countAll();

    /** 8. IN 子句批量查询 */
    List<UserInfo> selectByIds(@Param("idList") List<Integer> idList);

    /** 9. 返回单个字段 */
    List<String> selectAllUsernames();

    /** 10. 返回 Map */
    List<Map<String, Object>> selectAllAsMapList();

    // ========== 插入 ==========

    /** 11. 插入完整实体对象 */
    Integer insertUser(UserInfo userInfo);

    /** 12. 插入并回填自增主键 */
    Integer insertUserAndReturnId(UserInfo userInfo);

    /** 13. 逐字段插入 */
    Integer insertUserByFields(
            @Param("username") String username,
            @Param("password") String password,
            @Param("age") Integer age,
            @Param("gender") Integer gender,
            @Param("phone") String phone);

    /** 14. 批量插入 */
    Integer batchInsert(@Param("userList") List<UserInfo> userList);

    // ========== 更新 ==========

    /** 15. 按主键更新全部字段 */
    Integer updateById(UserInfo userInfo);

    /** 16. 按主键选择性更新 */
    Integer updatePassword(
            @Param("id") Integer id,
            @Param("newPassword") String newPassword);

    /** 17. 条件更新 */
    Integer updateAgeByCondition(
            @Param("username") String username,
            @Param("password") String password,
            @Param("newAge") Integer newAge);

    /** 18. CASE WHEN 批量更新 */
    Integer batchUpdateAge(@Param("userList") List<UserInfo> userList);

    // ========== 删除 ==========

    /** 19. 按主键删除 */
    Integer deleteById(Integer id);

    /** 20. 条件删除(按用户名) */
    Integer deleteByUsername(@Param("username") String username);

    /** 21. 多条件删除(按年龄范围) */
    Integer deleteByAgeRange(
            @Param("minAge") Integer minAge,
            @Param("maxAge") Integer maxAge);

    /** 22. 批量删除 */
    Integer deleteByIds(@Param("idList") List<Integer> idList);

    /** 23. 清空表 */
    Integer deleteAll();
}

对比注解方式的关键区别:

  • XML 方式:接口只有方法声明,不需要 @Select@Insert 等注解,SQL 写在 XML 文件中。
  • 注解方式:SQL 直接写在接口方法的注解上。

创建 Mapper XML 文件

src/main/resources/mapper/ 路径下创建 UserInfoMapperXML.xml

XML 基础结构

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">

<!-- namespace = Mapper 接口的全限定类名 -->
<mapper namespace="com.example.mapper.UserInfoMapperXML">

    <!-- SQL 语句将写在这里 -->

</mapper>

⚠️ 关键约束namespace 必须与 Mapper 接口的全限定类名 完全一致,否则启动时报 BindingException

标签速查表

XML 标签 对应 SQL 对应接口注解 用途
<select> SELECT @Select 查询操作
<insert> INSERT @Insert 插入操作
<update> UPDATE @Update 更新操作
<delete> DELETE @Delete 删除操作
<resultMap> --- @Results 自定义结果映射
<sql> --- --- SQL 片段复用
<include> --- --- 引用 SQL 片段

XML 属性速查

属性 说明 示例
id 方法名,与接口方法一一对应 id="selectById"
resultType 返回值类型(全限定类名或别名) resultType="com.example.entity.UserInfo"
resultMap 自定义结果映射 ID resultMap="BaseResultMap"
parameterType 参数类型(可选,MyBatis 自动推断) parameterType="Integer"
useGeneratedKeys 是否使用 JDBC 自增主键 useGeneratedKeys="true"
keyProperty 自增主键回填到哪个属性 keyProperty="id"
keyColumn 数据库主键列名 keyColumn="id"
flushCache 执行后是否清空缓存 flushCache="true"
statementType Statement 类型 statementType="PREPARED"

查询 SQL:<select> 标签

1. 按主键查询

xml 复制代码
<!--
    id        = 接口方法名
    resultType = 返回值类型(全限定类名)
    parameterType = 参数类型(可选,这里不写 MyBatis 也会自动推断)
-->
<select id="selectById" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info WHERE id = #{id}
</select>

调用示例:

java 复制代码
UserInfo user = userInfoMapperXML.selectById(1);
// 执行 SQL:SELECT * FROM user_info WHERE id = 1
// 结果:UserInfo{id=1, username="admin", age=18, ...}

2. 查询全部

xml 复制代码
<select id="selectAll" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info
</select>

调用示例:

java 复制代码
List<UserInfo> list = userInfoMapperXML.selectAll();
// SQL:SELECT * FROM user_info
// 结果:[UserInfo{id=1,...}, UserInfo{id=2,...}, UserInfo{id=3,...}]

3. 多条件查询(@Param 多参数)

xml 复制代码
<!--
    @Param 指定的名称直接在 #{} 中使用
    parameterType 不需要写,MyBatis 自动处理多参数
-->
<select id="selectByCondition" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info
    WHERE username LIKE CONCAT('%', #{username}, '%')
    AND age >= #{minAge}
</select>

调用示例:

java 复制代码
List<UserInfo> users = userInfoMapperXML.selectByCondition("zhang", 18);
// SQL:SELECT * FROM user_info WHERE username LIKE '%zhang%' AND age >= 18
// 结果:[UserInfo{username="zhangsan", age=20, ...}]

4. LIKE 模糊查询

xml 复制代码
<!-- ✅ 安全写法:用 CONCAT + #{}  防止 SQL 注入 -->
<select id="selectByKeyword" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info WHERE username LIKE CONCAT('%', #{keyword}, '%')
</select>

<!-- ✅ 前缀匹配 -->
<select id="selectByPhonePrefix" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info WHERE phone LIKE CONCAT(#{prefix}, '%')
</select>

调用示例:

java 复制代码
List<UserInfo> users = userInfoMapperXML.selectByKeyword("admin");
// SQL:WHERE username LIKE '%admin%' → 匹配 "admin"

// ⚠️ 不推荐写法(有 SQL 注入风险):
// SELECT * FROM user_info WHERE username LIKE '%${keyword}%'

5. 排序查询(ORDER BY)

xml 复制代码
<!-- ⚠️ ORDER BY 的列名和排序方向必须使用 ${},不能用 #{} -->
<select id="selectAllOrdered" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info ORDER BY ${orderColumn} ${orderDirection}
</select>

调用示例:

java 复制代码
List<UserInfo> users = userInfoMapperXML.selectAllOrdered("age", "DESC");
// SQL:SELECT * FROM user_info ORDER BY age DESC

List<UserInfo> users2 = userInfoMapperXML.selectAllOrdered("create_time", "ASC");
// SQL:SELECT * FROM user_info ORDER BY create_time ASC

⚠️ 必须使用 ${} ,因为 JDBC 的 ? 占位符不能替代 SQL 关键字(列名、排序方向)。

必须在业务层对 orderColumn 做白名单校验,防止 SQL 注入:

java 复制代码
private static final Set<String> ALLOWED_COLUMNS = Set.of("id", "username", "age", "gender", "create_time");
if (!ALLOWED_COLUMNS.contains(orderColumn)) {
    throw new IllegalArgumentException("非法的排序列: " + orderColumn);
}

6. 分页查询(LIMIT)

xml 复制代码
<select id="selectByPage" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info LIMIT #{offset}, #{limit}
</select>

调用示例:

java 复制代码
// 第 1 页,每页 2 条
List<UserInfo> page1 = userInfoMapperXML.selectByPage(0, 2);
// SQL:SELECT * FROM user_info LIMIT 0, 2 → [admin, zhangsan]

// 第 2 页
List<UserInfo> page2 = userInfoMapperXML.selectByPage(2, 2);
// SQL:SELECT * FROM user_info LIMIT 2, 2 → [lisi]

7. 聚合查询(COUNT / MAX / MIN / AVG)

xml 复制代码
<!-- 统计总数 -->
<select id="countAll" resultType="java.lang.Integer">
    SELECT COUNT(*) FROM user_info
</select>

<!-- 按性别统计 -->
<select id="countByGender" resultType="Integer">
    SELECT COUNT(*) FROM user_info WHERE gender = #{gender}
</select>

<!-- 最大年龄 -->
<select id="selectMaxAge" resultType="Integer">
    SELECT MAX(age) FROM user_info
</select>

<!-- 最小年龄 -->
<select id="selectMinAge" resultType="Integer">
    SELECT MIN(age) FROM user_info
</select>

调用示例:

java 复制代码
Integer total = userInfoMapperXML.countAll();           // → 3
Integer maleCount = userInfoMapperXML.countByGender(1); // → 2
Integer maxAge = userInfoMapperXML.selectMaxAge();      // → 22

8. IN 子句批量查询(<foreach>

xml 复制代码
<!--
    collection = 集合参数名(对应 @Param 的值或默认 param1)
    item       = 遍历时的单个元素名称
    open       = 循环开始前拼接的字符
    separator  = 分隔符
    close      = 循环结束后拼接的字符
-->
<select id="selectByIds" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info
    WHERE id IN
    <foreach item="id" collection="idList" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

调用示例:

java 复制代码
List<Integer> ids = Arrays.asList(1, 3);
List<UserInfo> users = userInfoMapperXML.selectByIds(ids);
// SQL:SELECT * FROM user_info WHERE id IN (1, 3)
// 结果:[UserInfo{id=1}, UserInfo{id=3}]

XML 方式支持 <foreach> 不需要 <script> 包裹,这是与注解方式的重要区别。

9. 返回单个字段值

xml 复制代码
<!-- resultType 写字段对应的 Java 类型 -->
<select id="selectAllUsernames" resultType="java.lang.String">
    SELECT username FROM user_info
</select>

<select id="selectPhoneById" resultType="String">
    SELECT phone FROM user_info WHERE id = #{id}
</select>

10. 返回 Map

xml 复制代码
<!-- 单条记录返回 Map<String, Object>,key 是列名,value 是列值 -->
<select id="selectByIdAsMap" resultType="java.util.HashMap">
    SELECT * FROM user_info WHERE id = #{id}
</select>

<!-- 多条记录返回 List<Map<String, Object>> -->
<select id="selectAllAsMapList" resultType="java.util.HashMap">
    SELECT * FROM user_info
</select>

调用示例:

java 复制代码
Map<String, Object> map = userInfoMapperXML.selectByIdAsMap(1);
// → {id=1, username=admin, password=admin, age=18, gender=1, ...}

插入 SQL:<insert> 标签

1. 插入完整实体对象

xml 复制代码
<!--
    parameterType 可省略,MyBatis 自动推断
    #{} 中直接写实体类的属性名
-->
<insert id="insertUser" parameterType="com.example.entity.UserInfo">
    INSERT INTO user_info (username, password, age, gender, phone)
    VALUES (#{username}, #{password}, #{age}, #{gender}, #{phone})
</insert>

调用示例:

java 复制代码
UserInfo newUser = new UserInfo();
newUser.setUsername("wangwu");
newUser.setPassword("123456");
newUser.setAge(25);
newUser.setGender(1);
newUser.setPhone("18612340004");

Integer rows = userInfoMapperXML.insertUser(newUser);
// SQL:INSERT INTO user_info (username, password, age, gender, phone)
//      VALUES ('wangwu', '123456', 25, 1, '18612340004')
// 返回值:1
// ⚠️ newUser.getId() 仍为 null(未回填自增主键)

2. 插入并回填自增主键

xml 复制代码
<!--
    useGeneratedKeys="true"  → 使用 JDBC getGeneratedKeys() 获取自增主键
    keyProperty="id"         → 回填到 UserInfo 对象的 id 属性
    keyColumn="id"           → 数据库主键列名
-->
<insert id="insertUserAndReturnId"
        useGeneratedKeys="true"
        keyProperty="id"
        keyColumn="id">
    INSERT INTO user_info (username, password, age, gender, phone)
    VALUES (#{username}, #{password}, #{age}, #{gender}, #{phone})
</insert>

调用示例:

java 复制代码
UserInfo newUser = new UserInfo();
newUser.setUsername("zhaoliu");
newUser.setPassword("123456");
newUser.setAge(30);
newUser.setGender(1);
newUser.setPhone("18612340005");

userInfoMapperXML.insertUserAndReturnId(newUser);
System.out.println(newUser.getId()); // → 4(数据库自动分配的 ID)

关键useGeneratedKeys 只对自增主键有效。如果是手动赋值的主键(如 UUID),不需要此属性。

3. 逐字段插入

xml 复制代码
<!-- 使用 @Param 指定的名称 -->
<insert id="insertUserByFields">
    INSERT INTO user_info (username, password, age, gender, phone)
    VALUES (#{username}, #{password}, #{age}, #{gender}, #{phone})
</insert>

调用示例:

java 复制代码
userInfoMapperXML.insertUserByFields("zhouba", "123456", 35, 1, "18612340007");
// ⚠️ 逐字段传参时无法回填 ID(没有实体对象承载)

4. 批量插入(<foreach>

xml 复制代码
<!--
    separator=","  每组值之间用逗号分隔
    (#{user.username}, ...)  通过 item 前缀访问对象属性
-->
<insert id="batchInsert">
    INSERT INTO user_info (username, password, age, gender, phone) VALUES
    <foreach item="user" collection="userList" separator=",">
        (#{user.username}, #{user.password}, #{user.age}, #{user.gender}, #{user.phone})
    </foreach>
</insert>

调用示例:

java 复制代码
List<UserInfo> users = Arrays.asList(
    buildUser("userA", "pwdA", 20, 1, "10001"),
    buildUser("userB", "pwdB", 21, 2, "10002"),
    buildUser("userC", "pwdC", 22, 1, "10003")
);
Integer rows = userInfoMapperXML.batchInsert(users);
// SQL:INSERT INTO user_info (...) VALUES (...), (...), (...)
// 返回值:3
// ⚠️ 批量插入时 useGeneratedKeys 行为因 JDBC 驱动而异

5. 插入默认值

xml 复制代码
<!--
    age 和 phone 有 DEFAULT NULL,gender 有 DEFAULT 0
    create_time / update_time 由数据库自动填充
    插入时省略这些字段,数据库使用默认值
-->
<insert id="insertUserMinimal"
        useGeneratedKeys="true"
        keyProperty="id"
        keyColumn="id">
    INSERT INTO user_info (username, password)
    VALUES (#{username}, #{password})
</insert>

更新 SQL:<update> 标签

1. 按主键更新全部字段

xml 复制代码
<update id="updateById" parameterType="com.example.entity.UserInfo">
    UPDATE user_info SET
        username = #{username},
        password = #{password},
        age = #{age},
        gender = #{gender},
        phone = #{phone}
    WHERE id = #{id}
</update>

调用示例:

java 复制代码
UserInfo user = userInfoMapperXML.selectById(1);
user.setAge(26);
user.setPhone("18600000000");

Integer rows = userInfoMapperXML.updateById(user);
// SQL:UPDATE user_info SET username='admin', password='admin',
//      age=26, gender=1, phone='18600000000' WHERE id=1
// 返回值:1
// update_time 由数据库 ON UPDATE CURRENT_TIMESTAMP 自动更新

2. 按主键选择性更新

xml 复制代码
<!-- 只更新密码 -->
<update id="updatePassword">
    UPDATE user_info SET password = #{newPassword}
    WHERE id = #{id}
</update>

<!-- 只更新手机号 -->
<update id="updatePhone">
    UPDATE user_info SET phone = #{phone}
    WHERE id = #{id}
</update>

调用示例:

java 复制代码
userInfoMapperXML.updatePassword(1, "newPwd123");
// SQL:UPDATE user_info SET password = 'newPwd123' WHERE id = 1

userInfoMapperXML.updatePhone(2, "18699990000");
// SQL:UPDATE user_info SET phone = '18699990000' WHERE id = 2

3. 条件更新(WHERE 多条件)

xml 复制代码
<update id="updateAgeByCondition">
    UPDATE user_info SET age = #{newAge}
    WHERE username = #{username} AND password = #{password}
</update>

调用示例:

java 复制代码
Integer rows = userInfoMapperXML.updateAgeByCondition("zhangsan", "zhangsan", 30);
// SQL:UPDATE user_info SET age = 30 WHERE username='zhangsan' AND password='zhangsan'
// 返回值:1(匹配到并更新),0(用户名或密码不匹配)

4. CASE WHEN 批量更新

xml 复制代码
<!--
    一条 SQL 更新多行的不同值,性能优于循环逐条更新
    原理:CASE id WHEN 1 THEN 25 WHEN 2 THEN 30 END
-->
<update id="batchUpdateAge">
    UPDATE user_info SET age =
    <foreach item="user" collection="userList" open="CASE id" close="END">
        WHEN #{user.id} THEN #{user.age}
    </foreach>
    WHERE id IN
    <foreach item="user" collection="userList" open="(" separator="," close=")">
        #{user.id}
    </foreach>
</update>

调用示例:

java 复制代码
List<UserInfo> updates = Arrays.asList(
    buildUserWithId(1, 25),
    buildUserWithId(2, 30),
    buildUserWithId(3, 35)
);
userInfoMapperXML.batchUpdateAge(updates);
// SQL:UPDATE user_info SET age = CASE id
//      WHEN 1 THEN 25 WHEN 2 THEN 30 WHEN 3 THEN 35 END
//      WHERE id IN (1, 2, 3)

删除 SQL:<delete> 标签

1. 按主键删除

xml 复制代码
<delete id="deleteById" parameterType="Integer">
    DELETE FROM user_info WHERE id = #{id}
</delete>

调用示例:

java 复制代码
Integer rows = userInfoMapperXML.deleteById(5);
// SQL:DELETE FROM user_info WHERE id = 5
// 返回值:1(删除成功),0(ID 不存在)

2. 条件删除

xml 复制代码
<!-- 按用户名删除 -->
<delete id="deleteByUsername">
    DELETE FROM user_info WHERE username = #{username}
</delete>

<!-- 按年龄范围删除 -->
<delete id="deleteByAgeRange">
    DELETE FROM user_info
    WHERE age >= #{minAge} AND age &lt;= #{maxAge}
    <!-- 注意:XML 中 < 和 <= 需转义,或用 CDATA 包裹 -->
</delete>

<!-- 使用 CDATA 包裹特殊字符(推荐) -->
<delete id="deleteByAgeRangeSafe">
    DELETE FROM user_info
    WHERE <![CDATA[ age >= #{minAge} AND age <= #{maxAge} ]]>
</delete>

3. 批量删除(IN 子句)

xml 复制代码
<delete id="deleteByIds">
    DELETE FROM user_info
    WHERE id IN
    <foreach item="id" collection="idList" open="(" separator="," close=")">
        #{id}
    </foreach>
</delete>

调用示例:

java 复制代码
List<Integer> ids = Arrays.asList(4, 5, 6);
Integer rows = userInfoMapperXML.deleteByIds(ids);
// SQL:DELETE FROM user_info WHERE id IN (4, 5, 6)
// 返回值:实际删除的行数(如果 id=6 不存在,只删 2 行,返回 2)

4. 清空表

xml 复制代码
<!-- ⚠️ 危险操作:删除全表数据 -->
<delete id="deleteAll">
    DELETE FROM user_info
</delete>

调用示例:

java 复制代码
Integer rows = userInfoMapperXML.deleteAll();
// SQL:DELETE FROM user_info
// 返回值:删除的总行数

TRUNCATE TABLE 是 DDL,MyBatis 的 <delete> 不能执行。如需 TRUNCATE,必须通过 JDBC 原生方式。

动态 SQL

XML 方式相比注解方式的最大优势是 动态 SQL 支持更完善------不需要 <script> 包裹,直接使用 MyBatis 提供的动态标签。

<if> 条件判断

xml 复制代码
<!-- 根据传入参数动态拼接 WHERE 条件 -->
<select id="selectByDynamicCondition" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info
    WHERE 1=1
    <if test="username != null and username != ''">
        AND username LIKE CONCAT('%', #{username}, '%')
    </if>
    <if test="minAge != null">
        AND age >= #{minAge}
    </if>
    <if test="maxAge != null">
        AND age &lt;= #{maxAge}
    </if>
    <if test="gender != null">
        AND gender = #{gender}
    </if>
</select>

调用示例:

java 复制代码
// 只传 username
List<UserInfo> r1 = mapper.selectByDynamicCondition("zhang", null, null, null);
// SQL:SELECT * FROM user_info WHERE 1=1 AND username LIKE '%zhang%'

// 传 username + age
List<UserInfo> r2 = mapper.selectByDynamicCondition("zhang", 18, 30, null);
// SQL:SELECT * FROM user_info WHERE 1=1 AND username LIKE '%zhang%'
//      AND age >= 18 AND age <= 30

<where> 智能去除 AND/OR

xml 复制代码
<!--
    <where> 标签的作用:
    1. 如果内部有条件,自动添加 WHERE 关键字
    2. 自动去除第一个 AND 或 OR(解决 WHERE 1=1 的冗余问题)
-->
<select id="selectByWhere" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info
    <where>
        <if test="username != null and username != ''">
            AND username LIKE CONCAT('%', #{username}, '%')
        </if>
        <if test="minAge != null">
            AND age >= #{minAge}
        </if>
        <if test="gender != null">
            AND gender = #{gender}
        </if>
    </where>
</select>

使用 <where> 后不再需要 WHERE 1=1,MyBatis 会自动处理首条 AND。

<set> 智能去除逗号

xml 复制代码
<!--
    <set> 标签的作用:
    1. 自动添加 SET 关键字
    2. 自动去除末尾多余的逗号
-->
<update id="updateSelective">
    UPDATE user_info
    <set>
        <if test="username != null and username != ''">
            username = #{username},
        </if>
        <if test="password != null and password != ''">
            password = #{password},
        </if>
        <if test="age != null">
            age = #{age},
        </if>
        <if test="gender != null">
            gender = #{gender},
        </if>
        <if test="phone != null and phone != ''">
            phone = #{phone},
        </if>
    </set>
    WHERE id = #{id}
</update>

调用示例:

java 复制代码
UserInfo user = new UserInfo();
user.setId(1);
user.setAge(26);
// 只传 age,只更新 age 字段

userInfoMapperXML.updateSelective(user);
// SQL:UPDATE user_info SET age = 26 WHERE id = 1

<foreach> 集合遍历

xml 复制代码
<!--
    collection 支持的参数类型:
    - List       → list 或 @Param 指定的名称
    - Array      → array
    - Set        → collection
    - Map        → _parameter

    item  :当前遍历元素
    index :当前索引(可选)
    open  :开头拼接字符
    close :结尾拼接字符
    separator:元素间分隔符
-->
<select id="selectByMultipleIds" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info WHERE id IN
    <foreach item="id" index="index" collection="idList"
             open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

<choose> / <when> / <otherwise> 多分支选择

xml 复制代码
<!-- 类似 Java 的 switch-case-default -->
<select id="selectByChoose" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info
    <where>
        <choose>
            <when test="username != null and username != ''">
                AND username = #{username}
            </when>
            <when test="phone != null and phone != ''">
                AND phone = #{phone}
            </when>
            <otherwise>
                AND gender = 1
            </otherwise>
        </choose>
    </where>
</select>

调用示例:

java 复制代码
// username 不为空 → 按 username 查
mapper.selectByChoose("admin", null);
// SQL:SELECT * FROM user_info WHERE username = 'admin'

// username 为空,phone 不为空 → 按 phone 查
mapper.selectByChoose(null, "18612340001");
// SQL:SELECT * FROM user_info WHERE phone = '18612340001'

// 都为空 → otherwise 兜底
mapper.selectByChoose(null, null);
// SQL:SELECT * FROM user_info WHERE gender = 1

<trim> 自定义修剪

xml 复制代码
<!-- <trim> 是 <where> 和 <set> 的底层实现 -->

<!-- 等价于 <where> -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
    <if test="username != null">AND username = #{username}</if>
    <if test="age != null">AND age = #{age}</if>
</trim>
<!-- prefixOverrides="AND |OR "  → 去除前缀的 AND 或 OR -->

<!-- 等价于 <set> -->
<trim prefix="SET" suffixOverrides=",">
    <if test="username != null">username = #{username},</if>
    <if test="age != null">age = #{age},</if>
</trim>
<!-- suffixOverrides="," → 去除后缀的逗号 -->
<trim> 属性 作用 示例
prefix 在内容前添加前缀 prefix="WHERE"
suffix 在内容后添加后缀 suffix=")"
prefixOverrides 去除内容前匹配的字符 `prefixOverrides="AND
suffixOverrides 去除内容后匹配的字符 suffixOverrides=","

<sql> + <include> SQL 片段复用

xml 复制代码
<!-- 定义 SQL 片段 -->
<sql id="Base_Column_List">
    id, username, password, age, gender, phone, create_time, update_time
</sql>

<sql id="Where_Condition">
    <where>
        <if test="username != null and username != ''">
            AND username LIKE CONCAT('%', #{username}, '%')
        </if>
        <if test="minAge != null">
            AND age >= #{minAge}
        </if>
    </where>
</sql>

<!-- 引用 SQL 片段 -->
<select id="selectWithSqlFragment" resultType="com.example.entity.UserInfo">
    SELECT
    <include refid="Base_Column_List"/>
    FROM user_info
    <include refid="Where_Condition"/>
</select>

<include>refid 还可以跨 namespace 引用:refid="com.example.mapper.OtherMapper.SomeSQL"

<bind> 变量绑定

xml 复制代码
<!--
    使用 OGNL 表达式创建变量,解决数据库函数差异问题
    例如 MySQL 的 CONCAT 和 Oracle 的 || 语法不同
-->
<select id="selectByBind" resultType="com.example.entity.UserInfo">
    <bind name="pattern" value="'%' + keyword + '%'"/>
    SELECT * FROM user_info WHERE username LIKE #{pattern}
</select>

<bind> 常用于模糊查询,避免在不同数据库之间切换 CONCAT 语法。

高级特性

<resultMap> 自定义结果映射

当数据库列名与 Java 属性名不一致(且未开启驼峰转换),或需要复杂映射时使用:

xml 复制代码
<!-- 定义 resultMap -->
<resultMap id="BaseResultMap" type="com.example.entity.UserInfo">
    <!-- id 标签:主键映射 -->
    <id column="id" property="id" jdbcType="INTEGER"/>

    <!-- result 标签:普通字段映射 -->
    <result column="username" property="username" jdbcType="VARCHAR"/>
    <result column="password" property="password" jdbcType="VARCHAR"/>
    <result column="age" property="age" jdbcType="INTEGER"/>
    <result column="gender" property="gender" jdbcType="TINYINT"/>
    <result column="phone" property="phone" jdbcType="VARCHAR"/>
    <result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
    <result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>

<!-- 使用 resultMap(不是 resultType) -->
<select id="selectByIdWithResultMap" resultMap="BaseResultMap">
    SELECT * FROM user_info WHERE id = #{id}
</select>

resultMap vs resultType

场景 使用
列名与属性名一致(或已开启驼峰转换) resultType
列名与属性名不一致,且未开启驼峰转换 resultMap
需要嵌套对象映射(association / collection) resultMap
需要类型转换器(typeHandler) resultMap

<selectKey> 获取主键(非自增场景)

xml 复制代码
<!-- 场景:主键由程序生成(如 UUID),插入前获取 -->
<insert id="insertWithUUID">
    <!--
        order="BEFORE"  → 在 INSERT 之前执行
        keyProperty="id" → 将结果设置到 UserInfo.id
    -->
    <selectKey keyProperty="id" resultType="String" order="BEFORE">
        SELECT REPLACE(UUID(), '-', '')
    </selectKey>
    INSERT INTO user_info (id, username, password)
    VALUES (#{id}, #{username}, #{password})
</insert>

<!-- 场景:插入后获取自增主键(不依赖 useGeneratedKeys) -->
<insert id="insertWithSelectKey">
    <selectKey keyProperty="id" resultType="Integer" order="AFTER">
        SELECT LAST_INSERT_ID()
    </selectKey>
    INSERT INTO user_info (username, password)
    VALUES (#{username}, #{password})
</insert>

XML 中的特殊字符处理

xml 复制代码
<!-- 方式一:使用 XML 转义符 -->
<!--
    <   → &lt;
    >   → &gt;
    <=  → &lt;=
    >=  → &gt;=
    &   → &amp;
    "   → &quot;
    '   → '
-->

<select id="selectByAgeRange" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info WHERE age &gt;= #{minAge} AND age &lt;= #{maxAge}
</select>

<!-- 方式二:使用 CDATA 包裹(推荐) -->
<select id="selectByAgeRangeCDATA" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info
    WHERE <![CDATA[ age >= #{minAge} AND age <= #{maxAge} ]]>
</select>

推荐使用 CDATA,可读性更好,不容易遗漏转义。

参数传递机制

单参数传递

java 复制代码
// 接口
UserInfo selectById(Integer id);
xml 复制代码
<!-- 单参数时,{} 中的名称可以任意指定 -->
<select id="selectById" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info WHERE id = #{任意名称}
</select>

<!-- 两种写法都正确 -->
<select id="selectById" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info WHERE id = #{id}
</select>

多参数传递(使用 @Param)

java 复制代码
// 接口
List<UserInfo> selectByCondition(
    @Param("username") String username,
    @Param("minAge") Integer minAge);
xml 复制代码
<!-- @Param 指定的名称直接使用 -->
<select id="selectByCondition" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info
    WHERE username = #{username} AND age >= #{minAge}
</select>

未使用 @Param 时的默认名称:

参数位置 默认名称(从 0 开始) 默认名称(从 1 开始)
第 1 个参数 arg0 param1
第 2 个参数 arg1 param2
第 3 个参数 arg2 param3

推荐始终使用 @Param 明确指定参数名称,提高代码可读性和可维护性。

传递对象参数

java 复制代码
// 接口
Integer insertUser(UserInfo userInfo);
xml 复制代码
<!-- 直接使用属性名 -->
<insert id="insertUser">
    INSERT INTO user_info (username, password, age)
    VALUES (#{username}, #{password}, #{age})
</insert>
java 复制代码
// 使用 @Param 为对象加前缀
Integer insertUser(@Param("user") UserInfo userInfo);
xml 复制代码
<!-- 通过前缀访问属性 -->
<insert id="insertUser">
    INSERT INTO user_info (username, password, age)
    VALUES (#{user.username}, #{user.password}, #{user.age})
</insert>

${}#{} 的区别

详见:\[${}与 {}的区别]

在 XML 中的使用规则与注解方式完全一致:

符号 机制 防注入 适用场景
#{} JDBC PreparedStatement ? 占位符 ✅ 安全 WHERE 值、INSERT 值、UPDATE 值、LIMIT 值
${} 字符串直接拼接 ❌ 不安全 ORDER BY 列名、表名、LIKE(需手动防注入)
xml 复制代码
<!-- #{} → 预编译占位符 → 安全 -->
<select id="selectById" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info WHERE id = #{id}
</select>
-- 生成:SELECT * FROM user_info WHERE id = ?

<!-- ${} → 直接拼接 → 必须做白名单校验 -->
<select id="selectOrdered" resultType="com.example.entity.UserInfo">
    SELECT * FROM user_info ORDER BY ${column} ${direction}
</select>
-- 生成:SELECT * FROM user_info ORDER BY age DESC

XML 方式 vs 注解方式 对比总结⭐

维度 XML 方式 注解方式
SQL 位置 独立 XML 文件 注解字符串中
SQL 与代码分离 ✅ 是 ❌ 否
动态 SQL 原生支持 <if> <where> <script> 包裹
SQL 可读性 ✅ 高(长 SQL 不受限制) ❌ 低(长 SQL 拼接困难)
可维护性 ✅ SQL 集中管理 ❌ 散落在各方法上
SQL 调试 ✅ 可单独查看 XML ❌ 嵌在注解中难定位
配置复杂度 需配置 mapper-locations 无需额外配置
文件数量 接口 + XML(两份) 仅接口(一份)
适用场景 复杂 SQL、大量动态条件 简单 CRUD
团队分工 ✅ DBA 可独立维护 XML ❌ 需要懂 Java
相关推荐
小饼干在学嘎瓦1 小时前
秒杀场景Redis做预扣减,问题在哪里?
数据库·redis·mybatis
来杯@Java11 小时前
图书管理系统(基于springboot+vue前后端分离的项目)计算机毕业设计java
java·spring boot·spring·vue·毕业设计·mybatis·课程设计
Pluchon1 天前
萌萌技术分享笔记——Java综合项目
java·开发语言·笔记·git·github·mybatis·postman
骄马之死1 天前
MyBatis SqlSession 与缓存机制详解
mysql·mybatis
IronMurphy2 天前
SSM拷打第二讲!!!
java·spring·mybatis
祭曦念2 天前
ArkUI声明式UI入门:从XML到声明式的思维转变
xml·ui·鸿蒙
C+-C资深大佬3 天前
SSM 框架(Spring + SpringMVC + MyBatis)
java·spring·mybatis
二王一个今3 天前
springboot security 权限控制---循环依赖问题
mybatis
落木萧萧8253 天前
为什么我把 MyBatisGX 设计成现在这样
mybatis·orm