MyBatis 完整教程

MyBatis 完整教程

目录

  1. MyBatis 简介
  2. 环境搭建
  3. 核心概念
  4. 注解方式开发
  5. XML方式开发
  6. 动态SQL
  7. 结果映射
  8. 参数传递
  9. 完整实战案例
  10. 最佳实践

1. MyBatis 简介

MyBatis 是一款优秀的持久层框架,它支持:

  • 自定义 SQL
  • 存储过程
  • 高级映射

优势:

  • 避免了几乎所有的 JDBC 代码
  • 手动设置参数和获取结果集的工作
  • 灵活的 SQL 编写
  • 注解和 XML 两种配置方式

2. 环境搭建

2.1 Maven 依赖

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

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

    <!-- Lombok (可选,但推荐) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2.2 配置文件 (application.yml)

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver

# MyBatis 配置
mybatis:
  # XML 文件位置 (classpath: = src/main/resources/)
  # 实际对应: src/main/resources/mapper/*.xml
  mapper-locations: classpath:mapper/*.xml
  
  # 实体类包路径 (配置后XML中可直接用类名,无需写完整包名)
  # 例如: resultType="User" 而不是 resultType="com.example.demo.domain.User"
  type-aliases-package: com.example.demo.domain
  configuration:
    # 驼峰命名自动转换:数据库下划线命名 ↔ Java驼峰命名
    # 数据库字段: user_name, created_at, order_no
    # Java属性:   userName,  createdAt,  orderNo
    # 开启后无需手动映射,MyBatis自动完成转换
    map-underscore-to-camel-case: true
    
    # SQL 日志实现(开发时建议开启,生产环境建议关闭)
    # STDOUT_LOGGING - 标准输出到控制台
    # SLF4J - 使用 SLF4J 日志框架(推荐)
    # LOG4J2 - 使用 Log4j2
    # NO_LOGGING - 不输出日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    
    # 其他常用配置:
    # cache-enabled: true              # 开启二级缓存(默认true)
    # lazy-loading-enabled: false      # 延迟加载(默认false)
    # default-executor-type: SIMPLE    # 执行器类型:SIMPLE/REUSE/BATCH
    # default-statement-timeout: 25    # 超时时间(秒)
    # jdbc-type-for-null: NULL         # 空值对应的JDBC类型

2.3 主启动类

package com.example.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.example.demo.mapper") // 扫描 Mapper 接口
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

3. 核心概念

3.1 三层架构

Controller (控制层)
    ↓
Service (业务层)
    ↓
Mapper/DAO (持久层) ← MyBatis 在这里工作
    ↓
Database (数据库)

3.2 核心组件

组件 说明
Domain/Entity 实体类,对应数据库表
Mapper接口 定义数据库操作方法
Mapper.xml SQL 语句配置文件
SqlSession 执行SQL的会话对象(Spring Boot自动管理)

4. 注解方式开发

4.1 创建数据库表

CREATE TABLE user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100),
    age INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

INSERT INTO user (username, email, age) VALUES 
('张三', 'zhangsan@example.com', 25),
('李四', 'lisi@example.com', 30),
('王五', 'wangwu@example.com', 28);

4.2 创建实体类

package com.example.demo.domain;

import lombok.Data;
import java.time.LocalDateTime;

@Data
public class User {
    private Long id;
    private String username;
    private String email;
    private Integer age;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

4.3 创建 Mapper 接口(注解方式)

package com.example.demo.mapper;

import com.example.demo.domain.User;
import org.apache.ibatis.annotations.*;
import java.util.List;

@Mapper
public interface UserMapper {
    
    // ========== 查询操作 ==========
    
    /**
     * 查询所有用户
     */
    @Select("SELECT * FROM user")
    List<User> findAll();
    
    /**
     * 根据ID查询用户
     * @Param("id") - 指定参数在SQL中的名称,单个参数可省略,多个参数必须加
     */
    @Select("SELECT * FROM user WHERE id = #{id}")
    User findById(@Param("id") Long id);
    
    /**
     * 根据用户名查询
     */
    @Select("SELECT * FROM user WHERE username = #{username}")
    User findByUsername(@Param("username") String username);
    
    /**
     * 条件查询(多参数)
     */
    @Select("SELECT * FROM user WHERE age >= #{minAge} AND age <= #{maxAge}")
    List<User> findByAgeRange(@Param("minAge") Integer minAge, 
                               @Param("maxAge") Integer maxAge);
    
    /**
     * 模糊查询
     */
    @Select("SELECT * FROM user WHERE username LIKE CONCAT('%', #{keyword}, '%')")
    List<User> searchByUsername(@Param("keyword") String keyword);
    
    /**
     * 统计数量
     */
    @Select("SELECT COUNT(*) FROM user")
    int count();
    
    /**
     * 分页查询
     */
    @Select("SELECT * FROM user LIMIT #{limit} OFFSET #{offset}")
    List<User> findByPage(@Param("offset") int offset, 
                          @Param("limit") int limit);
    
    // ========== 插入操作 ==========
    
    /**
     * 插入用户(返回受影响行数)
     */
    @Insert("INSERT INTO user (username, email, age) VALUES (#{username}, #{email}, #{age})")
    int insert(User user);
    
    /**
     * 插入用户(返回自增主键)
     * @Options 配置项说明:
     * - useGeneratedKeys = true: 使用数据库自增主键
     * - keyProperty = "id": 将自增的主键值回填到 user 对象的 id 属性
     * 插入后,user.getId() 就能获取到数据库自动生成的ID
     * 
     * 参数映射规则:#{属性名} 会调用对象的 getter 方法
     * #{username} → user.getUsername()
     * #{email}    → user.getEmail()
     * #{age}      → user.getAge()
     */
    @Insert("INSERT INTO user (username, email, age) VALUES (#{username}, #{email}, #{age})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insertAndReturnId(User user);
    
    // ========== 更新操作 ==========
    
    /**
     * 更新用户信息
     */
    @Update("UPDATE user SET username = #{username}, email = #{email}, age = #{age} WHERE id = #{id}")
    int update(User user);
    
    /**
     * 更新部分字段
     */
    @Update("UPDATE user SET email = #{email} WHERE id = #{id}")
    int updateEmail(@Param("id") Long id, @Param("email") String email);
    
    // ========== 删除操作 ==========
    
    /**
     * 根据ID删除
     */
    @Delete("DELETE FROM user WHERE id = #{id}")
    int deleteById(@Param("id") Long id);
    
    /**
     * 根据条件删除
     */
    @Delete("DELETE FROM user WHERE age < #{age}")
    int deleteByAge(@Param("age") Integer age);
}

💡 返回类型说明

MyBatis 如何确定返回类型?

关键:通过方法签名的返回类型声明,而不是 SQL 语句!

MyBatis 会根据你在 Mapper 接口方法中声明的返回类型,自动处理查询结果的映射。

常见返回类型对比:

返回类型 说明 使用场景 示例方法
User 返回单个对象 按唯一键查询(ID、username等),预期0或1条结果 findById, findByUsername
List<User> 返回集合 条件查询,可能返回0条、1条或多条结果 findAll, findByAgeRange
int/long 返回数值 统计查询、增删改操作返回受影响行数 count(), insert(), update()
Map<K,V> 返回键值对 返回单行的动态列 findUserAsMap()
List<Map> 返回Map集合 返回多行的动态列 findAllAsMap()

重要说明:

// ✅ 返回 List<User> - 返回所有符合条件的记录
@Select("SELECT * FROM user WHERE age >= #{minAge} AND age <= #{maxAge}")
List<User> findByAgeRange(@Param("minAge") Integer minAge, 
                           @Param("maxAge") Integer maxAge);

// ⚠️ 返回 User - 只返回第一条记录,其他记录被忽略
@Select("SELECT * FROM user WHERE age >= #{minAge} AND age <= #{maxAge}")
User findByAgeRange(@Param("minAge") Integer minAge, 
                    @Param("maxAge") Integer maxAge);

区别:

  • 返回 User:如果 SQL 查询匹配了 10 条记录,MyBatis 只会返回第一条,其他 9 条被丢弃;如果没有结果则返回 null
  • 返回 List<User>:会返回所有匹配的记录(空列表、1条或多条)

选择原则:

  1. 确定只有一条结果 → 用 User

    // ID是主键,结果唯一
    @Select("SELECT * FROM user WHERE id = #{id}")
    User findById(@Param("id") Long id);
    
  2. 可能有多条结果 → 用 List<User>

    // 年龄范围查询,可能匹配多个用户
    @Select("SELECT * FROM user WHERE age >= #{minAge} AND age <= #{maxAge}")
    List<User> findByAgeRange(@Param("minAge") Integer minAge, 
                               @Param("maxAge") Integer maxAge);
    
  3. 增删改操作 → 用 int/long(返回受影响行数)

    @Insert("INSERT INTO user (username, email, age) VALUES (#{username}, #{email}, #{age})")
    int insert(User user);  // 返回插入的行数(通常是1)
    
    @Update("UPDATE user SET email = #{email} WHERE id = #{id}")
    int updateEmail(@Param("id") Long id, @Param("email") String email);
    
    @Delete("DELETE FROM user WHERE age < #{age}")
    int deleteByAge(@Param("age") Integer age);  // 返回删除的行数
    
  4. 统计查询 → 用 int/long

    @Select("SELECT COUNT(*) FROM user")
    int count();
    

实际案例:

// ❌ 错误示例:业务需要查询所有符合条件的用户,却用了 User
@Select("SELECT * FROM user WHERE status = 'active'")
User findActiveUsers();  // 只会返回第1个活跃用户,其他的丢失了!

// ✅ 正确示例:使用 List<User>
@Select("SELECT * FROM user WHERE status = 'active'")
List<User> findActiveUsers();  // 返回所有活跃用户

总结:

  • 返回类型是开发者根据业务需求查询特点主动声明的
  • MyBatis 不会自动判断结果数量来改变返回类型
  • 选择错误的返回类型可能导致数据丢失或空指针异常

4.4 Service 层

package com.example.demo.service;

import com.example.demo.domain.User;
import com.example.demo.mapper.UserMapper;
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 List<User> getAllUsers() {
        return userMapper.findAll();
    }
    
    public User getUserById(Long id) {
        return userMapper.findById(id);
    }
    
    /**
     * @Transactional 事务管理注解
     * 作用:保证方法内的所有数据库操作要么全部成功,要么全部回滚
     * - 方法执行成功 → 自动提交事务(commit)
     * - 方法抛出异常 → 自动回滚事务(rollback)
     */
    @Transactional
    public User createUser(User user) {
        userMapper.insertAndReturnId(user);
        return user; // id 已经被自动填充
    }
    
    @Transactional
    public boolean updateUser(User user) {
        return userMapper.update(user) > 0;
    }
    
    @Transactional
    public boolean deleteUser(Long id) {
        return userMapper.deleteById(id) > 0;
    }
}

4.5 Controller 层

package com.example.demo.controller;

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

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping
    public List<User> getAllUsers() {
        return userService.getAllUsers();
    }
    
    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userService.getUserById(id);
    }
    
    @PostMapping
    public User createUser(@RequestBody User user) {
        return userService.createUser(user);
    }
    
    @PutMapping("/{id}")
    public boolean updateUser(@PathVariable Long id, @RequestBody User user) {
        user.setId(id);
        return userService.updateUser(user);
    }
    
    @DeleteMapping("/{id}")
    public boolean deleteUser(@PathVariable Long id) {
        return userService.deleteUser(id);
    }
}

5. XML方式开发

5.1 数据库表(订单示例)

