MyBatis 完整教程
MyBatis 完整教程
目录
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条或多条)
选择原则:
-
确定只有一条结果 → 用
User
// ID是主键,结果唯一 @Select("SELECT * FROM user WHERE id = #{id}") User findById(@Param("id") Long id);
-
可能有多条结果 → 用
List<User>
// 年龄范围查询,可能匹配多个用户 @Select("SELECT * FROM user WHERE age >= #{minAge} AND age <= #{maxAge}") List<User> findByAgeRange(@Param("minAge") Integer minAge, @Param("maxAge") Integer maxAge);
-
增删改操作 → 用
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); // 返回删除的行数
-
统计查询 → 用
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 <= #{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名) |
❌ 不需要 |
最佳实践:
- ✅ 多个参数时,总是使用 @Param(清晰明了)
- ✅ 复杂查询条件,使用对象封装(可维护性好)
- ❌ 不要使用 Map 传参(类型不安全,难以维护)
- ❌ parameterType 一般省略(MyBatis 自动推断)
详细解释:
-
<where>
标签的作用:- 自动处理 WHERE 关键字
- 自动去除第一个条件前多余的 AND 或 OR
- 如果没有任何条件成立,不会生成 WHERE 子句
-
<if test="条件">
判断规则:username != null
:判断参数是否为nullusername != ''
:判断字符串是否为空串- 两个条件用
and
连接(注意是小写) - 也可以用
or
连接多个条件
-
XML 中的特殊字符转义:
核心问题:为什么 >=
可以直接写,而 <=
要转义?
<if test="minAge != null">
AND age >= #{minAge} <!-- ✅ >= 可以直接写 -->
</if>
<if test="maxAge != null">
AND age <= #{maxAge} <!-- ⚠️ <= 必须转义成 <= -->
</if>
原因:
<
是 XML 标签的开始符号(如<if>
、<select>
),必须转义>
是 XML 标签的结束符号,可以直接使用(但建议也转义)
XML 特殊字符对比:
符号 | 含义 | XML转义 | 是否必须转义 | 示例 |
---|---|---|---|---|
< |
小于 | < |
✅ 必须 | age < 18 |
<= |
小于等于 | <= |
✅ 必须 | age <= 18 |
> |
大于 | > |
⚠️ 建议(可不转义) | age > 18 或 age > 18 |
>= |
大于等于 | >= |
⚠️ 建议(可不转义) | age >= 18 或 age >= 18 |
& |
与符号 | & |
✅ 必须 | if (a && b) |
" |
双引号 | " |
⚠️ 属性值中建议 | name="value" |
' |
单引号 | ' |
⚠️ 属性值中建议 | name='value' |
为什么 <
必须转义?
<!-- ❌ 错误示例:XML解析器会认为 "age < 18" 中的 < 是一个标签开始 -->
<if test="age != null">
AND age < #{age} <!-- 报错!XML解析器混乱了 -->
</if>
<!-- ✅ 正确示例1:转义 -->
<if test="age != null">
AND age < #{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 > #{age} <!-- 推荐 -->
</if>
最佳实践:
方式1:使用转义字符(推荐,清晰明了)
<if test="minAge != null">AND age >= #{minAge}</if>
<if test="maxAge != null">AND age <= #{maxAge}</if>
<if test="age != null">AND age < #{age}</if>
<if test="age != null">AND age > #{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 >= 18 AND age <= 60 AND score > 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 <= 60 AND score > 80 <!-- 不一致,容易混淆 -->
</select>
总结:
<
必须转义成<
,否则XML解析报错>
可以不转义,但建议转义成>
(统一规范)- 简单SQL用转义,复杂SQL用CDATA
- 团队开发要统一规范(要么全转义,要么全用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 == "admin"">
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 && isActive">
AND status = #{status}
</if>
属性值中的转义规则总结:
位置 | 符号 | 是否需要转义 | 说明 |
---|---|---|---|
test 属性值中 | < |
❌ 不需要 | 引号内可以直接使用 |
test 属性值中 | > |
❌ 不需要 | 引号内可以直接使用 |
test 属性值中 | & |
✅ 需要(或用 and) | 用 & 或 and |
test 属性值中 | " |
✅ 需要(或换引号) | 外双内单,或转义 " |
test 属性值中 | ' |
✅ 需要(或换引号) | 外单内双,或转义 ' |
SQL 内容中 | < |
✅ 必须 | 用 < 或 CDATA |
SQL 内容中 | > |
⚠️ 建议 | 用 > 或 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 <= #{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 && username != '' && 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 == "active"">
AND status = 'active'
</if>
最佳实践建议:
-
test 属性中的比较运算符:直接使用
<
、>
<if test="age > 18 and age < 60">...</if>
-
test 属性中的逻辑运算符:使用
and
、or
,不用&&
、||
<!-- ✅ 推荐 --> <if test="a != null and b != null or c > 0">...</if> <!-- ❌ 不推荐(需要转义,麻烦)--> <if test="a != null && b != null || c > 0">...</if>
-
test 属性中的字符串比较:外双内单
<if test="status == 'active'">...</if>
-
SQL 内容中的比较运算符:
<
必须转义,>
建议转义AND age < 60 AND score > 80
-
复杂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>
详细解释:
-
执行规则:
- 从上到下依次判断
<when>
条件 - 只要有一个条件成立,执行对应的SQL,然后跳出(不再判断后续条件)
- 如果所有
<when>
都不成立,执行<otherwise>
<otherwise>
可以省略(相当于没有default分支)
- 从上到下依次判断
-
与
<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>
详细解释:
-
<set>
标签的作用:- 自动添加
SET
关键字 - 自动去除最后一个多余的逗号(这是核心功能)
- 如果所有条件都不成立,不会生成SET子句(避免SQL错误)
- 自动添加
-
为什么需要
<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>
注意事项:
- 每个字段赋值后都要加逗号,
<set>
会自动去除最后的逗号 - 如果所有
<if>
条件都不满足,UPDATE语句可能无效,建议至少保留一个必更新字段(如updated_at
) - WHERE条件必须写在
<set>
标签外面
6.4 foreach 标签(批量操作)
作用: 遍历集合或数组,生成重复的SQL片段(如批量插入、IN查询)
基本语法:
<foreach collection="集合名称" item="每个元素的变量名"
separator="分隔符" open="开始符号" close="结束符号" index="索引变量">
SQL片段(可使用 #{item} 访问当前元素)
</foreach>
属性说明:
属性 | 必填 | 说明 | 示例 |
---|---|---|---|
collection |
✅ 是 | 要遍历的集合名称 | list 、array 、ids (参数名) |
item |
✅ 是 | 当前元素的变量名 | user 、id 、item |
separator |
❌ 否 | 每个元素之间的分隔符 | , 、OR 、AND |
open |
❌ 否 | 整个循环开始前添加的字符 | ( 、VALUES |
close |
❌ 否 | 整个循环结束后添加的字符 | ) 、; |
index |
❌ 否 | 当前元素的索引(从0开始) | i 、idx |
示例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"
:每次遍历的元素命名为 userseparator=","
:每个元素之间用逗号分隔#{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"
:每个元素命名为 idopen="("
:循环开始前加左括号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 的类型声明
List
是java.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>
总结:
- Java 的
List<User>
= 类型声明 - XML 的
collection="list"
= 参数名(MyBatis 的默认命名) - 它们是两个不同层面的概念
- 建议使用
@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> 代替 |
⚠️ 使用 ${}
的安全建议:
- 严格验证输入
// ✅ 推荐:白名单验证
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);
}
- 避免用户直接输入
// ❌ 危险:直接使用用户输入
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);
}
- 优先使用其他方案
<!-- ❌ 不推荐:动态字段名 -->
<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>
总结:
${key}
用于字段名:因为字段名必须是SQL标识符,不能是参数#{value}
用于参数值:安全的预编译方式,防止SQL注入- 原则:能用
#{}
就不用${}
- 必须用
${}
时,要严格验证输入,防止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>
性能注意事项:
- 批量插入优化:
<!-- ✅ 推荐:一次性插入多条(高效)-->
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');
- 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));
}
- 批量插入数量限制:
// ⚠️ 批量插入建议每次 < 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片段前添加的内容 | WHERE 、SET 、( |
suffix |
在整个SQL片段后添加的内容 | ) 、; |
prefixOverrides |
去除SQL片段开头的指定字符 | AND 、OR 、, |
suffixOverrides |
去除SQL片段结尾的指定字符 | , 、AND 、OR |
注意: prefixOverrides
和 suffixOverrides
中的空格和竖线 |
都有意义!
完整示例:
<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>
详细解释:
prefix="WHERE"
:如果 trim 内容不为空,在前面加 WHEREprefixOverrides="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', '%')
)
prefixOverrides
和 suffixOverrides
的匹配规则:
<!-- 示例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 <= #{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>
总结要点:
<trim>
是最灵活的动态SQL标签,可以替代<where>
和<set>
prefixOverrides
去除开头多余内容,suffixOverrides
去除结尾多余内容- 竖线
|
表示"或"关系,可以匹配多个可能的字符 - 空格很重要:
"AND "
只匹配 "AND加空格" - 优先使用专用标签(
<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>
返回值:
- 默认返回
int
或long
(受影响的行数) - 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() |
map 或 hashmap |
返回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"> |
规律:
- association = 单个关联 → 用
javaType
(Java类型) - 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 最佳实践建议
-
默认使用单向关联
// 简单、安全、推荐 public class Order { private Long userId; // 只存外键ID }
-
需要双向时添加反向引用
// 按需添加 public class Order { private Long userId; private User user; // 反向引用 }
-
双向关联必须处理循环引用
// 使用 @JsonIgnore 或 DTO @JsonIgnore private User user;
-
按查询需求定义 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>
-
记忆口诀
单向简单又安全,双向功能更强大 默认单向是首选,需要双向再添加 双向一定防循环,@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?
- 多个参数时(推荐总是加)
// ❌ 不加 @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>
- 动态 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?
- 单个参数(简单类型)
// 可以省略 @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>
- 单个参数(对象类型)
// 可以省略 @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 性能优化
- 避免 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>
- 使用分页
<select id="findByPage" resultType="User">
SELECT * FROM user
LIMIT #{limit} OFFSET #{offset}
</select>
- 只查询需要的字段
<!-- 不推荐 -->
<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 常见问题
- 自增主键回填
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (username) VALUES (#{username})
</insert>
- 枚举类型处理
// 实体类
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);
- 日期时间处理
// 使用 Java 8 时间类型
@Data
public class User {
private LocalDateTime createdAt;
private LocalDate birthday;
private LocalTime loginTime;
}
总结
快速上手步骤
- ✅ 添加依赖:mybatis-spring-boot-starter + 数据库驱动
- ✅ 配置数据源:application.yml 配置数据库连接
- ✅ 创建实体类:对应数据库表
- ✅ 创建Mapper接口:定义数据库操作方法
- ✅ 选择实现方式:
- 简单操作 → 注解(@Select/@Insert/@Update/@Delete)
- 复杂操作 → XML 配置文件
- ✅ Service层调用:注入Mapper,编写业务逻辑
- ✅ Controller层暴露:提供REST API
核心要点
- @Mapper 注解标记接口
- @MapperScan 扫描包路径
- #{参数} 预编译参数(防SQL注入)
- ${参数} 字符串替换(动态表名/列名)
- resultMap 自定义结果映射
- 动态SQL 使用 if/choose/foreach/set 等标签
- @Transactional 事务管理
参考资源
- 官方文档:https://mybatis.org/mybatis-3/zh/index.html
- Spring Boot集成:https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
现在你已经掌握了 MyBatis 的核心知识,可以开始在项目中实践了!
建议从简单的 CRUD 操作开始,逐步尝试复杂的关联查询和动态 SQL。遇到问题时,多查看日志中的 SQL 语句,理解 MyBatis 的执行过程。