CREATE TABLE `order` (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(50) NOT NULL UNIQUE,
    user_id BIGINT NOT NULL,
    total_amount DECIMAL(10, 2),
    status VARCHAR(20),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE TABLE order_item (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_id BIGINT NOT NULL,
    product_name VARCHAR(100),
    quantity INT,
    price DECIMAL(10, 2),
    FOREIGN KEY (order_id) REFERENCES `order`(id)
);

5.2 实体类

package com.example.demo.domain;

import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

@Data
public class Order {
    private Long id;
    private String orderNo;
    private Long userId;
    private BigDecimal totalAmount;
    private String status;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    // 一对多关联
    private List<OrderItem> items;
}

@Data
public class OrderItem {
    private Long id;
    private Long orderId;
    private String productName;
    private Integer quantity;
    private BigDecimal price;
}

5.3 Mapper 接口

package com.example.demo.mapper;

import com.example.demo.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;

@Mapper
public interface OrderMapper {
    
    // 所有方法都在 XML 中实现
    Order findById(@Param("id") Long id);
    
    List<Order> findByUserId(@Param("userId") Long userId);
    
    List<Order> findByCondition(@Param("status") String status, 
                                @Param("minAmount") BigDecimal minAmount);
    
    int insert(Order order);
    
    int update(Order order);
    
    int deleteById(@Param("id") Long id);
    
    // 关联查询:查询订单及其所有订单项
    Order findByIdWithItems(@Param("id") Long id);
}

5.4 Mapper XML 配置

创建文件:src/main/resources/mapper/OrderMapper.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.demo.mapper.OrderMapper">

    <!-- ========== ResultMap 结果映射 ========== -->
    
    <!-- 基础结果映射 -->
    <resultMap id="BaseResultMap" type="com.example.demo.domain.Order">
        <id property="id" column="id"/>
        <result property="orderNo" column="order_no"/>
        <result property="userId" column="user_id"/>
        <result property="totalAmount" column="total_amount"/>
        <result property="status" column="status"/>
        <result property="createdAt" column="created_at"/>
        <result property="updatedAt" column="updated_at"/>
    </resultMap>
    
    <!-- 带关联的结果映射 -->
    <resultMap id="OrderWithItemsMap" type="com.example.demo.domain.Order" extends="BaseResultMap">
        <!-- collection: 一对多关联 -->
        <collection property="items" ofType="com.example.demo.domain.OrderItem">
            <id property="id" column="item_id"/>
            <result property="orderId" column="order_id"/>
            <result property="productName" column="product_name"/>
            <result property="quantity" column="quantity"/>
            <result property="price" column="price"/>
        </collection>
    </resultMap>
    
    <!-- ========== 字段映射说明 ========== -->
    <!--
    💡 如果 ResultMap 中有些字段没有映射会怎么样?
    
    **规则:**
    1. ✅ 已映射的字段:按照 ResultMap 配置进行映射
    2. ❌ 未映射的字段:该属性值为 null(不会自动映射,即使字段名一致)
    
    **重要特性:一旦使用 ResultMap,自动映射将失效!**
    
    示例:假设 Order 类有 7 个属性
    public class Order {
        private Long id;
        private String orderNo;
        private Long userId;
        private BigDecimal totalAmount;
        private String status;
        private LocalDateTime createdAt;
        private LocalDateTime updatedAt;  // ← 假设这个字段没有在 ResultMap 中映射
    }
    
    如果 ResultMap 中没有映射 updatedAt:
    <resultMap id="IncompleteMap" type="Order">
        <id property="id" column="id"/>
        <result property="orderNo" column="order_no"/>
        <result property="userId" column="user_id"/>
        <result property="totalAmount" column="total_amount"/>
        <result property="status" column="status"/>
        <result property="createdAt" column="created_at"/>
        <!-- updatedAt 没有映射! -->
    </resultMap>
    
    结果:
    - order.getId()         → 有值 ✅
    - order.getOrderNo()    → 有值 ✅
    - order.getUserId()     → 有值 ✅
    - order.getTotalAmount()→ 有值 ✅
    - order.getStatus()     → 有值 ✅
    - order.getCreatedAt()  → 有值 ✅
    - order.getUpdatedAt()  → null ❌(即使数据库有值,也不会自动映射)
    
    **三种解决方案:**
    
    方案1:补全所有字段映射(推荐用于字段名不一致的情况)
    <resultMap id="CompleteMap" type="Order">
        <id property="id" column="id"/>
        <result property="orderNo" column="order_no"/>
        <result property="userId" column="user_id"/>
        <result property="totalAmount" column="total_amount"/>
        <result property="status" column="status"/>
        <result property="createdAt" column="created_at"/>
        <result property="updatedAt" column="updated_at"/>  ← 补全
    </resultMap>
    
    方案2:开启 autoMappingBehavior(推荐,最常用)
    <resultMap id="BaseResultMap" type="Order" autoMapping="true">
        <!-- 只映射特殊字段(如字段名不一致的) -->
        <id property="id" column="id"/>
        <result property="orderNo" column="order_no"/>  <!-- 数据库是 order_no,需要映射 -->
        <!-- 其他字段名一致的字段会自动映射 -->
    </resultMap>
    
    方案3:使用 resultType 而不是 resultMap(适合简单场景)
    <!-- 当字段名一致(或开启了驼峰转换)时,直接用 resultType -->
    <select id="findById" resultType="Order">
        SELECT id, order_no, user_id, total_amount, status, created_at, updated_at
        FROM `order`
        WHERE id = #{id}
    </select>
    
    **⚠️ 什么叫"字段名一致"?**
    
    "一致"的定义取决于是否开启了驼峰命名转换(map-underscore-to-camel-case):
    
    情况1:未开启驼峰转换(map-underscore-to-camel-case: false)
    ┌─────────────────┬──────────────┬────────┐
    │ 数据库字段名     │ Java属性名   │ 是否一致│
    ├─────────────────┼──────────────┼────────┤
    │ id              │ id           │ ✅ 一致 │
    │ username        │ username     │ ✅ 一致 │
    │ order_no        │ orderNo      │ ❌ 不一致(需手动映射)│
    │ created_at      │ createdAt    │ ❌ 不一致(需手动映射)│
    └─────────────────┴──────────────┴────────┘
    结论:必须完全相同才算一致(包括大小写、下划线)
    
    情况2:开启驼峰转换(map-underscore-to-camel-case: true)← 推荐配置
    ┌─────────────────┬──────────────┬────────┐
    │ 数据库字段名     │ Java属性名   │ 是否一致│
    ├─────────────────┼──────────────┼────────┤
    │ id              │ id           │ ✅ 一致 │
    │ username        │ username     │ ✅ 一致 │
    │ order_no        │ orderNo      │ ✅ 一致(自动转换)│
    │ created_at      │ createdAt    │ ✅ 一致(自动转换)│
    │ user_id         │ userId       │ ✅ 一致(自动转换)│
    │ gmt_create      │ createdAt    │ ❌ 不一致(语义不同,需手动映射)│
    └─────────────────┴──────────────┴────────┘
    结论:下划线命名 ↔ 驼峰命名 自动转换,也算一致
    
    **转换规则详解:**
    order_no     → orderNo      (o_n → On)
    user_name    → userName     (u_n → Un)
    created_at   → createdAt    (c_a → Ca)
    is_deleted   → isDeleted    (i_d → Id)
    
    **实际示例对比:**
    
    // 未开启驼峰转换时
    <resultMap id="UserMap" type="User">
        <result property="userName" column="user_name"/>  <!-- 必须手动映射 -->
        <result property="createdAt" column="created_at"/> <!-- 必须手动映射 -->
    </resultMap>
    
    // 开启驼峰转换后(推荐!)
    <select id="findById" resultType="User">
        SELECT id, user_name, created_at FROM user WHERE id = #{id}
        <!-- user_name 自动映射到 userName -->
        <!-- created_at 自动映射到 createdAt -->
    </select>
    
    **配置驼峰转换:**
    # application.yml
    mybatis:
      configuration:
        map-underscore-to-camel-case: true  ← 开启此配置
    
    开启后的效果:
    ✅ 数据库用下划线命名(user_name, order_no)
    ✅ Java用驼峰命名(userName, orderNo)
    ✅ MyBatis自动转换,无需手动映射
    
    **最佳实践:**
    1. 如果所有字段都需要自定义映射 → 使用 <resultMap> 并映射所有字段
    2. 如果只有部分字段需要映射 → 使用 <resultMap autoMapping="true">
    3. 如果字段名一致(或已开启驼峰转换)→ 直接使用 resultType
    4. 配置文件已开启驼峰转换时(map-underscore-to-camel-case: true):
       - 数据库字段 order_no → Java属性 orderNo(自动转换)
       - 数据库字段 created_at → Java属性 createdAt(自动转换)
    
    **对比表:**
    | 场景 | 使用方式 | 是否自动映射 | 示例 |
    |------|---------|-------------|------|
    | 字段名完全一致 | resultType | ✅ 自动 | 数据库 id → Java id |
    | 开启驼峰转换 | resultType | ✅ 自动 | 数据库 user_name → Java userName |
    | 字段名不一致 | resultMap | ❌ 手动 | 数据库 gmt_create → Java createdAt |
    | 复杂关联查询 | resultMap | ❌ 手动 | 一对多、多对多关联 |
    | resultMap + autoMapping | resultMap autoMapping="true" | ⚠️ 部分自动 | 特殊字段手动,其他自动 |
    -->
    
    <!-- ========== SQL 片段(可复用) ========== -->
    
    <sql id="baseColumns">
        id, order_no, user_id, total_amount, status, created_at, updated_at
    </sql>
    
    <sql id="whereCondition">
        <where>
            <if test="status != null and status != ''">
                AND status = #{status}
            </if>
            <if test="minAmount != null">
                AND total_amount >= #{minAmount}
            </if>
        </where>
    </sql>
    
    <!-- ========== 查询操作 ========== -->
    
    <!-- 根据ID查询 -->
    <select id="findById" parameterType="long" resultMap="BaseResultMap">
        SELECT <include refid="baseColumns"/>
        FROM `order`
        WHERE id = #{id}
    </select>
    
    <!-- 根据用户ID查询 -->
    <select id="findByUserId" parameterType="long" resultMap="BaseResultMap">
        SELECT <include refid="baseColumns"/>
        FROM `order`
        WHERE user_id = #{userId}
        ORDER BY created_at DESC
    </select>
    
    <!-- 条件查询 -->
    <select id="findByCondition" resultMap="BaseResultMap">
        SELECT <include refid="baseColumns"/>
        FROM `order`
        <include refid="whereCondition"/>
        ORDER BY created_at DESC
    </select>
    
    <!-- 关联查询:订单 + 订单项 -->
    <select id="findByIdWithItems" parameterType="long" resultMap="OrderWithItemsMap">
        SELECT 
            o.id, o.order_no, o.user_id, o.total_amount, o.status, 
            o.created_at, o.updated_at,
            oi.id AS item_id, oi.order_id, oi.product_name, 
            oi.quantity, oi.price
        FROM `order` o
        LEFT JOIN order_item oi ON o.id = oi.order_id
        WHERE o.id = #{id}
    </select>
    
    <!-- ========== 插入操作 ========== -->
    
    <insert id="insert" parameterType="com.example.demo.domain.Order" 
            useGeneratedKeys="true" keyProperty="id">
        INSERT INTO `order` (order_no, user_id, total_amount, status)
        VALUES (#{orderNo}, #{userId}, #{totalAmount}, #{status})
    </insert>
    
    <!-- ========== 更新操作 ========== -->
    
    <update id="update" parameterType="com.example.demo.domain.Order">
        UPDATE `order`
        <set>
            <if test="orderNo != null">order_no = #{orderNo},</if>
            <if test="totalAmount != null">total_amount = #{totalAmount},</if>
            <if test="status != null">status = #{status},</if>
        </set>
        WHERE id = #{id}
    </update>
    
    <!-- ========== 删除操作 ========== -->
    
    <delete id="deleteById" parameterType="long">
        DELETE FROM `order` WHERE id = #{id}
    </delete>

</mapper>

6. 动态SQL

动态SQL是MyBatis的强大特性之一,可以根据不同条件动态拼接SQL语句,避免编写大量的if-else逻辑。

6.1 if 标签

作用: 根据条件判断是否包含某段SQL

基本语法:

<if test="条件表达式">
    SQL片段
</if>

完整示例:

Mapper 接口定义:

// 方式1:使用 @Param 注解(推荐)
List<User> findUsers(@Param("username") String username, 
                     @Param("minAge") Integer minAge, 
                     @Param("maxAge") Integer maxAge);

XML 配置:

<select id="findUsers" resultType="User">
    SELECT * FROM user
    <where>
        <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>
    </where>
</select>

💡 参数是如何传入的?

MyBatis 通过以下方式获取参数:

1. 使用 @Param 注解指定参数名(推荐)

// Mapper 接口
List<User> findUsers(@Param("username") String username, 
                     @Param("minAge") Integer minAge, 
                     @Param("maxAge") Integer maxAge);

// Service 层调用
List<User> users = userMapper.findUsers("张三", 20, 30);

工作原理:

  • @Param("username") 告诉 MyBatis:这个参数在 XML 中叫 username
  • XML 中的 #{username} 就能获取到传入的值 "张三"
  • test="username != null" 也是通过参数名 username 来判断

2. 不使用 @Param 的默认规则

// ❌ 不推荐:不加 @Param
List<User> findUsers(String username, Integer minAge, Integer maxAge);

// XML 中需要使用默认参数名(arg0, arg1 或 param1, param2)
<if test="arg0 != null">  <!-- 或者 param1 -->
    AND username = #{arg0}  <!-- 或者 #{param1} -->
</if>

3. parameterType 属性(可选)

<!-- parameterType 是可选的,MyBatis 可以自动推断 -->
<select id="findUsers" parameterType="map" resultType="User">
    SELECT * FROM user WHERE username = #{username}
</select>

parameterType 说明:

  • 可以省略:MyBatis 会自动推断参数类型
  • ⚠️ 一般不写:现代项目很少使用,容易造成混淆
  • 📝 如果要写:只在参数是复杂对象时才可能用到

4. 参数传递的几种方式对比

方式1:多个基本参数(使用 @Param)

// Mapper 接口
List<User> findUsers(@Param("username") String username, 
                     @Param("minAge") Integer minAge);

// XML 配置
<select id="findUsers" resultType="User">
    SELECT * FROM user 
    WHERE username = #{username} AND age >= #{minAge}
</select>

// 调用
List<User> users = userMapper.findUsers("张三", 20);

方式2:单个对象参数

// 定义查询条件对象
public class UserQuery {
    private String username;
    private Integer minAge;
    private Integer maxAge;
    // getters and setters
}

// Mapper 接口
List<User> findUsers(UserQuery query);

// XML 配置(直接使用对象属性名)
<select id="findUsers" resultType="User">
    SELECT * FROM user
    <where>
        <if test="username != null">
            AND username = #{username}  <!-- 自动调用 query.getUsername() -->
        </if>
        <if test="minAge != null">
            AND age >= #{minAge}  <!-- 自动调用 query.getMinAge() -->
        </if>
    </where>
</select>

// 调用
UserQuery query = new UserQuery();
query.setUsername("张三");
query.setMinAge(20);
List<User> users = userMapper.findUsers(query);

方式3:Map 参数

// Mapper 接口
List<User> findUsers(Map<String, Object> params);

// XML 配置(使用 Map 的 key)
<select id="findUsers" resultType="User">
    SELECT * FROM user
    WHERE username = #{username} AND age >= #{minAge}
</select>

// 调用
Map<String, Object> params = new HashMap<>();
params.put("username", "张三");
params.put("minAge", 20);
List<User> users = userMapper.findUsers(params);

参数获取总结表:

参数方式 Mapper 接口 XML 中如何获取 是否需要 @Param
单个基本参数 findById(Long id) #{id}#{任意名} ❌ 不需要
多个基本参数 find(String name, int age) #{param1}, #{param2} ✅ 建议加
多个 @Param find(@Param("name") String name) #{name} ✅ 推荐
对象参数 findUsers(UserQuery query) #{username} (属性名) ❌ 不需要
Map 参数 findUsers(Map params) #{username} (key名) ❌ 不需要

最佳实践:

  1. 多个参数时,总是使用 @Param(清晰明了)
  2. 复杂查询条件,使用对象封装(可维护性好)
  3. 不要使用 Map 传参(类型不安全,难以维护)
  4. parameterType 一般省略(MyBatis 自动推断)

详细解释:

  1. <where> 标签的作用:

    • 自动处理 WHERE 关键字
    • 自动去除第一个条件前多余的 AND 或 OR
    • 如果没有任何条件成立,不会生成 WHERE 子句
  2. <if test="条件"> 判断规则:

    • username != null:判断参数是否为null
    • username != '':判断字符串是否为空串
    • 两个条件用 and 连接(注意是小写)
    • 也可以用 or 连接多个条件
  3. XML 中的特殊字符转义:

核心问题:为什么 >= 可以直接写,而 <= 要转义?

<if test="minAge != null">
    AND age >= #{minAge}      <!-- ✅ >= 可以直接写 -->
</if>
<if test="maxAge != null">
    AND age &lt;= #{maxAge}   <!-- ⚠️ <= 必须转义成 &lt;= -->
</if>

原因:

  • < 是 XML 标签的开始符号(如 <if><select>),必须转义
  • > 是 XML 标签的结束符号,可以直接使用(但建议也转义)

XML 特殊字符对比:

符号 含义 XML转义 是否必须转义 示例
< 小于 &lt; 必须 age &lt; 18
<= 小于等于 &lt;= 必须 age &lt;= 18
> 大于 &gt; ⚠️ 建议(可不转义) age > 18age &gt; 18
>= 大于等于 &gt;= ⚠️ 建议(可不转义) age >= 18age &gt;= 18
& 与符号 &amp; 必须 if (a &amp;&amp; b)
" 双引号 &quot; ⚠️ 属性值中建议 name="value"
' 单引号 &apos; ⚠️ 属性值中建议 name='value'

为什么 < 必须转义?

<!-- ❌ 错误示例:XML解析器会认为 "age < 18" 中的 < 是一个标签开始 -->
<if test="age != null">
    AND age < #{age}  <!-- 报错!XML解析器混乱了 -->
</if>

<!-- ✅ 正确示例1:转义 -->
<if test="age != null">
    AND age &lt; #{age}  <!-- 正确 -->
</if>

<!-- ✅ 正确示例2:使用 CDATA -->
<if test="age != null">
    <![CDATA[ AND age < #{age} ]]>  <!-- 正确 -->
</if>

为什么 > 可以不转义?

<!-- ✅ 这样写不会报错(虽然不推荐)-->
<if test="age != null">
    AND age > #{age}  <!-- 可以,但不规范 -->
</if>

<!-- ✅ 更规范的写法 -->
<if test="age != null">
    AND age &gt; #{age}  <!-- 推荐 -->
</if>

最佳实践:

方式1:使用转义字符(推荐,清晰明了)

<if test="minAge != null">AND age &gt;= #{minAge}</if>
<if test="maxAge != null">AND age &lt;= #{maxAge}</if>
<if test="age != null">AND age &lt; #{age}</if>
<if test="age != null">AND age &gt; #{age}</if>

方式2:使用 CDATA(推荐,复杂SQL时使用)

<if test="minAge != null and maxAge != null">
    <![CDATA[
        AND age >= #{minAge} AND age <= #{maxAge}
    ]]>
</if>

<!-- CDATA 区域内的所有内容都被视为纯文本,不会被XML解析 -->
<if test="formula != null">
    <![CDATA[
        AND (price * quantity < 1000 OR discount > 0.5)
    ]]>
</if>

CDATA 说明:

  • <![CDATA[ 开始,]]> 结束
  • CDATA 区域内可以随意使用 <>& 等特殊字符
  • 适合包含多个比较运算符的复杂SQL

实际对比:

<!-- 方式1:全部转义(啰嗦但安全)-->
<select id="findUsers1" resultType="User">
    SELECT * FROM user
    WHERE age &gt;= 18 AND age &lt;= 60 AND score &gt; 80
</select>

<!-- 方式2:使用 CDATA(简洁清晰)-->
<select id="findUsers2" resultType="User">
    <![CDATA[
        SELECT * FROM user
        WHERE age >= 18 AND age <= 60 AND score > 80
    ]]>
</select>

<!-- 方式3:混合使用(不推荐,容易出错)-->
<select id="findUsers3" resultType="User">
    SELECT * FROM user
    WHERE age >= 18 AND age &lt;= 60 AND score > 80  <!-- 不一致,容易混淆 -->
</select>

总结:

  1. < 必须转义成 &lt;,否则XML解析报错
  2. > 可以不转义,但建议转义成 &gt;(统一规范)
  3. 简单SQL用转义,复杂SQL用CDATA
  4. 团队开发要统一规范(要么全转义,要么全用CDATA)

补充:属性值中的转义规则

在 MyBatis XML 中,属性值(如 test="...")也需要注意转义规则。

场景1:test 属性中的比较运算符

<!-- ✅ 正确:test 属性值中可以直接用 < 和 > -->
<if test="age != null and age > 18">
    AND age > 18
</if>

<if test="age != null and age < 60">
    AND age < 60
</if>

<if test="minAge != null and maxAge != null and minAge < maxAge">
    AND age BETWEEN #{minAge} AND #{maxAge}
</if>

重要:test 属性值中的 <> 不需要转义!

原因:

  • test 属性的值已经被引号包裹(test="..."
  • XML 解析器知道引号内的内容是属性值,不会当作标签解析
  • MyBatis 会正确处理属性值中的比较运算符

场景2:属性值中包含引号

<!-- ❌ 错误:属性值用双引号包裹,内部又有双引号 -->
<if test="username != null and username == "admin"">
    AND role = 'admin'
</if>

<!-- ✅ 方式1:外层用双引号,内层用单引号 -->
<if test="username != null and username == 'admin'">
    AND role = 'admin'
</if>

<!-- ✅ 方式2:使用转义 -->
<if test="username != null and username == &quot;admin&quot;">
    AND role = 'admin'
</if>

<!-- ✅ 方式3:外层用单引号,内层用双引号 -->
<if test='username != null and username == "admin"'>
    AND role = 'admin'
</if>

场景3:属性值中包含 & 符号

<!-- ❌ 错误:& 符号没有转义 -->
<if test="status != null && isActive">
    AND status = #{status}
</if>

<!-- ✅ 方式1:使用 and 代替 && -->
<if test="status != null and isActive">
    AND status = #{status}
</if>

<!-- ✅ 方式2:转义 & 符号 -->
<if test="status != null &amp;&amp; isActive">
    AND status = #{status}
</if>

属性值中的转义规则总结:

位置 符号 是否需要转义 说明
test 属性值中 < ❌ 不需要 引号内可以直接使用
test 属性值中 > ❌ 不需要 引号内可以直接使用
test 属性值中 & ✅ 需要(或用 and) &amp;and
test 属性值中 " ✅ 需要(或换引号) 外双内单,或转义 &quot;
test 属性值中 ' ✅ 需要(或换引号) 外单内双,或转义 &apos;
SQL 内容中 < ✅ 必须 &lt; 或 CDATA
SQL 内容中 > ⚠️ 建议 &gt; 或 CDATA

完整示例对比:

<!-- ========== test 属性值中的比较运算符 ========== -->

<!-- ✅ 正确:test 属性中的 < 和 > 不需要转义 -->
<select id="findUsers" resultType="User">
    SELECT * FROM user
    <where>
        <if test="minAge != null and minAge > 0">
            AND age >= #{minAge}
        </if>
        <if test="maxAge != null and maxAge < 150">
            AND age &lt;= #{maxAge}  <!-- SQL内容中的 < 需要转义 -->
        </if>
    </where>
</select>

<!-- ========== 复杂条件示例 ========== -->

<!-- ✅ 推荐:使用 and/or 代替 &&/|| -->
<if test="username != null and username != '' and age > 18">
    AND username = #{username} AND age > 18
</if>

<!-- ✅ 也可以:使用转义的 && -->
<if test="username != null &amp;&amp; username != '' &amp;&amp; age > 18">
    AND username = #{username} AND age > 18
</if>

<!-- ========== 字符串比较示例 ========== -->

<!-- ✅ 推荐:外双内单 -->
<if test="status != null and status == 'active'">
    AND status = 'active'
</if>

<!-- ✅ 也可以:外单内双 -->
<if test='status != null and status == "active"'>
    AND status = 'active'
</if>

<!-- ✅ 也可以:使用转义 -->
<if test="status != null and status == &quot;active&quot;">
    AND status = 'active'
</if>

最佳实践建议:

  1. test 属性中的比较运算符:直接使用 <>

    <if test="age > 18 and age < 60">...</if>
    
  2. test 属性中的逻辑运算符:使用 andor,不用 &&||

    <!-- ✅ 推荐 -->
    <if test="a != null and b != null or c > 0">...</if>
    
    <!-- ❌ 不推荐(需要转义,麻烦)-->
    <if test="a != null &amp;&amp; b != null || c > 0">...</if>
    
  3. test 属性中的字符串比较:外双内单

    <if test="status == 'active'">...</if>
    
  4. SQL 内容中的比较运算符:< 必须转义,> 建议转义

    AND age &lt; 60 AND score &gt; 80
    
  5. 复杂SQL:使用 CDATA

    <![CDATA[ AND age < 60 AND score > 80 ]]>
    

执行场景演示:

场景1:传入所有参数

// Mapper调用
findUsers("张三", 20, 30);

// 生成的SQL
SELECT * FROM user
WHERE username LIKE CONCAT('%', '张三', '%')
  AND age >= 20
  AND age <= 30

场景2:只传入 username

// Mapper调用
findUsers("李四", null, null);

// 生成的SQL
SELECT * FROM user
WHERE username LIKE CONCAT('%', '李四', '%')

场景3:只传入年龄范围

// Mapper调用
findUsers(null, 18, 60);

// 生成的SQL
SELECT * FROM user
WHERE age >= 18
  AND age <= 60

场景4:不传任何参数

// Mapper调用
findUsers(null, null, null);

// 生成的SQL
SELECT * FROM user
-- 注意:<where>标签智能处理,没有WHERE子句

常见判断条件:

<!-- 判断是否为null -->
<if test="id != null">
    AND id = #{id}
</if>

<!-- 判断字符串非空 -->
<if test="username != null and username != ''">
    AND username = #{username}
</if>

<!-- 判断数字大于0 -->
<if test="age != null and age > 0">
    AND age = #{age}
</if>

<!-- 判断集合非空 -->
<if test="ids != null and ids.size() > 0">
    AND id IN
    <foreach collection="ids" item="id" open="(" close=")" separator=",">
        #{id}
    </foreach>
</if>

<!-- 判断布尔值 -->
<if test="isDeleted != null and isDeleted">
    AND is_deleted = 1
</if>

6.2 choose-when-otherwise(类似switch)

作用: 多个条件中只选择一个执行(类似Java的switch-case)

基本语法:

<choose>
    <when test="条件1">SQL片段1</when>
    <when test="条件2">SQL片段2</when>
    <when test="条件3">SQL片段3</when>
    <otherwise>默认SQL片段</otherwise>
</choose>

完整示例:

<select id="findUsersByCondition" resultType="User">
    SELECT * FROM user
    <where>
        <choose>
            <when test="id != null">
                AND id = #{id}
            </when>
            <when test="username != null">
                AND username = #{username}
            </when>
            <when test="email != null">
                AND email = #{email}
            </when>
            <otherwise>
                AND status = 'active'
            </otherwise>
        </choose>
    </where>
</select>

详细解释:

  1. 执行规则:

    • 从上到下依次判断 <when> 条件
    • 只要有一个条件成立,执行对应的SQL,然后跳出(不再判断后续条件)
    • 如果所有 <when> 都不成立,执行 <otherwise>
    • <otherwise> 可以省略(相当于没有default分支)
  2. <if> 的区别:

    • <if>:可以同时满足多个条件,全部执行
    • <choose>:只执行第一个满足的条件,其他忽略

执行场景演示:

场景1:传入 id(优先级最高)

// Mapper调用
findUsersByCondition(1L, "张三", "test@example.com");

// 生成的SQL(只匹配id,忽略username和email)
SELECT * FROM user
WHERE id = 1

场景2:不传id,传入 username

// Mapper调用
findUsersByCondition(null, "张三", "test@example.com");

// 生成的SQL(匹配username,忽略email)
SELECT * FROM user
WHERE username = '张三'

场景3:只传入 email

// Mapper调用
findUsersByCondition(null, null, "test@example.com");

// 生成的SQL
SELECT * FROM user
WHERE email = 'test@example.com'

场景4:什么都不传(执行 otherwise)

// Mapper调用
findUsersByCondition(null, null, null);

// 生成的SQL
SELECT * FROM user
WHERE status = 'active'

实际应用场景:

示例1:按优先级排序

<!-- 优先级:精确ID > 模糊用户名 > 模糊邮箱 > 查询所有活跃用户 -->
<select id="searchUsers" resultType="User">
    SELECT * FROM user
    <where>
        <choose>
            <when test="id != null">
                id = #{id}  <!-- 最精确 -->
            </when>
            <when test="username != null and username != ''">
                username LIKE CONCAT('%', #{username}, '%')
            </when>
            <when test="email != null and email != ''">
                email LIKE CONCAT('%', #{email}, '%')
            </when>
            <otherwise>
                status = 'active'  <!-- 默认查询 -->
            </otherwise>
        </choose>
    </where>
</select>

示例2:不同排序策略

<select id="findUsersWithSort" resultType="User">
    SELECT * FROM user
    ORDER BY
    <choose>
        <when test="sortBy == 'age'">
            age DESC
        </when>
        <when test="sortBy == 'name'">
            username ASC
        </when>
        <when test="sortBy == 'createTime'">
            created_at DESC
        </when>
        <otherwise>
            id ASC  <!-- 默认按ID排序 -->
        </otherwise>
    </choose>
</select>

对比 if 和 choose:

使用 <if>(多个条件可同时生效):

<where>
    <if test="id != null">AND id = #{id}</if>
    <if test="username != null">AND username = #{username}</if>
</where>
<!-- 如果两个都传,生成:WHERE id = 1 AND username = '张三' -->

使用 <choose>(只生效一个):

<where>
    <choose>
        <when test="id != null">AND id = #{id}</when>
        <when test="username != null">AND username = #{username}</when>
    </choose>
</where>
<!-- 如果两个都传,生成:WHERE id = 1(只用id,忽略username)-->

6.3 set 标签(动态更新)

作用: 动态生成UPDATE语句的SET子句,自动处理逗号

基本语法:

<update id="updateXxx">
    UPDATE 表名
    <set>
        <if test="条件1">字段1 = #{值1},</if>
        <if test="条件2">字段2 = #{值2},</if>
    </set>
    WHERE id = #{id}
</update>

完整示例:

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

详细解释:

  1. <set> 标签的作用:

    • 自动添加 SET 关键字
    • 自动去除最后一个多余的逗号(这是核心功能)
    • 如果所有条件都不成立,不会生成SET子句(避免SQL错误)
  2. 为什么需要 <set> 标签?

不使用 <set> 的问题:

<!-- ❌ 错误示例 -->
<update id="updateUser">
    UPDATE user SET
    <if test="username != null">username = #{username},</if>
    <if test="email != null">email = #{email},</if>
    <if test="age != null">age = #{age},</if>  <!-- 最后一个逗号怎么处理? -->
    WHERE id = #{id}
</update>

<!-- 如果只传age,生成的SQL:-->
UPDATE user SET age = 25,  WHERE id = 1
                      ↑ 这个逗号会导致SQL语法错误!

使用 <set> 后:

<!-- ✅ 正确示例 -->
<update id="updateUser">
    UPDATE user
    <set>
        <if test="username != null">username = #{username},</if>
        <if test="email != null">email = #{email},</if>
        <if test="age != null">age = #{age},</if>
    </set>
    WHERE id = #{id}
</update>

<!-- 生成的SQL(<set>自动去除最后的逗号):-->
UPDATE user SET age = 25 WHERE id = 1
                        ↑ 逗号被自动去除了!

执行场景演示:

场景1:更新所有字段

// Mapper调用
User user = new User();
user.setId(1L);
user.setUsername("张三");
user.setEmail("zhangsan@example.com");
user.setAge(25);
updateUser(user);

// 生成的SQL
UPDATE user 
SET username = '张三', 
    email = 'zhangsan@example.com', 
    age = 25 
WHERE id = 1

场景2:只更新部分字段

// Mapper调用(只更新email)
User user = new User();
user.setId(1L);
user.setEmail("newemail@example.com");  // 只设置email
updateUser(user);

// 生成的SQL
UPDATE user 
SET email = 'newemail@example.com' 
WHERE id = 1

场景3:只更新一个字段

// Mapper调用(只更新age)
User user = new User();
user.setId(1L);
user.setAge(30);
updateUser(user);

// 生成的SQL(注意:最后没有逗号)
UPDATE user 
SET age = 30 
WHERE id = 1

实际应用场景:

示例1:复杂的动态更新

<update id="updateUserSelective">
    UPDATE user
    <set>
        <!-- 字符串字段:判断非空 -->
        <if test="username != null and username != ''">
            username = #{username},
        </if>
        <if test="email != null and email != ''">
            email = #{email},
        </if>
        
        <!-- 数字字段:判断不为null -->
        <if test="age != null">
            age = #{age},
        </if>
        
        <!-- 布尔字段 -->
        <if test="isVip != null">
            is_vip = #{isVip},
        </if>
        
        <!-- 更新时间:总是更新 -->
        updated_at = NOW(),
    </set>
    WHERE id = #{id}
</update>

示例2:批量更新某些字段

<update id="batchUpdateStatus">
    UPDATE user
    <set>
        status = #{status},
        updated_at = NOW()
    </set>
    WHERE id IN
    <foreach collection="ids" item="id" open="(" close=")" separator=",">
        #{id}
    </foreach>
</update>

示例3:根据条件决定更新哪些字段

<update id="updateUserByRole">
    UPDATE user
    <set>
        <!-- 管理员可以更新所有字段 -->
        <if test="role == 'admin'">
            <if test="username != null">username = #{username},</if>
            <if test="email != null">email = #{email},</if>
            <if test="status != null">status = #{status},</if>
        </if>
        
        <!-- 普通用户只能更新部分字段 -->
        <if test="role == 'user'">
            <if test="email != null">email = #{email},</if>
        </if>
        
        updated_at = NOW()
    </set>
    WHERE id = #{id}
</update>

与 trim 标签的对比:

使用 <set> 标签(推荐,简洁):

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

使用 <trim> 标签实现相同效果(更灵活但复杂):

<update id="updateUser">
    UPDATE user
    <trim prefix="SET" suffixOverrides=",">
        <if test="username != null">username = #{username},</if>
        <if test="email != null">email = #{email},</if>
    </trim>
    WHERE id = #{id}
</update>

注意事项:

  1. 每个字段赋值后都要加逗号,<set> 会自动去除最后的逗号
  2. 如果所有 <if> 条件都不满足,UPDATE语句可能无效,建议至少保留一个必更新字段(如 updated_at
  3. WHERE条件必须写在 <set> 标签外面

6.4 foreach 标签(批量操作)

作用: 遍历集合或数组,生成重复的SQL片段(如批量插入、IN查询)

基本语法:

<foreach collection="集合名称" item="每个元素的变量名" 
         separator="分隔符" open="开始符号" close="结束符号" index="索引变量">
    SQL片段(可使用 #{item} 访问当前元素)
</foreach>

属性说明:

属性 必填 说明 示例
collection ✅ 是 要遍历的集合名称 listarrayids(参数名)
item ✅ 是 当前元素的变量名 useriditem
separator ❌ 否 每个元素之间的分隔符 ,ORAND
open ❌ 否 整个循环开始前添加的字符 (VALUES
close ❌ 否 整个循环结束后添加的字符 );
index ❌ 否 当前元素的索引(从0开始) iidx

示例1:批量插入

<!-- 批量插入 -->
<insert id="batchInsert" parameterType="list">
    INSERT INTO user (username, email, age)
    VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.username}, #{user.email}, #{user.age})
    </foreach>
</insert>

详细解释:

  • collection="list":遍历参数中的 list 集合
  • item="user":每次遍历的元素命名为 user
  • separator=",":每个元素之间用逗号分隔
  • #{user.username}:访问当前 user 对象的 username 属性

执行场景:

// Mapper调用
List<User> users = Arrays.asList(
    new User("张三", "zhangsan@example.com", 25),
    new User("李四", "lisi@example.com", 30),
    new User("王五", "wangwu@example.com", 28)
);
batchInsert(users);

// 生成的SQL
INSERT INTO user (username, email, age)
VALUES
    ('张三', 'zhangsan@example.com', 25),
    ('李四', 'lisi@example.com', 30),
    ('王五', 'wangwu@example.com', 28)

示例2:IN 查询

<!-- IN 查询 -->
<select id="findByIds" resultType="User">
    SELECT * FROM user
    WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

详细解释:

  • collection="ids":遍历参数名为 ids 的集合
  • item="id":每个元素命名为 id
  • open="(":循环开始前加左括号
  • close=")":循环结束后加右括号
  • separator=",":元素之间用逗号分隔

执行场景:

// Mapper接口定义
List<User> findByIds(@Param("ids") List<Long> ids);

// 调用
List<Long> ids = Arrays.asList(1L, 3L, 5L, 7L);
List<User> users = findByIds(ids);

// 生成的SQL
SELECT * FROM user
WHERE id IN (1, 3, 5, 7)
              ↑      ↑
            open   close

示例3:批量删除

<!-- 批量删除 -->
<delete id="batchDelete">
    DELETE FROM user WHERE id IN
    <foreach collection="array" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</delete>

详细解释:

  • collection="array":遍历数组类型的参数

执行场景:

// Mapper接口定义(参数是数组)
int batchDelete(Long[] ids);

// 调用
Long[] ids = {1L, 2L, 3L};
batchDelete(ids);

// 生成的SQL
DELETE FROM user WHERE id IN (1, 2, 3)

示例4:动态拼接多个OR条件

<select id="searchUsers" resultType="User">
    SELECT * FROM user
    <where>
        <foreach collection="keywords" item="keyword" separator="OR">
            username LIKE CONCAT('%', #{keyword}, '%')
        </foreach>
    </where>
</select>

执行场景:

// Mapper调用
List<String> keywords = Arrays.asList("张", "李", "王");
List<User> users = searchUsers(keywords);

// 生成的SQL
SELECT * FROM user
WHERE username LIKE CONCAT('%', '张', '%')
   OR username LIKE CONCAT('%', '李', '%')
   OR username LIKE CONCAT('%', '王', '%')

示例5:使用 index 属性

Mapper 接口:

// 方式1:不使用 @Param(单个参数)
int batchInsertWithIndex(List<User> users);

// 方式2:使用 @Param(推荐)
int batchInsertWithIndex(@Param("userList") List<User> users);

XML 配置:

<!-- 方式1:不使用 @Param 时,collection 必须写 "list" -->
<insert id="batchInsertWithIndex">
    INSERT INTO user (username, email, age, sort_order)
    VALUES
    <foreach collection="list" item="user" index="i" separator=",">
        (#{user.username}, #{user.email}, #{user.age}, #{i})
    </foreach>
</insert>

<!-- 方式2:使用 @Param 时,collection 写 @Param 指定的名称 -->
<insert id="batchInsertWithIndex">
    INSERT INTO user (username, email, age, sort_order)
    VALUES
    <foreach collection="userList" item="user" index="i" separator=",">
        (#{user.username}, #{user.email}, #{user.age}, #{i})
    </foreach>
</insert>

💡 重要区别:Java 的 List 和 collection="list"

这两个 list 不是同一个含义

1. Java 中的 List<User>

List<User> users = Arrays.asList(user1, user2, user3);
//  ↑
// 这是 Java 的集合类型,表示一个 User 对象的列表
  • 这是 Java 的类型声明
  • Listjava.util.List 接口
  • <User> 是泛型,表示列表中元素的类型

2. XML 中的 collection="list"

<foreach collection="list" item="user">
<!--              ↑
     这是 MyBatis 的默认参数名,不是 Java 类型
-->
  • 这是 MyBatis 的参数名
  • 当方法参数是单个 List 类型且没有 @Param 时,MyBatis 默认把它命名为 "list"
  • 这是一个字符串,用来引用参数

详细对比:

Java 接口 参数类型 XML collection 属性 说明
batchInsert(List<User> users) List<User> collection="list" 单个List参数,默认名 "list"
batchInsert(@Param("users") List<User> users) List<User> collection="users" 用 @Param 指定名称
batchInsert(User[] users) User[] collection="array" 数组参数,默认名 "array"
batchInsert(@Param("ids") List<Long> ids) List<Long> collection="ids" 用 @Param 指定名称

工作流程图解:

Java 代码调用:
userMapper.batchInsert(Arrays.asList(user1, user2));
                       ↓
                传入一个 List<User> 类型的参数
                       ↓
MyBatis 内部处理:
- 检测到参数类型是 List
- 没有 @Param 注解
- 自动给这个参数命名为 "list"
                       ↓
XML 中通过名称引用:
<foreach collection="list">  ← 这里的 "list" 是参数的名字
         ↑
    通过名字 "list" 找到传入的 List<User> 对象

常见错误示例:

// Mapper 接口(使用了 @Param)
int batchInsert(@Param("users") List<User> users);
<!-- ❌ 错误:使用了 @Param("users"),但 collection 还写 "list" -->
<foreach collection="list" item="user">
    <!-- 报错:Parameter 'list' not found -->
</foreach>

<!-- ✅ 正确:collection 要和 @Param 的值一致 -->
<foreach collection="users" item="user">
    (#{user.username}, #{user.email})
</foreach>

执行场景:

// Mapper调用
List<User> users = Arrays.asList(user1, user2, user3);
//  ↑ 这是 Java 的 List 类型
batchInsertWithIndex(users);

// 在 MyBatis 内部,这个 List 被命名为 "list"(如果没有 @Param)
// 所以 XML 中 collection="list" 才能找到这个参数

// 生成的SQL(index从0开始)
INSERT INTO user (username, email, age, sort_order)
VALUES
    ('张三', 'zhangsan@example.com', 25, 0),
    ('李四', 'lisi@example.com', 30, 1),
    ('王五', 'wangwu@example.com', 28, 2)

MyBatis 默认参数名规则:

参数类型 没有 @Param 时的默认名 示例
List "list" collection="list"
数组 "array" collection="array"
Map "map" 键名直接访问
其他单个对象 对象的属性名 #{username}
多个参数(无@Param) param1, param2, arg0, arg1 #{param1}, #{arg0}

最佳实践建议:

// ✅ 推荐:总是使用 @Param,清晰明了
int batchInsert(@Param("users") List<User> users);
int batchInsert(@Param("ids") List<Long> ids);
<!-- ✅ 推荐:collection 使用有意义的名称 -->
<foreach collection="users" item="user">
    ...
</foreach>

<foreach collection="ids" item="id">
    ...
</foreach>
// ❌ 不推荐:依赖默认名称 "list",不够清晰
int batchInsert(List<User> users);
<!-- ❌ 不推荐:collection="list" 不够语义化 -->
<foreach collection="list" item="user">
    ...
</foreach>

总结:

  1. Java 的 List<User> = 类型声明
  2. XML 的 collection="list" = 参数名(MyBatis 的默认命名)
  3. 它们是两个不同层面的概念
  4. 建议使用 @Param 明确指定参数名,避免混淆

示例6:遍历Map

<select id="findByMap" resultType="User">
    SELECT * FROM user
    <where>
        <foreach collection="params" item="value" index="key" separator="AND">
            ${key} = #{value}
        </foreach>
    </where>
</select>

💡 为什么这里使用 ${key} 而不是 #{key}

这是一个关键问题,涉及 MyBatis 中 #{}${} 的核心区别!

答案:key 是字段名,必须直接拼接到 SQL 中,不能用预编译参数。

#{} vs ${} 的本质区别:

特性 #{}(推荐) ${}(谨慎使用)
实现方式 预编译(PreparedStatement) 字符串拼接
SQL注入 ✅ 安全(自动转义) ❌ 不安全(可能注入)
使用场景 参数值(WHERE条件、INSERT值等) 表名、字段名、ORDER BY
生成SQL 使用 ? 占位符 直接替换为实际值
类型处理 自动处理类型转换 原样拼接

详细对比:

1. #{} - 预编译参数(安全,推荐)

<!-- 使用 #{} -->
<select id="findByUsername" resultType="User">
    SELECT * FROM user WHERE username = #{username}
</select>
// Java 调用
findByUsername("张三");

// 生成的预编译SQL(使用 ? 占位符)
SELECT * FROM user WHERE username = ?

// 执行时 MyBatis 会:
// 1. 准备 PreparedStatement
// 2. 设置参数:setString(1, "张三")
// 3. 执行SQL
// 结果:安全,防止SQL注入

2. ${} - 字符串替换(不安全,慎用)

<!-- 使用 ${} -->
<select id="findByUsername" resultType="User">
    SELECT * FROM user WHERE username = '${username}'
</select>
// Java 调用
findByUsername("张三");

// 生成的SQL(直接字符串替换)
SELECT * FROM user WHERE username = '张三'

// ⚠️ 如果传入恶意参数
findByUsername("' OR '1'='1");

// 生成的SQL(SQL注入!)
SELECT * FROM user WHERE username = '' OR '1'='1'
-- 这会返回所有用户!

为什么示例6必须用 ${key}

<foreach collection="params" item="value" index="key">
    ${key} = #{value}
    <!-- ↑字段名    ↑参数值 -->
</foreach>

原因分析:

// Map 的内容
params.put("username", "张三");
params.put("age", 25);

期望生成的SQL:

WHERE username = '张三' AND age = 25
      ↑字段名       ↑值      ↑字段名 ↑值

如果使用 #{key}(错误):

-- 错误的SQL
WHERE ? = '张三' AND ? = 25
--    ↑ 字段名不能是占位符!

-- 这是无效的SQL,数据库会报错
-- 字段名必须是明确的标识符,不能是参数

如果使用 ${key}(正确):

-- 正确的SQL
WHERE username = '张三' AND age = 25
--    ↑ 字段名直接拼接进SQL

完整对比示例:

<!-- 示例1:字段名用 ${},值用 #{} -->
<foreach collection="params" item="value" index="key">
    ${key} = #{value}  <!-- ✅ 正确 -->
</foreach>

<!-- 示例2:都用 #{} -->
<foreach collection="params" item="value" index="key">
    #{key} = #{value}  <!-- ❌ 错误:字段名不能是 ? -->
</foreach>

<!-- 示例3:都用 ${} -->
<foreach collection="params" item="value" index="key">
    ${key} = '${value}'  <!-- ⚠️ 不安全:值有SQL注入风险 -->
</foreach>

执行场景:

// Mapper调用
Map<String, Object> params = new HashMap<>();
params.put("username", "张三");
params.put("age", 25);
params.put("status", "active");
findByMap(params);

// 生成的SQL(${key} 被替换为字段名,#{value} 被替换为 ?)
SELECT * FROM user
WHERE username = ?    -- 参数1: "张三"
  AND age = ?         -- 参数2: 25
  AND status = ?      -- 参数3: "active"

什么时候必须用 ${}

场景 示例 原因
动态表名 SELECT * FROM ${tableName} 表名不能是参数
动态字段名 ORDER BY ${orderColumn} 字段名不能是参数
动态排序 ORDER BY id ${sortOrder} ASC/DESC不能是参数
IN 子句(旧版) WHERE id IN (${ids}) 现代应用用 <foreach> 代替

⚠️ 使用 ${} 的安全建议:

  1. 严格验证输入
// ✅ 推荐:白名单验证
public List<User> findByColumn(String column, Object value) {
    // 验证字段名是否在允许的列表中
    if (!Arrays.asList("username", "age", "status").contains(column)) {
        throw new IllegalArgumentException("Invalid column: " + column);
    }
    return userMapper.findByColumn(column, value);
}
  1. 避免用户直接输入
// ❌ 危险:直接使用用户输入
String userInput = request.getParameter("column");
findByColumn(userInput, value);  // SQL注入风险!

// ✅ 安全:使用枚举或映射
Map<String, String> columnMap = Map.of(
    "name", "username",
    "userAge", "age"
);
String column = columnMap.get(request.getParameter("field"));
if (column != null) {
    findByColumn(column, value);
}
  1. 优先使用其他方案
<!-- ❌ 不推荐:动态字段名 -->
<select id="findByColumn" resultType="User">
    SELECT * FROM user WHERE ${column} = #{value}
</select>

<!-- ✅ 推荐:使用 <choose> 明确指定 -->
<select id="findByColumn" resultType="User">
    SELECT * FROM user
    <where>
        <choose>
            <when test="column == 'username'">
                AND username = #{value}
            </when>
            <when test="column == 'age'">
                AND age = #{value}
            </when>
            <when test="column == 'status'">
                AND status = #{value}
            </when>
        </choose>
    </where>
</select>

总结:

  1. ${key} 用于字段名:因为字段名必须是SQL标识符,不能是参数
  2. #{value} 用于参数值:安全的预编译方式,防止SQL注入
  3. 原则:能用 #{} 就不用 ${}
  4. 必须用 ${} 时,要严格验证输入,防止SQL注入

collection 参数的不同取值:

参数类型 collection 取值 说明
List<User> list list 单参数List,默认名称是 list
User[] array array 单参数数组,默认名称是 array
@Param("ids") List<Long> ids ids 使用 @Param 指定的名称
Map<String, Object> map map 或 map 的key 遍历Map

完整示例对比:

不使用 @Param

// Mapper接口
List<User> findByIds(List<Long> ids);

// XML配置
<foreach collection="list" item="id">  <!-- 必须用 list -->
    #{id}
</foreach>

使用 @Param(推荐):

// Mapper接口
List<User> findByIds(@Param("ids") List<Long> ids);

// XML配置
<foreach collection="ids" item="id">  <!-- 使用参数名 ids -->
    #{id}
</foreach>

性能注意事项:

  1. 批量插入优化:
<!-- ✅ 推荐:一次性插入多条(高效)-->
INSERT INTO user (username, email) VALUES
    ('张三', 'a@example.com'),
    ('李四', 'b@example.com')

<!-- ❌ 不推荐:多次单条插入(效率低)-->
INSERT INTO user (username, email) VALUES ('张三', 'a@example.com');
INSERT INTO user (username, email) VALUES ('李四', 'b@example.com');
  1. IN 查询数量限制:
// ⚠️ 警告:IN 的参数不宜过多(建议 < 1000)
// WHERE id IN (1, 2, 3, ..., 10000)  ← 可能导致SQL过长或性能问题

// ✅ 建议:分批查询
List<Long> ids = ...; // 10000个ID
List<User> allUsers = new ArrayList<>();
for (int i = 0; i < ids.size(); i += 500) {
    List<Long> batch = ids.subList(i, Math.min(i + 500, ids.size()));
    allUsers.addAll(findByIds(batch));
}
  1. 批量插入数量限制:
// ⚠️ 批量插入建议每次 < 500 条,避免SQL过长
if (users.size() > 500) {
    // 分批插入
    for (int i = 0; i < users.size(); i += 500) {
        List<User> batch = users.subList(i, Math.min(i + 500, users.size()));
        batchInsert(batch);
    }
}

6.5 trim 标签(更灵活的拼接)

作用: 灵活处理SQL片段的前缀、后缀,以及去除多余的分隔符(最强大的拼接标签)

基本语法:

<trim prefix="前缀" suffix="后缀" 
      prefixOverrides="要去除的前缀" suffixOverrides="要去除的后缀">
    SQL片段
</trim>

属性说明:

属性 说明 示例
prefix 在整个SQL片段前添加的内容 WHERESET(
suffix 在整个SQL片段后添加的内容 );
prefixOverrides 去除SQL片段开头的指定字符 AND OR ,
suffixOverrides 去除SQL片段结尾的指定字符 ,AND OR

注意: prefixOverridessuffixOverrides 中的空格和竖线 | 都有意义!

完整示例:

<select id="findUsers" resultType="User">
    SELECT * FROM user
    <trim prefix="WHERE" prefixOverrides="AND |OR ">
        <if test="username != null">
            AND username = #{username}
        </if>
        <if test="email != null">
            AND email = #{email}
        </if>
    </trim>
</select>

详细解释:

  1. prefix="WHERE":如果 trim 内容不为空,在前面加 WHERE
  2. prefixOverrides="AND |OR ":去除开头的 AND OR (注意有空格)
    • AND |OR 表示:AND OR (竖线 | 是"或"的意思)
    • 空格很重要:AND 会匹配 "AND " 但不会匹配 "AND"

执行场景演示:

场景1:传入两个条件

// Mapper调用
findUsers("张三", "test@example.com");

// 生成的SQL
SELECT * FROM user
WHERE username = '张三'  -- 开头的 "AND " 被去除了
  AND email = 'test@example.com'

场景2:只传入第二个条件

// Mapper调用
findUsers(null, "test@example.com");

// 生成的SQL
SELECT * FROM user
WHERE email = 'test@example.com'  -- 开头的 "AND " 被去除

场景3:不传任何条件

// Mapper调用
findUsers(null, null);

// 生成的SQL
SELECT * FROM user
-- 没有 WHERE 子句(trim内容为空)

实际应用场景:

示例1:模拟 <where> 标签

<!-- 使用 <where> 标签(简洁)-->
<select id="findUsers1" resultType="User">
    SELECT * FROM user
    <where>
        <if test="username != null">AND username = #{username}</if>
        <if test="email != null">AND email = #{email}</if>
    </where>
</select>

<!-- 使用 <trim> 实现相同效果(更灵活)-->
<select id="findUsers2" resultType="User">
    SELECT * FROM user
    <trim prefix="WHERE" prefixOverrides="AND |OR ">
        <if test="username != null">AND username = #{username}</if>
        <if test="email != null">AND email = #{email}</if>
    </trim>
</select>

示例2:模拟 <set> 标签

<!-- 使用 <set> 标签(简洁)-->
<update id="updateUser1">
    UPDATE user
    <set>
        <if test="username != null">username = #{username},</if>
        <if test="email != null">email = #{email},</if>
    </set>
    WHERE id = #{id}
</update>

<!-- 使用 <trim> 实现相同效果(更灵活)-->
<update id="updateUser2">
    UPDATE user
    <trim prefix="SET" suffixOverrides=",">
        <if test="username != null">username = #{username},</if>
        <if test="email != null">email = #{email},</if>
    </trim>
    WHERE id = #{id}
</update>

示例3:动态生成 IN 子句

<select id="findByConditions" resultType="User">
    SELECT * FROM user
    WHERE status = 'active'
    <trim prefix="AND id IN" prefixOverrides="," suffix=")">
        <if test="ids != null and ids.size() > 0">
            (
            <foreach collection="ids" item="id" separator=",">
                #{id}
            </foreach>
        </if>
    </trim>
</select>

执行场景:

// 传入ID列表
findByConditions(Arrays.asList(1L, 2L, 3L));

// 生成的SQL
SELECT * FROM user
WHERE status = 'active'
  AND id IN (1, 2, 3)

// 不传ID
findByConditions(null);

// 生成的SQL
SELECT * FROM user
WHERE status = 'active'
-- 没有 AND id IN 部分

示例4:复杂的 OR 条件

<select id="complexSearch" resultType="User">
    SELECT * FROM user
    <trim prefix="WHERE" prefixOverrides="AND |OR ">
        <if test="id != null">
            AND id = #{id}
        </if>
        <trim prefix="AND (" suffix=")" prefixOverrides="OR ">
            <if test="username != null">
                OR username LIKE CONCAT('%', #{username}, '%')
            </if>
            <if test="email != null">
                OR email LIKE CONCAT('%', #{email}, '%')
            </if>
        </trim>
    </trim>
</select>

执行场景:

// Mapper调用
complexSearch(null, "张三", "test@example.com");

// 生成的SQL
SELECT * FROM user
WHERE (
    username LIKE CONCAT('%', '张三', '%')  -- 开头的 OR 被去除
    OR email LIKE CONCAT('%', 'test@example.com', '%')
)

prefixOverridessuffixOverrides 的匹配规则:

<!-- 示例1:去除 "AND " 和 "OR "(注意空格)-->
<trim prefixOverrides="AND |OR ">
    AND username = 'test'  <!-- 匹配成功,去除 "AND " -->
    OR email = 'test'      <!-- 匹配成功,去除 "OR " -->
    ANDOR status = 'active' <!-- 不匹配(没有空格)-->
</trim>
<!-- 示例2:去除逗号 -->
<trim suffixOverrides=",">
    username = 'test',
    email = 'test',  <!-- 最后的逗号会被去除 -->
</trim>
<!-- 示例3:去除多个可能的后缀 -->
<trim suffixOverrides=", |AND |OR ">
    username = 'test',   <!-- 去除逗号+空格 -->
    email = 'test' AND   <!-- 去除 " AND" -->
    age = 25 OR          <!-- 去除 " OR" -->
</trim>

标签对比总结:

标签 适用场景 灵活性 推荐度
<where> 动态WHERE条件 低(固定功能) ⭐⭐⭐⭐⭐(最常用)
<set> 动态UPDATE 低(固定功能) ⭐⭐⭐⭐⭐(最常用)
<trim> 任意动态拼接 高(完全自定义) ⭐⭐⭐(复杂场景)

选择建议:

  • ✅ 优先使用 <where><set>(简洁明了)
  • ✅ 需要自定义前缀/后缀时才用 <trim>(更灵活但复杂)
  • <trim> 可以实现 <where><set> 的所有功能,但代码可读性较差

完整示例:综合使用

<select id="advancedSearch" resultType="User">
    SELECT * FROM user
    <trim prefix="WHERE" prefixOverrides="AND |OR ">
        <!-- 基础条件 -->
        <if test="status != null">
            AND status = #{status}
        </if>
        
        <!-- 复杂OR条件组 -->
        <if test="keyword != null and keyword != ''">
            AND (
                username LIKE CONCAT('%', #{keyword}, '%')
                OR email LIKE CONCAT('%', #{keyword}, '%')
            )
        </if>
        
        <!-- 时间范围 -->
        <if test="startDate != null">
            AND created_at >= #{startDate}
        </if>
        <if test="endDate != null">
            AND created_at &lt;= #{endDate}
        </if>
        
        <!-- 年龄范围 -->
        <trim prefix="AND age" prefixOverrides="IN ">
            <if test="ages != null and ages.size() > 0">
                IN
                <foreach collection="ages" item="age" open="(" close=")" separator=",">
                    #{age}
                </foreach>
            </if>
        </trim>
    </trim>
    ORDER BY created_at DESC
</select>

总结要点:

  1. <trim> 是最灵活的动态SQL标签,可以替代 <where><set>
  2. prefixOverrides 去除开头多余内容,suffixOverrides 去除结尾多余内容
  3. 竖线 | 表示"或"关系,可以匹配多个可能的字符
  4. 空格很重要"AND " 只匹配 "AND加空格"
  5. 优先使用专用标签<where><set>),复杂场景再考虑 <trim>

7. 结果映射

💡 如果不加 resultType 或 resultMap 会怎么样?

核心规则:

  • <select> 语句:必须指定 resultType 或 resultMap(否则报错)
  • <insert><update><delete> 语句:不需要 resultType/resultMap(返回受影响行数)

情况1:<select> 不加 resultType/resultMap

<!-- ❌ 错误示例 -->
<select id="findById">
    SELECT * FROM user WHERE id = #{id}
</select>

结果:编译或运行时报错!

org.apache.ibatis.executor.ExecutorException: 
A query was run and no Result Maps were found for the Mapped Statement 
'com.example.demo.mapper.UserMapper.findById'.

原因:

  • MyBatis 不知道如何将查询结果映射到 Java 对象
  • 必须明确指定返回类型

正确做法:

<!-- ✅ 方式1:使用 resultType -->
<select id="findById" resultType="User">
    SELECT * FROM user WHERE id = #{id}
</select>

<!-- ✅ 方式2:使用 resultMap -->
<select id="findById" resultMap="UserResultMap">
    SELECT * FROM user WHERE id = #{id}
</select>

情况2:<insert><update><delete> 不加 resultType/resultMap

<!-- ✅ 正确:增删改操作不需要 resultType/resultMap -->
<insert id="insert">
    INSERT INTO user (username, email, age) 
    VALUES (#{username}, #{email}, #{age})
</insert>

<update id="update">
    UPDATE user SET username = #{username} WHERE id = #{id}
</update>

<delete id="deleteById">
    DELETE FROM user WHERE id = #{id}
</delete>

返回值:

  • 默认返回 intlong(受影响的行数)
  • Mapper 接口方法的返回类型通常是 int
// Mapper 接口
public interface UserMapper {
    int insert(User user);      // 返回插入的行数(通常是1)
    int update(User user);      // 返回更新的行数
    int deleteById(Long id);    // 返回删除的行数
}

执行示例:

// 插入1条记录
int rows = userMapper.insert(user);
System.out.println(rows);  // 输出:1

// 批量删除3条记录
int deletedRows = userMapper.deleteByStatus("inactive");
System.out.println(deletedRows);  // 输出:3

// 更新操作,但没有记录被更新(WHERE条件不匹配)
int updatedRows = userMapper.updateById(999L);
System.out.println(updatedRows);  // 输出:0

情况3:<select> 返回值类型与 resultType 不匹配

<!-- Mapper 接口定义 -->
List<User> findAll();

<!-- ❌ 错误:返回类型应该是集合,但 resultType 写的是 List -->
<select id="findAll" resultType="List">
    SELECT * FROM user
</select>

<!-- ✅ 正确:resultType 指定集合的元素类型,不是集合本身 -->
<select id="findAll" resultType="User">
    SELECT * FROM user
</select>

重要规则:

  • Mapper 接口方法返回 List<User>
  • XML 中 resultType 写 User(元素类型),不是 List
  • MyBatis 会自动根据方法签名判断是返回单个对象还是集合

对应关系:

Mapper 方法返回类型 XML resultType 说明
User findById(Long id) User 返回单个对象
List<User> findAll() User 返回集合,resultType写元素类型
Map<String, Object> findAsMap() maphashmap 返回Map
List<Map<String, Object>> findAllAsMap() map 返回Map集合
int count() int 或不写 返回基本类型
Long countUsers() long 或不写 返回基本类型

情况4:特殊的返回类型

返回 Map:

<!-- Mapper 接口 -->
Map<String, Object> findByIdAsMap(@Param("id") Long id);

<!-- XML 配置 -->
<select id="findByIdAsMap" resultType="map">
    SELECT id, username, email, age FROM user WHERE id = #{id}
</select>

返回基本类型:

<!-- Mapper 接口 -->
int count();
Long maxId();
String findUsernameById(@Param("id") Long id);

<!-- XML 配置 -->
<select id="count" resultType="int">
    SELECT COUNT(*) FROM user
</select>

<select id="maxId" resultType="long">
    SELECT MAX(id) FROM user
</select>

<select id="findUsernameById" resultType="string">
    SELECT username FROM user WHERE id = #{id}
</select>

MyBatis 内置的类型别名:

Java 类型 MyBatis 别名
int / Integer int, integer
long / Long long
String string
boolean / Boolean boolean
float / Float float
double / Double double
BigDecimal bigdecimal
Date date
Map map, hashmap
List list, arraylist

情况5:同时指定 resultType 和 resultMap

<!-- ❌ 错误:不能同时使用 -->
<select id="findById" resultType="User" resultMap="UserResultMap">
    SELECT * FROM user WHERE id = #{id}
</select>

结果:报错!

Cannot specify both resultType and resultMap in the same statement

正确做法:只能二选一

<!-- ✅ 方式1:简单映射用 resultType -->
<select id="findById" resultType="User">
    SELECT * FROM user WHERE id = #{id}
</select>

<!-- ✅ 方式2:复杂映射用 resultMap -->
<select id="findById" resultMap="UserResultMap">
    SELECT * FROM user WHERE id = #{id}
</select>

总结对比表:

操作类型 是否需要 resultType/resultMap 返回值 示例
<select> 必须 对象、集合、基本类型、Map resultType="User"
<insert> ❌ 不需要 受影响行数(int) 插入1条返回1
<update> ❌ 不需要 受影响行数(int) 更新3条返回3
<delete> ❌ 不需要 受影响行数(int) 删除5条返回5

选择 resultType 还是 resultMap?

场景 推荐使用 原因
字段名一致(或开启驼峰转换) resultType 简单自动映射
字段名不一致 resultMap 手动指定映射关系
一对一关联 resultMap + <association> 需要嵌套映射
一对多关联 resultMap + <collection> 需要集合映射
多对多关联 resultMap + <collection> 需要复杂映射
返回 Map resultType="map" 动态列

7.1 自动映射(字段名一致)

<!-- 当数据库字段和实体类属性完全一致时 -->
<select id="findById" resultType="User">
    SELECT id, username, email, age FROM user WHERE id = #{id}
</select>

7.2 ResultMap(自定义映射)

<resultMap id="UserResultMap" type="com.example.demo.domain.User">
    <!-- id: 主键映射 -->
    <id property="id" column="user_id"/>
    
    <!-- result: 普通字段映射 -->
    <result property="username" column="user_name"/>
    <result property="email" column="user_email"/>
    <result property="age" column="user_age"/>
    <result property="createdAt" column="gmt_create"/>
</resultMap>

<select id="findById" resultMap="UserResultMap">
    SELECT user_id, user_name, user_email, user_age, gmt_create
    FROM t_user
    WHERE user_id = #{id}
</select>

7.3 一对一映射(association)

<!-- 用户信息表 -->
<resultMap id="UserDetailMap" type="com.example.demo.domain.User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    
    <!-- association: 一对一关联 -->
    <association property="profile" javaType="com.example.demo.domain.UserProfile">
        <id property="id" column="profile_id"/>
        <result property="realName" column="real_name"/>
        <result property="phone" column="phone"/>
        <result property="address" column="address"/>
    </association>
</resultMap>

<select id="findUserWithProfile" resultMap="UserDetailMap">
    SELECT 
        u.id, u.username,
        p.id AS profile_id, p.real_name, p.phone, p.address
    FROM user u
    LEFT JOIN user_profile p ON u.id = p.user_id
    WHERE u.id = #{id}
</select>

7.4 一对多映射(collection)

<resultMap id="UserWithOrdersMap" type="com.example.demo.domain.User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    
    <!-- collection: 一对多关联 -->
    <collection property="orders" ofType="com.example.demo.domain.Order">
        <id property="id" column="order_id"/>
        <result property="orderNo" column="order_no"/>
        <result property="totalAmount" column="total_amount"/>
        <result property="status" column="status"/>
    </collection>
</resultMap>

<select id="findUserWithOrders" resultMap="UserWithOrdersMap">
    SELECT 
        u.id, u.username,
        o.id AS order_id, o.order_no, o.total_amount, o.status
    FROM user u
    LEFT JOIN `order` o ON u.id = o.user_id
    WHERE u.id = #{id}
</select>

7.5 多对多映射(collection + 中间表)

场景:学生和课程的关系

  • 一个学生可以选多门课程
  • 一门课程可以被多个学生选择

数据库设计(三张表):

-- 学生表
CREATE TABLE student (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50)
);

-- 课程表
CREATE TABLE course (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50)
);

-- 中间表(关联表)
CREATE TABLE student_course (
    student_id BIGINT,
    course_id BIGINT,
    PRIMARY KEY (student_id, course_id)
);

Java 实体类:

// 学生类
public class Student {
    private Long id;
    private String name;
    private List<Course> courses;  // ← 多对多:集合
}

// 课程类
public class Course {
    private Long id;
    private String name;
    private List<Student> students;  // ← 反向关系(可选)
}

MyBatis 配置:

<resultMap id="StudentWithCoursesMap" type="com.example.demo.domain.Student">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    
    <!-- collection: 多对多也用 collection -->
    <collection property="courses" ofType="com.example.demo.domain.Course">
        <id property="id" column="course_id"/>
        <result property="name" column="course_name"/>
    </collection>
</resultMap>

<select id="findStudentWithCourses" resultMap="StudentWithCoursesMap">
    SELECT 
        s.id, s.name,
        c.id AS course_id, c.name AS course_name
    FROM student s
    LEFT JOIN student_course sc ON s.id = sc.student_id  -- ← 第一次 JOIN:连接中间表
    LEFT JOIN course c ON sc.course_id = c.id            -- ← 第二次 JOIN:连接目标表
    WHERE s.id = #{id}
</select>

🎯 关系映射完整规律总结

📌 一、从数据库角度理解

关系类型 外键位置 表的数量 示例
一对一 A表或B表(任一方) 2张表 用户 ↔ 用户档案
一对多 多的一方(B表) 2张表 用户 ↔ 订单
多对多 独立的中间表 3张表 学生 ↔ 课程

规律:

  • 一对一/一对多:2张表,直接 JOIN
  • 多对多:3张表(需要中间表),JOIN 两次

📌 二、从 Java 实体类角度理解

关系类型 属性类型 记忆技巧
一对一 Profile profile; 单个对象 - 一个人一张身份证
一对多 List<Order> orders; 集合 - 一个人多个订单
多对多 List<Course> courses; 集合 - 一个学生多门课

规律:

// 看属性类型就知道用什么标签
private Profile profile;        // ← 单个对象 → association
private List<Order> orders;     // ← 集合    → collection
private List<Course> courses;   // ← 集合    → collection

📌 三、从 MyBatis 配置角度理解

关系类型 标签 类型属性 完整写法
一对一 <association> javaType="对象类" <association property="profile" javaType="Profile">
一对多 <collection> ofType="元素类" <collection property="orders" ofType="Order">
多对多 <collection> ofType="元素类" <collection property="courses" ofType="Course">

规律:

  1. association = 单个关联 → 用 javaType(Java类型)
  2. collection = 集合 → 用 ofType(集合里元素的类型)

记忆口诀:

一对一 → association → javaType    (单个Java对象)
一对多 → collection  → ofType      (集合Of某类型)
多对多 → collection  → ofType      (集合Of某类型)

📌 四、从 SQL 角度理解

关系类型 JOIN 次数 SQL 模式
一对一 1次 JOIN FROM A LEFT JOIN B ON A.id = B.a_id
一对多 1次 JOIN FROM A LEFT JOIN B ON A.id = B.a_id
多对多 2次 JOIN FROM A LEFT JOIN AB ON A.id = AB.a_id
LEFT JOIN B ON AB.b_id = B.id

规律:

  • 一对一/一对多:只连接目标表(1次 JOIN)
  • 多对多:先连中间表,再连目标表(2次 JOIN)

📌 五、完整对比示例

1️⃣ 一对一(User → Profile)

// 实体类
public class User {
    private Long id;
    private String username;
    private Profile profile;  // ← 单个对象
}
<!-- MyBatis 配置 -->
<resultMap id="UserDetailMap" type="User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <association property="profile" javaType="Profile">  <!-- ← association + javaType -->
        <id property="id" column="profile_id"/>
        <result property="phone" column="phone"/>
    </association>
</resultMap>

<!-- SQL:1次 JOIN -->
<select id="findUserWithProfile" resultMap="UserDetailMap">
    SELECT u.id, u.username, p.id AS profile_id, p.phone
    FROM user u
    LEFT JOIN user_profile p ON u.id = p.user_id  -- ← 直接连目标表
    WHERE u.id = #{id}
</select>

2️⃣ 一对多(User → Orders)

// 实体类
public class User {
    private Long id;
    private String username;
    private List<Order> orders;  // ← 集合
}
<!-- MyBatis 配置 -->
<resultMap id="UserWithOrdersMap" type="User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <collection property="orders" ofType="Order">  <!-- ← collection + ofType -->
        <id property="id" column="order_id"/>
        <result property="orderNo" column="order_no"/>
    </collection>
</resultMap>

<!-- SQL:1次 JOIN -->
<select id="findUserWithOrders" resultMap="UserWithOrdersMap">
    SELECT u.id, u.username, o.id AS order_id, o.order_no
    FROM user u
    LEFT JOIN `order` o ON u.id = o.user_id  -- ← 直接连目标表
    WHERE u.id = #{id}
</select>

3️⃣ 多对多(Student → Courses)

// 实体类
public class Student {
    private Long id;
    private String name;
    private List<Course> courses;  // ← 集合
}
<!-- MyBatis 配置 -->
<resultMap id="StudentWithCoursesMap" type="Student">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <collection property="courses" ofType="Course">  <!-- ← collection + ofType -->
        <id property="id" column="course_id"/>
        <result property="name" column="course_name"/>
    </collection>
</resultMap>

<!-- SQL:2次 JOIN(关键区别!)-->
<select id="findStudentWithCourses" resultMap="StudentWithCoursesMap">
    SELECT s.id, s.name, c.id AS course_id, c.name AS course_name
    FROM student s
    LEFT JOIN student_course sc ON s.id = sc.student_id  -- ← 第1次:连中间表
    LEFT JOIN course c ON sc.course_id = c.id            -- ← 第2次:连目标表
    WHERE s.id = #{id}
</select>

📌 六、终极记忆法

🔴 方法1:看Java属性类型

private Profile profile;        → association + javaType
private List<Order> orders;     → collection + ofType
private List<Course> courses;   → collection + ofType

🔴 方法2:看表的数量

2张表(一对一/一对多)→ 1次 JOIN
3张表(多对多)        → 2次 JOIN(中间表是核心)

🔴 方法3:看外键位置

一对一 → 外键在任一表     → 直接 JOIN
一对多 → 外键在"多"的表   → 直接 JOIN
多对多 → 外键在中间表     → 2次 JOIN

🔴 方法4:口诀

单个对象 association,多个对象 collection
javaType 定整体,ofType 定元素
一二直连目标表,多多要过中间表

📌 七、常见错误对比

错误写法 正确写法 说明
<association ofType="Order"> <collection ofType="Order"> 集合不能用 association
<collection javaType="List"> <collection ofType="Order"> collection 用 ofType
<association javaType="List"> <association javaType="Profile"> association 指定具体类

📌 八、快速判断流程图

看 Java 实体类属性
       ↓
   是集合吗?
      /  \
    是    否
    ↓     ↓
collection  association
    ↓           ↓
 ofType    javaType
    ↓           ↓
看表数量      1次JOIN
  /  \
2张  3张
 ↓    ↓
一对多 多对多
1次   2次
JOIN  JOIN

📌 九、实战对照表

业务场景 关系 Java类型 标签 JOIN次数
用户-档案 一对一 Profile association 1次
用户-订单 一对多 List<Order> collection 1次
学生-课程 多对多 List<Course> collection 2次
订单-订单详情 一对多 List<OrderItem> collection 1次
文章-标签 多对多 List<Tag> collection 2次
用户-钱包 一对一 Wallet association 1次

📌 十、单向关联 vs 双向关联

什么是单向关联和双向关联?

在关系映射中,可以选择单向关联(只能从一方访问另一方)或双向关联(互相访问)。


10.1 一对一关系的单向 vs 双向

单向关联(User → Profile)
// 用户类
public class User {
    private Long id;
    private String username;
    private UserProfile profile;  // ← 可以访问档案
}

// 档案类
public class UserProfile {
    private Long id;
    private Long userId;      // ← 只有外键ID
    private String realName;
    // 没有 User 对象
}

特点:

  • 只能从 User 访问 Profile
  • Profile 不能直接访问 User(只有 userId

使用:

User user = userMapper.findUserWithProfile(1L);
System.out.println(user.getProfile().getPhone());  // ✅ 可以

UserProfile profile = profileMapper.findById(1L);
System.out.println(profile.getUser().getUsername());  // ❌ 不行!没有 user 对象

双向关联(User ↔ Profile)
// 用户类
public class User {
    private Long id;
    private String username;
    private UserProfile profile;  // ← 可以访问档案
}

// 档案类
public class UserProfile {
    private Long id;
    private Long userId;      // ← 保留外键ID
    private String realName;
    private User user;        // ← 反向引用:可以访问用户
}

特点:

  • User 可以访问 Profile
  • Profile 也可以访问 User(双向)

MyBatis 配置(反向查询):

<!-- 查询档案及其用户 -->
<resultMap id="ProfileWithUserMap" type="UserProfile">
    <id property="id" column="id"/>
    <result property="userId" column="user_id"/>
    <result property="realName" column="real_name"/>
    
    <!-- 反向关联用户 -->
    <association property="user" javaType="User">
        <id property="id" column="user_id"/>
        <result property="username" column="username"/>
    </association>
</resultMap>

<select id="findProfileWithUser" resultMap="ProfileWithUserMap">
    SELECT 
        p.id, p.user_id, p.real_name,
        u.username
    FROM user_profile p
    LEFT JOIN user u ON p.user_id = u.id
    WHERE p.id = #{id}
</select>

使用:

// 正向查询
User user = userMapper.findUserWithProfile(1L);
System.out.println(user.getProfile().getPhone());  // ✅ 可以

// 反向查询
UserProfile profile = profileMapper.findProfileWithUser(1L);
System.out.println(profile.getUser().getUsername());  // ✅ 可以!

10.2 一对多关系的单向 vs 双向

单向关联(User → Orders)
// 用户类
public class User {
    private Long id;
    private String username;
    private List<Order> orders;  // ← 可以访问订单列表
}

// 订单类
public class Order {
    private Long id;
    private Long userId;     // ← 只有外键ID
    private String orderNo;
    // 没有 User 对象
}

特点:

  • 只能从 User 访问 Orders
  • Order 不能直接访问 User

双向关联(User ↔ Orders)
// 用户类
public class User {
    private Long id;
    private String username;
    private List<Order> orders;  // ← 可以访问订单列表
}

// 订单类
public class Order {
    private Long id;
    private Long userId;     // ← 保留外键ID
    private String orderNo;
    private User user;       // ← 反向引用:可以访问用户
}

MyBatis 配置(反向查询):

<!-- 查询订单及其用户 -->
<resultMap id="OrderWithUserMap" type="Order">
    <id property="id" column="id"/>
    <result property="userId" column="user_id"/>
    <result property="orderNo" column="order_no"/>
    
    <!-- 多对一:反向关联用户(注意用 association,不是 collection)-->
    <association property="user" javaType="User">
        <id property="id" column="user_id"/>
        <result property="username" column="username"/>
    </association>
</resultMap>

<select id="findOrderWithUser" resultMap="OrderWithUserMap">
    SELECT 
        o.id, o.user_id, o.order_no,
        u.username
    FROM `order` o
    LEFT JOIN user u ON o.user_id = u.id
    WHERE o.id = #{id}
</select>

关键点: 反向查询(多 → 一)用的是 <association>,不是 <collection>

使用场景:

// 订单列表:显示每个订单的下单用户
List<Order> orders = orderMapper.findAllOrdersWithUser();
for (Order order : orders) {
    System.out.println(order.getOrderNo() + " - " + order.getUser().getUsername());
}

// 输出:
// ORD001 - zhangsan
// ORD002 - lisi

10.3 多对多关系的单向 vs 双向

单向关联(Student → Courses)
// 学生类
public class Student {
    private Long id;
    private String name;
    private List<Course> courses;  // ← 可以访问课程列表
}

// 课程类
public class Course {
    private Long id;
    private String name;
    // 没有 students 列表
}

双向关联(Student ↔ Courses)
// 学生类
public class Student {
    private Long id;
    private String name;
    private List<Course> courses;  // ← 学生的课程列表
}

// 课程类
public class Course {
    private Long id;
    private String name;
    private List<Student> students;  // ← 反向:选这门课的学生列表
}

MyBatis 配置(反向查询):

<!-- 查询课程及选课学生 -->
<resultMap id="CourseWithStudentsMap" type="Course">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    
    <!-- 反向:课程的学生列表 -->
    <collection property="students" ofType="Student">
        <id property="id" column="student_id"/>
        <result property="name" column="student_name"/>
    </collection>
</resultMap>

<select id="findCourseWithStudents" resultMap="CourseWithStudentsMap">
    SELECT 
        c.id, c.name,
        s.id AS student_id, s.name AS student_name
    FROM course c
    LEFT JOIN student_course sc ON c.id = sc.course_id
    LEFT JOIN student s ON sc.student_id = s.id
    WHERE c.id = #{id}
</select>

10.4 双向关联的循环引用问题

问题:JSON 序列化死循环
// 双向关联
public class User {
    private List<Order> orders;
}

public class Order {
    private User user;
}

// 查询并返回给前端
@GetMapping("/users/{id}")
public Result getUser(@PathVariable Long id) {
    User user = userMapper.findUserWithOrders(id);
    return Result.success(user);  // ❌ 会死循环!
}

循环引用结构:

user {
    orders: [
        order1 {
            user: {
                orders: [
                    order1 { user: { ... } }  // 无限循环
                ]
            }
        }
    ]
}

解决方案1:使用 @JsonIgnore
public class Order {
    private Long id;
    private String orderNo;
    
    @JsonIgnore  // ← 序列化时忽略 user 字段
    private User user;
}

序列化结果:

{
  "id": 1,
  "username": "zhangsan",
  "orders": [
    {
      "id": 101,
      "orderNo": "ORD001"
    }
  ]
}

解决方案2:使用 @JsonManagedReference 和 @JsonBackReference
public class User {
    @JsonManagedReference  // ← 主引用(会序列化)
    private List<Order> orders;
}

public class Order {
    @JsonBackReference  // ← 反向引用(会被忽略)
    private User user;
}

解决方案3:使用 DTO(最佳实践)
// 实体类(内部使用,可以双向)
public class User {
    private List<Order> orders;
}

public class Order {
    private User user;
}

// DTO 类(返回给前端,单向)
public class UserDTO {
    private Long id;
    private String username;
    private List<OrderDTO> orders;
}

public class OrderDTO {
    private Long id;
    private String orderNo;
    // 没有 user 字段,打断循环
}

// Controller
@GetMapping("/users/{id}")
public Result getUser(@PathVariable Long id) {
    User user = userMapper.findUserWithOrders(id);
    UserDTO dto = convertToDTO(user);  // 转换
    return Result.success(dto);
}

10.5 单向 vs 双向对比表

特性 单向关联 双向关联
访问方式 只能单向访问 双向互相访问
循环引用 ✅ 不会出现 ⚠️ 需要处理
配置复杂度 ✅ 简单 ⚠️ 需要配置两个方向
内存占用 ✅ 较少 ⚠️ 较多
使用场景 访问方向明确 双向访问需求频繁
推荐度 ✅ 默认选择 ⚠️ 按需使用

10.6 何时使用单向,何时使用双向?

推荐使用单向关联:

✅ 大多数情况(默认选择)
✅ 访问方向明确(总是从主表查从表)
✅ 反向查询需求少
✅ 需要返回 JSON API(避免循环引用)
✅ 追求简单性

示例场景:

  • 用户 → 档案(总是查用户顺便看档案)
  • 文章 → 作者(总是看文章是谁写的)

推荐使用双向关联:

✅ 双向访问需求频繁
✅ 两个方向同样重要
✅ 内部业务逻辑(不直接返回给前端)
✅ 有完善的循环引用处理机制

示例场景:

  • 订单 ↔ 用户(既查用户的订单,也查订单的用户)
  • 评论 ↔ 用户(既查用户的评论,也查评论的作者)
  • 员工 ↔ 部门(既查员工的部门,也查部门的员工)

10.7 关系映射方向总结

一对一关系:
方向 User 类 UserProfile 类 MyBatis 标签
单向 UserProfile profile Long userId <association>
双向 UserProfile profile Long userId + User user 两边都配 <association>

一对多关系:
方向 User 类 Order 类 MyBatis 标签
单向 List<Order> orders Long userId <collection>
双向 List<Order> orders Long userId + User user User 用 <collection>
Order 用 <association>

关键: 反向(多 → 一)用 <association>,不是 <collection>


多对多关系:
方向 Student 类 Course 类 MyBatis 标签
单向 List<Course> courses <collection>
双向 List<Course> courses List<Student> students 两边都用 <collection>

10.8 最佳实践建议

  1. 默认使用单向关联

    // 简单、安全、推荐
    public class Order {
        private Long userId;  // 只存外键ID
    }
    
  2. 需要双向时添加反向引用

    // 按需添加
    public class Order {
        private Long userId;
        private User user;  // 反向引用
    }
    
  3. 双向关联必须处理循环引用

    // 使用 @JsonIgnore 或 DTO
    @JsonIgnore
    private User user;
    
  4. 按查询需求定义 ResultMap

    <!-- 查询1:只查用户和订单号(不包含 user) -->
    <resultMap id="UserWithOrdersSimple" type="User">
        <collection property="orders" ofType="Order">
            <result property="orderNo" column="order_no"/>
        </collection>
    </resultMap>
    
    <!-- 查询2:查订单和用户信息(包含 user) -->
    <resultMap id="OrderWithUser" type="Order">
        <association property="user" javaType="User">
            <result property="username" column="username"/>
        </association>
    </resultMap>
    
  5. 记忆口诀

    单向简单又安全,双向功能更强大
    默认单向是首选,需要双向再添加
    双向一定防循环,@JsonIgnore 或 DTO
    多对一用 association,一对多用 collection
    

8. 参数传递

8.1 单个参数

// Mapper 接口
User findById(Long id);
String findUsername(Long id);

// XML
<select id="findById" parameterType="long" resultType="User">
    SELECT * FROM user WHERE id = #{id}
</select>

8.2 多个参数(@Param)

// Mapper 接口
List<User> findByCondition(@Param("username") String username, 
                           @Param("minAge") Integer minAge);

// XML
<select id="findByCondition" resultType="User">
    SELECT * FROM user 
    WHERE username = #{username} AND age >= #{minAge}
</select>

@Param 注解详解:

@Param 用于指定参数在 SQL 中的名称,让 MyBatis 知道如何映射参数。

🔴 什么时候必须使用 @Param?

  1. 多个参数时(推荐总是加)
// ❌ 不加 @Param - 会报错或无法识别
User findByUsernameAndAge(String username, Integer age);

// ✅ 加 @Param - 明确指定参数名
User findByUsernameAndAge(@Param("username") String username, 
                          @Param("age") Integer age);

对应 XML:

<select id="findByUsernameAndAge" resultType="User">
    SELECT * FROM user 
    WHERE username = #{username} AND age = #{age}
</select>
  1. 动态 SQL 中需要引用参数名
List<User> findUsers(@Param("username") String username, 
                     @Param("minAge") Integer minAge);
<select id="findUsers" resultType="User">
    SELECT * FROM user
    <where>
        <if test="username != null">
            AND username = #{username}
        </if>
        <if test="minAge != null">
            AND age >= #{minAge}
        </if>
    </where>
</select>

🟢 什么时候可以省略 @Param?

  1. 单个参数(简单类型)
// 可以省略 @Param
User findById(Long id);
List<User> findByAge(Integer age);

// XML 中参数名可以随意写(建议保持一致)
<select id="findById" resultType="User">
    SELECT * FROM user WHERE id = #{id}
    <!-- 也可以写 #{userId}、#{value}、#{_parameter} -->
</select>
  1. 单个参数(对象类型)
// 可以省略 @Param
int insert(User user);
<insert id="insert">
    INSERT INTO user (username, email, age)
    VALUES (#{username}, #{email}, #{age})
    <!-- 直接使用对象的属性名 -->
</insert>

📊 @Param 使用对比表

场景 是否需要 @Param 示例
单个简单参数 ❌ 不需要 findById(Long id)
单个对象参数 ❌ 不需要 insert(User user)
多个参数 ✅ 必须 find(@Param("name") String name, @Param("age") int age)
动态SQL引用参数 ✅ 必须 <if test="name != null">
集合参数 ⚠️ 建议加 findByIds(@Param("ids") List<Long> ids)

💡 不加 @Param 的默认规则

MyBatis 会使用以下默认参数名:

// 方法定义
List<User> findByCondition(String username, Integer age);

// 不加 @Param 时,MyBatis 使用默认名称
// param1, param2, param3... 或 arg0, arg1, arg2...

对应 XML(不推荐):

<select id="findByCondition" resultType="User">
    SELECT * FROM user 
    WHERE username = #{param1} AND age = #{param2}
    <!-- 或者 #{arg0} 和 #{arg1} -->
</select>

❌ 这种方式可读性差,强烈不推荐!

🎯 最佳实践

建议:只要是 Mapper 方法,都加上 @Param

@Mapper
public interface UserMapper {
    
    // ✅ 单个参数也加,更清晰
    User findById(@Param("id") Long id);
    
    // ✅ 多个参数必须加
    List<User> findByCondition(@Param("username") String username, 
                               @Param("age") Integer age);
    
    // ✅ 对象参数可以不加,但加了更明确
    int insert(@Param("user") User user);
    
    // ✅ 集合参数建议加
    int batchInsert(@Param("users") List<User> users);
    List<User> findByIds(@Param("ids") List<Long> ids);
}

原因:

  • ✅ 代码可读性更好
  • ✅ XML 中参数名更明确
  • ✅ 避免升级 MyBatis 版本导致的兼容问题
  • ✅ 团队开发统一规范

8.3 对象参数

// Mapper 接口
int insert(User user);

// XML
<insert id="insert" parameterType="User">
    INSERT INTO user (username, email, age)
    VALUES (#{username}, #{email}, #{age})
</insert>

8.4 Map参数

// Mapper 接口
List<User> findByMap(Map<String, Object> params);

// XML
<select id="findByMap" parameterType="map" resultType="User">
    SELECT * FROM user
    WHERE username = #{username} AND age = #{age}
</select>

// 调用
Map<String, Object> params = new HashMap<>();
params.put("username", "张三");
params.put("age", 25);
List<User> users = userMapper.findByMap(params);

8.5 集合参数

// Mapper 接口
int batchInsert(List<User> users);
List<User> findByIds(@Param("ids") List<Long> ids);

// XML
<insert id="batchInsert">
    INSERT INTO user (username, email, age) VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.username}, #{user.email}, #{user.age})
    </foreach>
</insert>

<select id="findByIds" resultType="User">
    SELECT * FROM user WHERE id IN
    <foreach collection="ids" item="id" open="(" close=")" separator=",">
        #{id}
    </foreach>
</select>

9. 完整实战案例

9.1 场景:电商订单系统

数据库设计

-- 用户表
CREATE TABLE user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100),
    password VARCHAR(100),
    balance DECIMAL(10, 2) DEFAULT 0.00,
    status VARCHAR(20) DEFAULT 'active',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 商品表
CREATE TABLE product (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    stock INT NOT NULL DEFAULT 0,
    category VARCHAR(50)
);

-- 订单表
CREATE TABLE `order` (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(50) NOT NULL UNIQUE,
    user_id BIGINT NOT NULL,
    total_amount DECIMAL(10, 2) NOT NULL,
    status VARCHAR(20) DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES user(id)
);

-- 订单明细表
CREATE TABLE order_item (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_id BIGINT NOT NULL,
    product_id BIGINT NOT NULL,
    product_name VARCHAR(100),
    price DECIMAL(10, 2) NOT NULL,
    quantity INT NOT NULL,
    subtotal DECIMAL(10, 2) NOT NULL,
    FOREIGN KEY (order_id) REFERENCES `order`(id),
    FOREIGN KEY (product_id) REFERENCES product(id)
);

-- 插入测试数据
INSERT INTO user (username, email, password, balance) VALUES
('张三', 'zhangsan@test.com', '123456', 1000.00),
('李四', 'lisi@test.com', '123456', 500.00);

INSERT INTO product (name, price, stock, category) VALUES
('iPhone 15', 5999.00, 100, '手机'),
('MacBook Pro', 12999.00, 50, '电脑'),
('AirPods', 1299.00, 200, '耳机');

实体类

// User.java
package com.example.demo.domain;

import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

@Data
public class User {
    private Long id;
    private String username;
    private String email;
    private String password;
    private BigDecimal balance;
    private String status;
    private LocalDateTime createdAt;
}

// Product.java
@Data
public class Product {
    private Long id;
    private String name;
    private BigDecimal price;
    private Integer stock;
    private String category;
}

// Order.java
@Data
public class Order {
    private Long id;
    private String orderNo;
    private Long userId;
    private BigDecimal totalAmount;
    private String status;
    private LocalDateTime createdAt;
    
    // 关联数据
    private User user;
    private List<OrderItem> items;
}

// OrderItem.java
@Data
public class OrderItem {
    private Long id;
    private Long orderId;
    private Long productId;
    private String productName;
    private BigDecimal price;
    private Integer quantity;
    private BigDecimal subtotal;
}

Mapper 接口

// ProductMapper.java
package com.example.demo.mapper;

import com.example.demo.domain.Product;
import org.apache.ibatis.annotations.*;
import java.util.List;

@Mapper
public interface ProductMapper {
    
    @Select("SELECT * FROM product WHERE id = #{id}")
    Product findById(@Param("id") Long id);
    
    @Select("SELECT * FROM product WHERE category = #{category}")
    List<Product> findByCategory(@Param("category") String category);
    
    @Update("UPDATE product SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity}")
    int reduceStock(@Param("id") Long id, @Param("quantity") Integer quantity);
    
    @Update("UPDATE product SET stock = stock + #{quantity} WHERE id = #{id}")
    int increaseStock(@Param("id") Long id, @Param("quantity") Integer quantity);
}

// OrderMapper.java
@Mapper
public interface OrderMapper {
    
    // XML 实现
    int insert(Order order);
    
    Order findById(@Param("id") Long id);
    
    Order findByIdWithDetails(@Param("id") Long id);
    
    List<Order> findByUserId(@Param("userId") Long userId);
    
    int updateStatus(@Param("id") Long id, @Param("status") String status);
}

// OrderItemMapper.java
@Mapper
public interface OrderItemMapper {
    
    @Insert("INSERT INTO order_item (order_id, product_id, product_name, price, quantity, subtotal) " +
            "VALUES (#{orderId}, #{productId}, #{productName}, #{price}, #{quantity}, #{subtotal})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(OrderItem item);
    
    @Select("SELECT * FROM order_item WHERE order_id = #{orderId}")
    List<OrderItem> findByOrderId(@Param("orderId") Long orderId);
    
    int batchInsert(@Param("items") List<OrderItem> items);
}

OrderMapper.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.demo.mapper.OrderMapper">

    <!-- 基础结果映射 -->
    <resultMap id="BaseResultMap" type="com.example.demo.domain.Order">
        <id property="id" column="id"/>
        <result property="orderNo" column="order_no"/>
        <result property="userId" column="user_id"/>
        <result property="totalAmount" column="total_amount"/>
        <result property="status" column="status"/>
        <result property="createdAt" column="created_at"/>
    </resultMap>
    
    <!-- 完整结果映射:订单 + 用户 + 订单项 -->
    <resultMap id="OrderDetailMap" type="com.example.demo.domain.Order" extends="BaseResultMap">
        <!-- 关联用户信息 -->
        <association property="user" javaType="com.example.demo.domain.User">
            <id property="id" column="user_id"/>
            <result property="username" column="username"/>
            <result property="email" column="email"/>
        </association>
        
        <!-- 关联订单项列表 -->
        <collection property="items" ofType="com.example.demo.domain.OrderItem">
            <id property="id" column="item_id"/>
            <result property="orderId" column="order_id"/>
            <result property="productId" column="product_id"/>
            <result property="productName" column="product_name"/>
            <result property="price" column="price"/>
            <result property="quantity" column="quantity"/>
            <result property="subtotal" column="subtotal"/>
        </collection>
    </resultMap>
    
    <!-- 插入订单 -->
    <insert id="insert" parameterType="com.example.demo.domain.Order" 
            useGeneratedKeys="true" keyProperty="id">
        INSERT INTO `order` (order_no, user_id, total_amount, status)
        VALUES (#{orderNo}, #{userId}, #{totalAmount}, #{status})
    </insert>
    
    <!-- 根据ID查询订单 -->
    <select id="findById" parameterType="long" resultMap="BaseResultMap">
        SELECT * FROM `order` WHERE id = #{id}
    </select>
    
    <!-- 查询订单详情(包含用户和订单项) -->
    <select id="findByIdWithDetails" parameterType="long" resultMap="OrderDetailMap">
        SELECT 
            o.id, o.order_no, o.user_id, o.total_amount, o.status, o.created_at,
            u.username, u.email,
            oi.id AS item_id, oi.order_id, oi.product_id, 
            oi.product_name, oi.price, oi.quantity, oi.subtotal
        FROM `order` o
        LEFT JOIN user u ON o.user_id = u.id
        LEFT JOIN order_item oi ON o.id = oi.order_id
        WHERE o.id = #{id}
    </select>
    
    <!-- 根据用户ID查询订单列表 -->
    <select id="findByUserId" parameterType="long" resultMap="BaseResultMap">
        SELECT * FROM `order` 
        WHERE user_id = #{userId}
        ORDER BY created_at DESC
    </select>
    
    <!-- 更新订单状态 -->
    <update id="updateStatus">
        UPDATE `order` 
        SET status = #{status}
        WHERE id = #{id}
    </update>

</mapper>

OrderItemMapper.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.demo.mapper.OrderItemMapper">

    <!-- 批量插入订单项 -->
    <insert id="batchInsert">
        INSERT INTO order_item (order_id, product_id, product_name, price, quantity, subtotal)
        VALUES
        <foreach collection="items" item="item" separator=",">
            (#{item.orderId}, #{item.productId}, #{item.productName}, 
             #{item.price}, #{item.quantity}, #{item.subtotal})
        </foreach>
    </insert>

</mapper>

Service 层

package com.example.demo.service;

import com.example.demo.domain.*;
import com.example.demo.mapper.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private OrderItemMapper orderItemMapper;
    
    @Autowired
    private ProductMapper productMapper;
    
    @Autowired
    private UserMapper userMapper;
    
    /**
     * 创建订单(事务管理)
     */
    @Transactional(rollbackFor = Exception.class)
    public Order createOrder(Long userId, List<OrderItemRequest> itemRequests) {
        // 1. 计算订单总额
        BigDecimal totalAmount = BigDecimal.ZERO;
        List<OrderItem> items = new ArrayList<>();
        
        for (OrderItemRequest request : itemRequests) {
            // 查询商品
            Product product = productMapper.findById(request.getProductId());
            if (product == null) {
                throw new RuntimeException("商品不存在: " + request.getProductId());
            }
            
            // 检查库存
            if (product.getStock() < request.getQuantity()) {
                throw new RuntimeException("库存不足: " + product.getName());
            }
            
            // 扣减库存
            int updated = productMapper.reduceStock(product.getId(), request.getQuantity());
            if (updated == 0) {
                throw new RuntimeException("库存扣减失败: " + product.getName());
            }
            
            // 计算小计
            BigDecimal subtotal = product.getPrice().multiply(new BigDecimal(request.getQuantity()));
            totalAmount = totalAmount.add(subtotal);
            
            // 构建订单项
            OrderItem item = new OrderItem();
            item.setProductId(product.getId());
            item.setProductName(product.getName());
            item.setPrice(product.getPrice());
            item.setQuantity(request.getQuantity());
            item.setSubtotal(subtotal);
            items.add(item);
        }
        
        // 2. 创建订单
        Order order = new Order();
        order.setOrderNo("ORD" + UUID.randomUUID().toString().substring(0, 8).toUpperCase());
        order.setUserId(userId);
        order.setTotalAmount(totalAmount);
        order.setStatus("pending");
        
        orderMapper.insert(order);
        
        // 3. 创建订单项
        for (OrderItem item : items) {
            item.setOrderId(order.getId());
        }
        orderItemMapper.batchInsert(items);
        
        // 4. 返回完整订单信息
        return orderMapper.findByIdWithDetails(order.getId());
    }
    
    /**
     * 查询订单详情
     */
    public Order getOrderDetail(Long orderId) {
        return orderMapper.findByIdWithDetails(orderId);
    }
    
    /**
     * 查询用户订单列表
     */
    public List<Order> getUserOrders(Long userId) {
        return orderMapper.findByUserId(userId);
    }
    
    /**
     * 取消订单
     */
    @Transactional(rollbackFor = Exception.class)
    public void cancelOrder(Long orderId) {
        Order order = orderMapper.findById(orderId);
        if (order == null) {
            throw new RuntimeException("订单不存在");
        }
        
        if (!"pending".equals(order.getStatus())) {
            throw new RuntimeException("订单状态不允许取消");
        }
        
        // 恢复库存
        List<OrderItem> items = orderItemMapper.findByOrderId(orderId);
        for (OrderItem item : items) {
            productMapper.increaseStock(item.getProductId(), item.getQuantity());
        }
        
        // 更新订单状态
        orderMapper.updateStatus(orderId, "cancelled");
    }
    
    // DTO 类
    @lombok.Data
    public static class OrderItemRequest {
        private Long productId;
        private Integer quantity;
    }
}

Controller 层

package com.example.demo.controller;

import com.example.demo.domain.Order;
import com.example.demo.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    /**
     * 创建订单
     */
    @PostMapping
    public Order createOrder(@RequestBody CreateOrderRequest request) {
        return orderService.createOrder(request.getUserId(), request.getItems());
    }
    
    /**
     * 查询订单详情
     */
    @GetMapping("/{orderId}")
    public Order getOrderDetail(@PathVariable Long orderId) {
        return orderService.getOrderDetail(orderId);
    }
    
    /**
     * 查询用户订单列表
     */
    @GetMapping("/user/{userId}")
    public List<Order> getUserOrders(@PathVariable Long userId) {
        return orderService.getUserOrders(userId);
    }
    
    /**
     * 取消订单
     */
    @PostMapping("/{orderId}/cancel")
    public void cancelOrder(@PathVariable Long orderId) {
        orderService.cancelOrder(orderId);
    }
    
    @lombok.Data
    public static class CreateOrderRequest {
        private Long userId;
        private List<OrderService.OrderItemRequest> items;
    }
}

9.2 测试示例

package com.example.demo;

import com.example.demo.domain.*;
import com.example.demo.mapper.*;
import com.example.demo.service.OrderService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

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

@SpringBootTest
public class MyBatisTests {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private ProductMapper productMapper;
    
    @Autowired
    private OrderService orderService;
    
    @Test
    public void testFindUser() {
        User user = userMapper.findById(1L);
        System.out.println("用户信息: " + user);
    }
    
    @Test
    public void testCreateOrder() {
        // 构建订单请求
        List<OrderService.OrderItemRequest> items = new ArrayList<>();
        
        OrderService.OrderItemRequest item1 = new OrderService.OrderItemRequest();
        item1.setProductId(1L);
        item1.setQuantity(2);
        items.add(item1);
        
        OrderService.OrderItemRequest item2 = new OrderService.OrderItemRequest();
        item2.setProductId(3L);
        item2.setQuantity(1);
        items.add(item2);
        
        // 创建订单
        Order order = orderService.createOrder(1L, items);
        System.out.println("订单创建成功: " + order);
    }
    
    @Test
    public void testQueryOrderDetail() {
        Order order = orderService.getOrderDetail(1L);
        System.out.println("订单详情: " + order);
        System.out.println("订单项数量: " + order.getItems().size());
    }
}

10. 最佳实践

10.1 注解 vs XML 如何选择?

场景 推荐方式 原因
简单CRUD 注解 代码简洁,易于维护
复杂SQL XML 可读性好,易于调试
动态SQL XML 功能更强大
多表关联 XML 结果映射更清晰
项目规范 混合使用 根据实际情况选择

10.2 命名规范

// Mapper 接口方法命名
findById()          // 查询单个
findAll()           // 查询所有
findByXxx()         // 条件查询
insert()            // 插入
update()            // 更新
deleteById()        // 删除
count()             // 统计
batchInsert()       // 批量操作

10.3 性能优化

  1. 避免 N+1 查询问题
<!-- 不推荐:会产生 N+1 查询 -->
<select id="findOrders" resultMap="OrderMap">
    SELECT * FROM order
</select>
<select id="findItems" resultType="OrderItem">
    SELECT * FROM order_item WHERE order_id = #{orderId}
</select>

<!-- 推荐:使用 JOIN 一次查询 -->
<select id="findOrdersWithItems" resultMap="OrderWithItemsMap">
    SELECT o.*, oi.* 
    FROM order o
    LEFT JOIN order_item oi ON o.id = oi.order_id
</select>
  1. 使用分页
<select id="findByPage" resultType="User">
    SELECT * FROM user
    LIMIT #{limit} OFFSET #{offset}
</select>
  1. 只查询需要的字段
<!-- 不推荐 -->
<select id="findUsers" resultType="User">
    SELECT * FROM user
</select>

<!-- 推荐 -->
<select id="findUsers" resultType="User">
    SELECT id, username, email FROM user
</select>

10.4 事务管理

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private AccountMapper accountMapper;
    
    /**
     * 使用 @Transactional 确保数据一致性
     */
    @Transactional(rollbackFor = Exception.class)
    public void registerUser(User user) {
        // 1. 创建用户
        userMapper.insert(user);
        
        // 2. 创建账户
        Account account = new Account();
        account.setUserId(user.getId());
        account.setBalance(BigDecimal.ZERO);
        accountMapper.insert(account);
        
        // 如果发生异常,以上操作都会回滚
    }
}

10.5 SQL 注入防护

<!-- 推荐:使用 #{} 预编译 -->
<select id="findByUsername" resultType="User">
    SELECT * FROM user WHERE username = #{username}
</select>

<!-- 危险:使用 ${} 字符串拼接,可能 SQL 注入 -->
<select id="findByUsername" resultType="User">
    SELECT * FROM user WHERE username = '${username}'
</select>

<!-- ${} 适用场景:动态表名、列名 -->
<select id="findFromTable" resultType="User">
    SELECT * FROM ${tableName} WHERE id = #{id}
</select>

10.6 日志配置

# application.yml
logging:
  level:
    # 打印 MyBatis SQL 日志
    com.example.demo.mapper: DEBUG

mybatis:
  configuration:
    # 使用标准输出打印 SQL
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

10.7 常见问题

  1. 自增主键回填
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user (username) VALUES (#{username})
</insert>
  1. 枚举类型处理
// 实体类
public class Order {
    private OrderStatus status; // 枚举
}

public enum OrderStatus {
    PENDING, PAID, SHIPPED, COMPLETED, CANCELLED
}

// Mapper
@Update("UPDATE `order` SET status = #{status} WHERE id = #{id}")
int updateStatus(@Param("id") Long id, @Param("status") OrderStatus status);
  1. 日期时间处理
// 使用 Java 8 时间类型
@Data
public class User {
    private LocalDateTime createdAt;
    private LocalDate birthday;
    private LocalTime loginTime;
}

总结

快速上手步骤

  1. 添加依赖:mybatis-spring-boot-starter + 数据库驱动
  2. 配置数据源:application.yml 配置数据库连接
  3. 创建实体类:对应数据库表
  4. 创建Mapper接口:定义数据库操作方法
  5. 选择实现方式
    • 简单操作 → 注解(@Select/@Insert/@Update/@Delete)
    • 复杂操作 → XML 配置文件
  6. Service层调用:注入Mapper,编写业务逻辑
  7. Controller层暴露:提供REST API

核心要点

  • @Mapper 注解标记接口
  • @MapperScan 扫描包路径
  • #{参数} 预编译参数(防SQL注入)
  • ${参数} 字符串替换(动态表名/列名)
  • resultMap 自定义结果映射
  • 动态SQL 使用 if/choose/foreach/set 等标签
  • @Transactional 事务管理

参考资源


现在你已经掌握了 MyBatis 的核心知识,可以开始在项目中实践了!

建议从简单的 CRUD 操作开始,逐步尝试复杂的关联查询和动态 SQL。遇到问题时,多查看日志中的 SQL 语句,理解 MyBatis 的执行过程。

posted @ 2025-10-07 16:19  逝雪  阅读(2)  评论(0)    收藏  举报