目录
[MyBatis 进阶详解与图书管理系统实战](#MyBatis 进阶详解与图书管理系统实战)
[1. 什么是动态 SQL?为什么需要它?](#1. 什么是动态 SQL?为什么需要它?)
[2. 动态 SQL 标签详解(文档核心点扩展)](#2. 动态 SQL 标签详解(文档核心点扩展))
[2.1 标签:最常用的判断逻辑](#2.1 标签:最常用的判断逻辑)
[2.2 标签:万能的"修剪工"](#2.2 标签:万能的“修剪工”)
[2.3 标签:智能的 WHERE 子句](#2.3 标签:智能的 WHERE 子句)
[2.4
标签:用于 UPDATE 更新语句](#2.4 标签:用于 UPDATE 更新语句)
[2.5 标签:循环遍历](#2.5 标签:循环遍历)
[3. 项目实战扩展知识点](#3. 项目实战扩展知识点)
[3.1 分页查询的原理 (Pagination)](#3.1 分页查询的原理 (Pagination))
[3.2 统一结果封装 (Result Wrapper)](#3.2 统一结果封装 (Result Wrapper))
[3.3 拦截器与强制登录 (Session & Interceptor)](#3.3 拦截器与强制登录 (Session & Interceptor))
[XML 映射文件详解(MyBatis 核心)](#XML 映射文件详解(MyBatis 核心))
[🌟 背景知识](#🌟 背景知识)
[✅ 场景说明](#✅ 场景说明)
[✅ 使用
标签的例子](#✅ 使用 标签的例子)
[1. Mapper XML 文件中的写法](#1. Mapper XML 文件中的写法)
[✅ 执行结果示例](#✅ 执行结果示例)
[情况一:只修改了 name 和 email](#情况一:只修改了 name 和 email)
[情况二:只修改了 age](#情况二:只修改了 age)
[🔍 总结
的核心功能](#🔍 总结 的核心功能)
[💡 小贴士](#💡 小贴士)
[✅ 补充:为什么不用手动拼接?](#✅ 补充:为什么不用手动拼接?)
[🎯 目标:理解 的作用与属性](#🎯 目标:理解 的作用与属性)
[✅ 典型场景:批量删除用户](#✅ 典型场景:批量删除用户)
[🔧 的基本语法](#🔧 的基本语法)
[✅ 实际例子:批量删除用户](#✅ 实际例子:批量删除用户)
[1. Java 代码准备](#1. Java 代码准备)
[2. Mapper XML 写法](#2. Mapper XML 写法)
[3. 生成的 SQL 效果](#3. 生成的 SQL 效果)
[📌 各个属性详解(带例子)](#📌 各个属性详解(带例子))
[🔹 collection:要遍历的集合](#🔹 collection:要遍历的集合)
[示例1:参数是 List 类型](#示例1:参数是 List 类型)
[示例2:参数是 Map,集合在 map 中](#示例2:参数是 Map,集合在 map 中)
[示例3:参数是 POJO,集合是字段](#示例3:参数是 POJO,集合是字段)
[🔹 item:当前元素的别名](#🔹 item:当前元素的别名)
[🔹 open 和 close:控制括号](#🔹 open 和 close:控制括号)
[示例:IN 查询需要括号](#示例:IN 查询需要括号)
[🔹 separator:元素之间的分隔符](#🔹 separator:元素之间的分隔符)
[示例2:AND 连接条件](#示例2:AND 连接条件)
[✅ 更复杂的例子:批量插入](#✅ 更复杂的例子:批量插入)
[Mapper XML](#Mapper XML)
[生成的 SQL](#生成的 SQL)
[⚠️ 注意事项(避坑指南)](#⚠️ 注意事项(避坑指南))
[✅ 总结: 的核心功能](#✅ 总结: 的核心功能)
[🧩 记忆口诀](#🧩 记忆口诀)
[🎯 目标:理解"分页查询的扩展"------为什么需要 PageResult?怎么用?](#🎯 目标:理解“分页查询的扩展”——为什么需要 PageResult?怎么用?)
[✅ 先回顾一下基础:什么是分页?](#✅ 先回顾一下基础:什么是分页?)
[🔍 那么,"扩展"到底是什么意思?](#🔍 那么,“扩展”到底是什么意思?)
[✅ 举个实际例子说明](#✅ 举个实际例子说明)
[💡 假设数据库中有 25 条用户数据](#💡 假设数据库中有 25 条用户数据)
[第一步:执行 SQL 查询数据(带 LIMIT)](#第一步:执行 SQL 查询数据(带 LIMIT))
[第二步:执行另一个 SQL 查询总数](#第二步:执行另一个 SQL 查询总数)
[🧱 然后,我们把这两部分结果封装成一个对象:PageResult](#🧱 然后,我们把这两部分结果封装成一个对象:PageResult)
[✅ 如何计算这些值?](#✅ 如何计算这些值?)
[✅ 最终返回的 PageResult 对象内容](#✅ 最终返回的 PageResult 对象内容)
[🚨 为什么要这么做?(重要!)](#🚨 为什么要这么做?(重要!))
[❌ 如果不封装,会发生什么?](#❌ 如果不封装,会发生什么?)
[✅ 封装后的好处:](#✅ 封装后的好处:)
[✅ 实际开发中的流程图](#✅ 实际开发中的流程图)
[✅ 注意事项(文档里说的"注意")](#✅ 注意事项(文档里说的“注意”))
[✅ 总结:"扩展"到底是什么?](#✅ 总结:“扩展”到底是什么?)
[📌 记忆口诀](#📌 记忆口诀)
[🎯 目标:理解 Result 的作用与使用场景](#🎯 目标:理解 Result 的作用与使用场景)
[✅ 先看一个真实场景](#✅ 先看一个真实场景)
[❌ 传统方式(不推荐)](#❌ 传统方式(不推荐))
[✅ 正确做法:统一结果封装](#✅ 正确做法:统一结果封装)
[✅ 标准结构详解](#✅ 标准结构详解)
[🔧 举个完整例子:用户登录接口](#🔧 举个完整例子:用户登录接口)
[1. 前端请求](#1. 前端请求)
[2. 后端处理逻辑](#2. 后端处理逻辑)
[3. 返回结果(JSON)](#3. 返回结果(JSON))
[✅ 情况一:登录成功](#✅ 情况一:登录成功)
[✅ 情况二:密码错误](#✅ 情况二:密码错误)
[✅ 情况三:未登录(参数为空)](#✅ 情况三:未登录(参数为空))
[✅ 如何实现 Result 类?](#✅ 如何实现 Result 类?)
[✅ 为什么需要统一封装?(重点!)](#✅ 为什么需要统一封装?(重点!))
[✅ 实际项目中的效果](#✅ 实际项目中的效果)
[✅ 扩展:常见状态码建议](#✅ 扩展:常见状态码建议)
[✅ 总结:"统一结果封装"到底是什么?](#✅ 总结:“统一结果封装”到底是什么?)
[💡 核心思想:](#💡 核心思想:)
[📌 记忆口诀](#📌 记忆口诀)
[🎯 目标:理解 Result 的作用与使用场景](#🎯 目标:理解 Result 的作用与使用场景)
[✅ 先看一个真实场景](#✅ 先看一个真实场景)
[❌ 传统方式(不推荐)](#❌ 传统方式(不推荐))
[✅ 正确做法:统一结果封装](#✅ 正确做法:统一结果封装)
[✅ 标准结构详解](#✅ 标准结构详解)
[🔧 举个完整例子:用户登录接口](#🔧 举个完整例子:用户登录接口)
[1. 前端请求](#1. 前端请求)
[2. 后端处理逻辑](#2. 后端处理逻辑)
[3. 返回结果(JSON)](#3. 返回结果(JSON))
[✅ 情况一:登录成功](#✅ 情况一:登录成功)
[✅ 情况二:密码错误](#✅ 情况二:密码错误)
[✅ 情况三:未登录(参数为空)](#✅ 情况三:未登录(参数为空))
[✅ 如何实现 Result 类?](#✅ 如何实现 Result 类?)
[✅ 为什么需要统一封装?(重点!)](#✅ 为什么需要统一封装?(重点!))
[✅ 实际项目中的效果](#✅ 实际项目中的效果)
[✅ 扩展:常见状态码建议](#✅ 扩展:常见状态码建议)
[✅ 总结:"统一结果封装"到底是什么?](#✅ 总结:“统一结果封装”到底是什么?)
[💡 核心思想:](#💡 核心思想:)
[📌 记忆口诀](#📌 记忆口诀)
这是一份非常详细的 MyBatis 进阶与动态 SQL 实战教程。针对新手小白,我会把每一个概念拆碎了讲,配合文档中的案例,补充大量的背景知识、底层原理以及代码实现的细节。
这份教程不仅涵盖了文档内容,还扩展了 Spring Boot 整合、RESTful 接口设计、分页原理等实际开发必备知识。
MyBatis 进阶详解与图书管理系统实战
第一部分:核心知识点深度解析
1. 什么是动态 SQL?为什么需要它?
概念解释:
想象你在写 SQL 语句就像在"造句"。
-
静态 SQL:句子是死的,比如"我要一个苹果"。无论发生什么,你只要苹果。
-
动态 SQL:句子是活的,根据情况变化。比如"如果有红苹果,我就要红的;如果没有,我就要青的;如果没钱,我就不要了"。
在编程中,用户的搜索条件是千变万化的。用户可能只输入了名字,也可能同时输入了名字和年龄。
-
如果不使用动态 SQL,你需要写无数个
if-else在 Java 代码里拼接字符串,这非常容易出错(比如少个空格、多了个逗号),而且容易导致 SQL 注入漏洞。 -
MyBatis 动态 SQL :提供了一套类似 HTML 标签的语法(如
<if>,<where>),让你在 XML 中逻辑清晰地组装 SQL,MyBatis 会自动帮你处理空格、逗号等繁琐细节。
2. 动态 SQL 标签详解(文档核心点扩展)
2.1 <if> 标签:最常用的判断逻辑
场景:用户注册时,性别(gender)是选填项。如果用户没填,数据库就用默认值;如果填了,就插入用户填的值。
代码逻辑分析:
XML
XML
<!-- test 属性里面写的是 Java 对象的属性名,不是数据库字段名 -->
<if test="gender != null">
gender,
</if>
-
新手扩展知识:
-
test属性支持 OGNL 表达式。除了判空,还可以判断字符串是否为空串(name != '')或者数字大小(age > 18)。 -
陷阱 :在
<if>中判断字符串相等时,要小心单引号和双引号的嵌套。
-
2.2 <trim> 标签:万能的"修剪工"
场景:拼接 SQL 时,最头疼的就是多余的逗号 , 或者多余的 AND。
比如:INSERT INTO user (name, age,) VALUES ('Tom', 18,) ------ 这里的结尾逗号会导致 SQL 报错。
<trim> 的四大属性:
-
prefix:在整个内容前面加上什么(比如加上()。 -
suffix:在整个内容后面加上什么(比如加上))。 -
suffixOverrides:如果内容最后多出来了什么字符,就把它去掉(比如去掉,)。 -
prefixOverrides:如果内容最前面多出来了什么字符,就把它去掉(比如去掉AND)。
2.3 <where> 标签:智能的 WHERE 子句
场景:多条件查询。
-
传统笨办法 :
WHERE 1=1。为什么?为了后面拼接AND时不出错。 -
MyBatis 办法 :
<where>标签。-
如果标签内没有内容(所有 if 都不满足),它就不会生成
WHERE关键字。 -
如果标签内有内容,它会自动去掉开头多余的
AND或OR。
-
2.4 <set> 标签:用于 UPDATE 更新语句
场景:只修改用户修改过的字段,没修改的保持原样。
-
它会自动插入
SET关键字。 -
它会自动去掉行尾多余的逗号。
2.5 <foreach> 标签:循环遍历
场景:批量删除(DELETE FROM user WHERE id IN (1, 2, 3))。
属性解析:
-
collection:你要遍历的 Java 集合(List, Set, Array)。 -
item:当前遍历到的元素起个别名(类似 Java foreach 中的变量名)。 -
open:循环开始前加什么(如()。 -
close:循环结束后加什么(如))。 -
separator:元素之间用什么分隔(如,)。
3. 项目实战扩展知识点
3.1 分页查询的原理 (Pagination)
文档中提到了 LIMIT 关键字。
-
公式 :
LIMIT (当前页码 - 1) * 每页条数, 每页条数 -
举例:每页显示 10 条。
-
第 1 页:
LIMIT 0, 10(从第0条开始,取10条) -
第 2 页:
LIMIT 10, 10(从第10条开始,取10条) -
第 3 页:
LIMIT 20, 10
-
-
扩展 :在实际企业开发中,通常会封装一个
PageResult对象,包含:-
List<T> records:当前页的数据列表。 -
long total:总条数(用于计算总页数)。 -
注意 :分页通常需要执行两条 SQL,一条查数据,一条查
COUNT(*)总数。
-
3.2 统一结果封装 (Result Wrapper)
文档中提到了 Result<T> 类。
-
为什么需要? 前后端分离开发时,前端需要根据一个统一的状态码(Status Code)来判断请求是成功还是失败,而不是去猜。
-
标准结构:
-
code:业务状态码(200成功,-1未登录,500系统错误)。 -
msg:提示信息("操作成功" 或 "密码错误")。 -
data:真正的数据(比如查询到的用户对象)。
-
3.3 拦截器与强制登录 (Session & Interceptor)
文档通过 HttpSession 判断用户是否登录。
-
原理:HTTP 是无状态的。服务器通过 Session ID 识别这还是刚才那个用户。
-
进阶 :在实际 Spring Boot 项目中,通常不会在每个 Controller 方法里写
if (session == null),而是使用 Spring Interceptor (拦截器) 或 AOP (切面) 来统一处理登录校验。
第二部分:完整代码识别与深度注释
以下代码基于文档中的"图书管理系统"进行重构和完善。为了让你完全理解,我将代码分为 Model (实体层) 、Controller (控制层) 、Service (业务层) 、Mapper (持久层) 四个部分,并提供了完整的 XML 配置。
BookManagementSystem.java
java
/**
* ==================================================================================
* 模块一:实体类 Model (POJO)
* 作用:对应数据库中的表结构,用于在各层之间传递数据。
* 使用了 Lombok 插件的 @Data 注解,自动生成 getter/setter/toString 等方法,简化代码。
* ==================================================================================
*/
package com.example.demo.model;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
// 图书实体类,对应数据库表 book_info
@Data
public class BookInfo {
// 图书ID,主键
private Integer id;
// 书名
private String bookName;
// 作者
private String author;
// 库存数量
private Integer count;
// 价格 (涉及金钱通常使用 BigDecimal 避免精度丢失,但文档使用了 Decimal/Double,此处保持兼容)
private BigDecimal price;
// 出版社
private String publish;
// 状态:1-可借阅, 2-不可借阅, 0-已删除(逻辑删除)
private Integer status;
// 状态的中文描述(不存数据库,只用于前端展示)
private String statusCN;
// 创建时间
private Date createTime;
// 更新时间
private Date updateTime;
}
// 分页请求参数类,接收前端传来的页码和每页大小
@Data
public class PageRequest {
// 当前页码,默认第1页
private int currentPage = 1;
// 每页显示数量,默认10条
private int pageSize = 10;
// 计算数据库查询的偏移量 (Offset)
// MyBatis 查询时使用:LIMIT offset, pageSize
public int getOffset() {
return (currentPage - 1) * pageSize;
}
}
// 分页结果响应类,返回给前端的标准分页数据结构
@Data
public class PageResult<T> {
// 数据库中的总记录数(用于前端计算有多少页)
private int total;
// 当前页的数据列表
private java.util.List<T> records;
// 回传请求的分页参数,方便前端核对
private PageRequest pageRequest;
// 构造函数
public PageResult(Integer total, PageRequest pageRequest, java.util.List<T> records) {
this.total = total;
this.pageRequest = pageRequest;
this.records = records;
}
}
/**
* ==================================================================================
* 模块二:持久层 Mapper Interface
* 作用:定义访问数据库的接口。MyBatis 会根据 XML 或注解自动生成实现类。
* ==================================================================================
*/
package com.example.demo.mapper;
import com.example.demo.model.BookInfo;
import com.example.demo.model.PageRequest;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Insert;
import java.util.List;
@Mapper // 标识这是一个 MyBatis 的 Mapper 接口,Spring 启动时会扫描并创建 Bean
public interface BookInfoMapper {
/**
* 查询有效图书的总数量
* SQL解释:统计 status 不为 0 (0代表被逻辑删除了) 的记录数
* 应用场景:分页查询时,先要知道总共有多少条数据,才能计算总页数
*/
@Select("select count(1) from book_info where status <> 0")
Integer count();
/**
* 分页查询图书列表
* SQL解释:
* 1. where status != 0: 过滤掉已删除的图书
* 2. order by id desc: 按 ID 倒序排列,新添加的书在最前面
* 3. limit #{offset}, #{pageSize}: 分页的核心,从 offset 开始取 pageSize 条
* 注意:#{offset} 会从 PageRequest 对象中调用 getOffset() 方法获取
*/
@Select("select * from book_info where status != 0 order by id desc limit #{offset}, #{pageSize}")
List<BookInfo> queryBookListByPage(PageRequest pageRequest);
/**
* 添加图书
* 使用 @Insert 注解
* #{xxx} 对应 BookInfo 对象中的属性名
*/
@Insert("insert into book_info (book_name, author, count, price, publish, status) " +
"values (#{bookName}, #{author}, #{count}, #{price}, #{publish}, #{status})")
Integer insertBook(BookInfo bookInfo);
/**
* 根据 ID 查询单本图书详情
* 用于"修改图书"页面回显数据
*/
@Select("select * from book_info where id = #{bookId} and status <> 0")
BookInfo queryBookById(Integer bookId);
/**
* 修改图书信息(动态 SQL)
* 这里的实现比较复杂,通常不在注解里写,而是配合 XML 文件使用。
* 请看下文的 XML 部分。
*/
Integer updateBook(BookInfo bookInfo);
/**
* 批量删除图书
* 接收一个 ID 列表,将这些书的状态置为 0
*/
void batchDeleteBook(List<Integer> ids);
}
/**
* ==================================================================================
* 模块三:业务层 Service
* 作用:处理业务逻辑(如状态转换、事务控制)。它是 Controller 和 Mapper 的中间层。
* ==================================================================================
*/
package com.example.demo.service;
import com.example.demo.mapper.BookInfoMapper;
import com.example.demo.model.BookInfo;
import com.example.demo.model.PageRequest;
import com.example.demo.model.PageResult;
import com.example.demo.enums.BookStatus; // 假设有一个枚举类定义状态
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service // 标识这是一个业务层组件,交给 Spring 管理
public class BookService {
@Autowired // 注入 Mapper 接口,MyBatis 已经帮我们生成了代理对象
private BookInfoMapper bookInfoMapper;
/**
* 获取分页图书列表
* 业务逻辑:
* 1. 查询总数。
* 2. 查询当前页数据。
* 3. 遍历数据,将数字状态 (1, 2) 转换为中文 ("可借阅", "不可借阅"),方便前端显示。
*/
public PageResult<BookInfo> getBookListByPage(PageRequest pageRequest) {
// 1. 获取总记录数
Integer count = bookInfoMapper.count();
// 2. 获取当前页的数据列表
List<BookInfo> books = bookInfoMapper.queryBookListByPage(pageRequest);
// 3. 处理状态显示的业务逻辑
for (BookInfo book : books) {
// 这里使用了枚举工具类将 1 -> "可借阅" (文档中提及的逻辑)
// 假设 BookStatus.getNameByCode 是一个静态方法
// 如果没有枚举,可以使用简单的 if-else 替代
if (book.getStatus() == 1) {
book.setStatusCN("可借阅");
} else if (book.getStatus() == 2) {
book.setStatusCN("不可借阅");
} else {
book.setStatusCN("无效");
}
}
// 4. 封装结果返回
return new PageResult<>(count, pageRequest, books);
}
// 添加图书业务
public Integer addBook(BookInfo bookInfo) {
return bookInfoMapper.insertBook(bookInfo);
}
// 根据ID查询业务
public BookInfo queryBookById(Integer bookId) {
return bookInfoMapper.queryBookById(bookId);
}
// 更新图书业务
public Integer updateBook(BookInfo bookInfo) {
return bookInfoMapper.updateBook(bookInfo);
}
// 批量删除业务
public void batchDeleteBook(List<Integer> ids) {
bookInfoMapper.batchDeleteBook(ids);
}
}
/**
* ==================================================================================
* 模块四:控制层 Controller
* 作用:接收 HTTP 请求,解析参数,调用 Service,返回 JSON 数据。
* ==================================================================================
*/
package com.example.demo.controller;
import com.example.demo.model.*;
import com.example.demo.service.BookService;
import com.example.demo.common.Result; // 假设有一个统一返回结果类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpSession;
import java.util.List;
@RestController // 组合注解:@Controller + @ResponseBody,表示返回的都是数据而非页面
@RequestMapping("/book") // 统一定义接口前缀,如 /book/addBook
public class BookController {
@Autowired
private BookService bookService;
// 简单的日志打印(实际开发推荐使用 Slf4j)
// Logger log = LoggerFactory.getLogger(BookController.class);
/**
* 获取图书列表接口
* 请求路径:/book/getListByPage?currentPage=1&pageSize=10
*/
@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest, HttpSession session) {
// 1. 登录校验 (强制登录逻辑)
// Constants.SESSION_USER_KEY 是一个常量字符串
if (session.getAttribute("session_user_key") == null) {
// 用户未登录,返回特定的未登录状态码
return Result.unlogin();
}
// 2. 调用业务层获取数据
PageResult<BookInfo> pageResult = bookService.getBookListByPage(pageRequest);
// 3. 封装统一格式返回成功数据
return Result.success(pageResult);
}
/**
* 添加图书接口
* 请求路径:/book/addBook (POST)
* 参数自动封装进 BookInfo 对象
*/
@RequestMapping("/addBook")
public String addBook(BookInfo bookInfo) {
// 1. 参数校验 (防止空指针和脏数据)
if (!StringUtils.hasLength(bookInfo.getBookName()) ||
!StringUtils.hasLength(bookInfo.getAuthor()) ||
bookInfo.getCount() == null ||
bookInfo.getPrice() == null) {
return "输入参数不合法,请检查入参!";
}
try {
// 2. 调用服务添加
bookService.addBook(bookInfo);
return ""; // 按照文档约定,返回空字符串代表成功
} catch (Exception e) {
// log.error("添加失败", e);
return "添加失败: " + e.getMessage();
}
}
/**
* 更新图书接口
* 包含"逻辑删除"功能(前端传 status=0 即可)
*/
@RequestMapping("/updateBook")
public String updateBook(BookInfo bookInfo) {
try {
bookService.updateBook(bookInfo);
return "";
} catch (Exception e) {
return e.getMessage();
}
}
/**
* 批量删除接口
* 参数:ids=1,2,3 (Spring MVC 自动将逗号分隔字符串转为 List)
*/
@RequestMapping("/batchDeleteBook")
public boolean batchDeleteBook(@RequestParam List<Integer> ids) {
try {
bookService.batchDeleteBook(ids);
return true;
} catch (Exception e) {
return false;
}
}
// 根据ID查询图书(用于修改页面的回显)
@RequestMapping("/queryBookById")
public BookInfo queryBookById(Integer bookId) {
if (bookId == null || bookId <= 0) return new BookInfo();
return bookService.queryBookById(bookId);
}
}
XML 映射文件详解(MyBatis 核心)
上面 Java 代码中的 updateBook 和 batchDeleteBook 方法比较复杂,通常配合 XML 文件编写动态 SQL。
BookInfoMapper.xml
XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace 必须对应 Mapper 接口的全限定名 -->
<mapper namespace="com.example.demo.mapper.BookInfoMapper">
<!--
动态更新 SQL
对应接口:Integer updateBook(BookInfo bookInfo);
知识点解析:
1. <set> 标签:自动添加 SET 关键字,并去除最后多余的逗号。
2. <if test="...">:判断传入对象的属性是否为空。如果不为空,才更新该字段。
这样设计的好处是:我们可以重用这个方法实现"只修改价格"、"只修改状态"或"修改全部信息"。
-->
<update id="updateBook">
update book_info
<set>
<if test="bookName != null">
book_name = #{bookName},
</if>
<if test="author != null">
author = #{author},
</if>
<if test="count != null">
count = #{count},
</if>
<if test="price != null">
price = #{price},
</if>
<if test="publish != null">
publish = #{publish},
</if>
<if test="status != null">
status = #{status},
</if>
</set>
where id = #{id}
</update>
<!--
批量删除 SQL (实际上是批量更新状态为0)
对应接口:void batchDeleteBook(List<Integer> ids);
SQL 预览:update book_info set status=0 where id in (1, 2, 3)
知识点解析:
1. <foreach>:用于遍历集合。
2. collection="ids":对应接口参数名(Spring Boot MyBatis Starter 默认参数名为 list 或 collection,或者使用 @Param("ids") 指定)。
3. item="id":遍历时的临时变量名。
4. open="(" close=")" separator=",":智能生成 (1, 2, 3) 这种格式。
-->
<update id="batchDeleteBook">
update book_info set status = 0
where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</update>
</mapper>
学习建议与总结
对于新手小白,阅读完这份代码后,建议按照以下步骤练习:
-
理解分层架构:明白 Controller 负责接收请求,Service 负责业务逻辑,Mapper 负责和数据库说话。不要把 SQL 语句写在 Controller 里。
-
多写动态 SQL :手动敲一遍 XML 中的
<if>和<foreach>标签,故意写错一点(比如多加个逗号),看看 MyBatis 的报错,这样印象最深。 -
调试分页 :启动项目,在浏览器输入不同的
currentPage参数,观察控制台打印出的 SQL 语句中LIMIT后面的数字是如何变化的。 -
安全意识 :始终记住使用
#{}也就是预编译参数,防止 SQL 注入。虽然文档中没有详细展开$符号,但新手要尽量避免使用${}。
这份代码和解释基本涵盖了 MyBatis 进阶操作的核心内容,如果能完全看懂并跑通,说明你已经跨过了 MyBatis 的新手门槛!
我们来详细解释一下 <set> 标签在 MyBatis 中的作用,并通过一个具体的例子帮助你理解。
🌟 背景知识
在 MyBatis 的动态 SQL 中,<set> 标签专门用于 UPDATE 语句。它的作用是:
- 自动插入
SET关键字。 - 自动去掉行尾多余的逗号(,)。
这解决了我们在写动态更新语句时常见的两个问题:
- 如果某些字段没被修改,我们不想把它们加进去;
- 但又不能让 SQL 最后多出一个逗号,比如:
SET name='张三', age=25,❌
✅ 场景说明
假设有一个用户表 user,包含字段:
sql
id, name, email, age
现在我们要做一个"只更新用户修改过的字段,未修改的保持不变"的功能。
例如:
-
用户只改了
name和email,那么age不变; -
但我们希望生成的 SQL 是:
sqlUPDATE user SET name = '李四', email = 'lisi@example.com' WHERE id = 1;而不是错误地写成:
sqlUPDATE user SET name = '李四', email = 'lisi@example.com', WHERE id = 1; -- 多了个逗号!
✅ 使用 <set> 标签的例子
1. Mapper XML 文件中的写法
XML
<update id="updateUser" parameterType="User">
UPDATE user
<set>
<if test="name != null and name != ''">
name = #{name},
</if>
<if test="email != null and email != ''">
email = #{email},
</if>
<if test="age != null">
age = #{age}
</if>
</set>
WHERE id = #{id}
</update>
注意:每个
<if>条件里我们都写了,,包括最后一个字段也写了,但这没关系 ------<set>会自动处理掉多余的逗号!
✅ 执行结果示例
情况一:只修改了 name 和 email
传入参数:
java
User user = new User();
user.setId(1);
user.setName("李四");
user.setEmail("lisi@example.com");
// age 没设置(null)
生成的 SQL:
sql
UPDATE user
SET name = '李四', email = 'lisi@example.com'
WHERE id = 1
✅ 正确!没有多余逗号,也没有无效字段。
情况二:只修改了 age
传入参数:
sql
user.setAge(30);
生成的 SQL:
sql
UPDATE user
SET age = 30
WHERE id = 1
✅ 只有 age 字段被更新。
情况三:所有字段都不改(全为空)
生成的 SQL:
sql
UPDATE user
SET
WHERE id = 1
❌ 这样会报错!
⚠️ 所以通常我们会加个判断,确保至少有一个字段要改,或者用其他方式避免空 SET。
🔍 总结 <set> 的核心功能
| 功能 | 说明 |
|---|---|
✅ 自动添加 SET |
即使你没写 SET,它也会帮你加上 |
| ✅ 自动去掉末尾逗号 | 即使你在每个条件后面都加了 ,,它也会自动删掉最后一个 |
| ✅ 支持动态字段 | 只有满足条件的字段才会被加入 SQL |
💡 小贴士
<set>标签只能用于UPDATE语句;- 它内部会处理多个
<if>或<choose>等标签的内容; - 推荐在每个字段后都加上逗号(除了最后一个),这样逻辑更清晰,MyBatis 会自动清理。
✅ 补充:为什么不用手动拼接?
如果不使用 <set>,你可能得自己判断是否要加 SET,还要判断是否有字段被修改,以及是否要加逗号。代码复杂且容易出错。
而用了 <set> 后,你可以放心地写:
XML
<set>
<if test="xxx">xxx = #{xxx},</if>
<if test="yyy">yyy = #{yyy},</if>
</set>
MyBatis 会帮你搞定一切!
希望这个例子能让你彻底明白 <set> 标签的作用 😊
如果有更多 MyBatis 动态 SQL 的问题,也可以继续问我!
当然可以!我们来 详细、深入地 介绍一下 MyBatis 中的 <foreach> 标签,结合实际场景和代码示例,让你彻底理解它的用法。
🎯 目标:理解 <foreach> 的作用与属性
<foreach> 是 MyBatis 提供的一个动态 SQL 标签 ,用于在 SQL 中对 Java 集合(List、Set、Array)进行循环遍历,常用于:
- 批量插入
- 批量删除
- 批量更新
- IN 条件查询等
✅ 典型场景:批量删除用户
比如我们要执行:
DELETE FROM user WHERE id IN (1, 2, 3)
其中 1, 2, 3 是从 Java 传过来的一个列表,如 List<Integer> ids = Arrays.asList(1, 2, 3);
这时候就可以使用 <foreach> 来动态生成这些 ID。
🔧 <foreach> 的基本语法
XML
<foreach collection="?" item="?" open="?" close="?" separator="?">
<!-- 循环体内容 -->
</foreach>
| 属性 | 说明 |
|---|---|
collection |
要遍历的集合对象(List、Set、Array 等),对应 Java 参数中的集合变量名 |
item |
每次遍历时当前元素的别名(相当于 Java 中的 i 或 element) |
open |
循环开始前添加的内容(例如 "(") |
close |
循环结束后添加的内容(例如 ")") |
separator |
元素之间的分隔符(例如 ",") |
✅ 实际例子:批量删除用户
1. Java 代码准备
java
List<Integer> ids = new ArrayList<>();
ids.add(1);
ids.add(2);
ids.add(3);
// 调用 Mapper 方法
userMapper.deleteUsersByIds(ids);
2. Mapper XML 写法
XML
<delete id="deleteUsersByIds" parameterType="java.util.List">
DELETE FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>
3. 生成的 SQL 效果
MyBatis 会自动把 ids 列表里的每个值都拿出来,拼成:
sql
DELETE FROM user WHERE id IN (1, 2, 3)
✅ 完美!
📌 各个属性详解(带例子)
🔹 collection:要遍历的集合
示例1:参数是 List 类型
XML
<foreach collection="ids" item="id" ...>
ids是你传入的List<Integer>对象的名字。
示例2:参数是 Map,集合在 map 中
XML
Map<String, Object> params = new HashMap<>();
params.put("ids", Arrays.asList(1, 2, 3));
XML
<foreach collection="ids" item="id" ...>
示例3:参数是 POJO,集合是字段
java
class UserBatch {
private List<Integer> userIds;
// getter/setter
}
XML
<foreach collection="userIds" item="id" ...>
⚠️ 注意:如果集合是数组,
collection可以写为array(如果是数组类型)或直接写数组名。
🔹 item:当前元素的别名
XML
<foreach collection="ids" item="id" ...>
#{id}
</foreach>
- 每次循环时,
id就代表当前的数字(如 1、2、3) - 你可以换成任意名字,比如
item、value、uid都行
XML
<foreach collection="ids" item="userId" ...>
#{userId}
</foreach>
🔹 open 和 close:控制括号
示例:IN 查询需要括号
XML
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
→ 输出:(1, 2, 3)
示例:没有括号的情况(比如批量插入)
XML
INSERT INTO user(name) VALUES
<foreach collection="names" item="name" open="" close="" separator=",">
(#{name})
</foreach>
→ 输出:(张三), (李四), (王五)
🔹 separator:元素之间的分隔符
| 值 | 效果 |
|---|---|
, |
最常用,用于 IN 查询 |
; |
用于多条语句 |
AND |
用于多个条件连接 |
示例1:逗号分隔
XML
<foreach collection="ids" item="id" separator=",">
#{id}
</foreach>
→ 输出:1, 2, 3
示例2:AND 连接条件
XML
<where>
<foreach collection="conditions" item="cond" separator=" AND ">
#{cond}
</foreach>
</where>
✅ 更复杂的例子:批量插入
假设我们要批量插入多个用户:
XML
List<User> users = Arrays.asList(
new User("张三", "zhangsan@example.com"),
new User("李四", "lisi@example.com")
);
Mapper XML
XML
<insert id="batchInsertUsers" parameterType="java.util.List">
INSERT INTO user(name, email) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.name}, #{user.email})
</foreach>
</insert>
生成的 SQL
sql
INSERT INTO user(name, email) VALUES
('张三', 'zhangsan@example.com'),
('李四', 'lisi@example.com')
✅ 成功批量插入!
⚠️ 注意事项(避坑指南)
-
防止 SQL 注入
- 使用
#{}而不是${},避免拼接出问题。 - 例如:
#{id}是安全的,${id}不安全。
- 使用
-
空集合处理
-
如果集合为空,
<foreach>不会生成任何内容,可能导致 SQL 错误。 -
建议加判断:
XML<if test="ids != null and ids.size() > 0"> <foreach collection="ids" item="id" open="(" close=")" separator=","> #{id} </foreach> </if>
-
-
性能注意
- 批量操作建议不要超过 1000 条,否则可能超限。
- MySQL 默认
max_allowed_packet大小限制了单条 SQL 的长度。
-
集合类型命名规范
- 如果参数是
List<?> list,collection写"list" - 如果是
List<?> ids,写"ids" - 如果是
UserBatch batch,且内部有List<Integer> userIds,则写"userIds"
- 如果参数是
✅ 总结:<foreach> 的核心功能
| 功能 | 说明 |
|---|---|
| ✅ 遍历集合 | 支持 List、Set、Array |
| ✅ 自动拼接 | 生成符合 SQL 规范的字符串 |
| ✅ 控制格式 | 通过 open, close, separator 控制输出格式 |
| ✅ 安全性 | 使用 #{} 防止 SQL 注入 |
🧩 记忆口诀
"集(collection)中取项(item),开(open)闭(close)分隔(separator)"
如果你还有其他场景想看,比如:
- 批量更新
- 多条件 IN 查询
- 动态构建 WHERE 条件
也可以继续问我,我会给你更多例子 😊
你提到的"扩展 "部分,是关于 分页查询在实际开发中的封装方式。我们来一步步、详细地解释清楚,让你彻底看懂这个"扩展"的含义。
🎯 目标:理解"分页查询的扩展"------为什么需要 PageResult?怎么用?
✅ 先回顾一下基础:什么是分页?
分页就是把数据分成一页一页显示,比如:
- 每页 10 条
- 第 1 页:第 1~10 条
- 第 2 页:第 11~20 条
- ...
MySQL 中使用 LIMIT 实现:
LIMIT 起始位置, 每页条数
👉 起始位置 = (当前页码 - 1) × 每页条数
例如:
- 第 1 页:
(1-1)*10 = 0→LIMIT 0, 10 - 第 2 页:
(2-1)*10 = 10→LIMIT 10, 10 - 第 3 页:
(3-1)*10 = 20→LIMIT 20, 10
✅ 这个公式和例子你已经懂了,没问题!
🔍 那么,"扩展"到底是什么意思?
"扩展"是指:在真实项目中,我们不会只返回一个 List 数据,而是会封装成一个更完整的对象 ------
PageResult。
这就像:
- 你点外卖,商家不会只给你一盒饭;
- 而是给你一个袋子,里面有饭 + 饮料 + 筷子 + 收据(信息完整)。
同样,分页结果不只是"数据",还应该包括:
- 当前页的数据列表
- 总记录数
- 总页数
- 是否有下一页等
✅ 举个实际例子说明
假设我们要查用户列表,每页 10 条,当前是第 2 页。
💡 假设数据库中有 25 条用户数据
第一步:执行 SQL 查询数据(带 LIMIT)
SELECT * FROM user
LIMIT 10, 10; -- 取第 11~20 条
→ 返回 10 条用户数据(第 2 页)
第二步:执行另一个 SQL 查询总数
SELECT COUNT(*) FROM user;
→ 返回 25
🧱 然后,我们把这两部分结果封装成一个对象:PageResult
public class PageResult<T> {
private List<T> records; // 当前页的数据列表(如 10 条用户)
private long total; // 总条数(如 25)
private int pageNum; // 当前页码(如 2)
private int pageSize; // 每页条数(如 10)
private int totalPages; // 总页数(25 / 10 = 3 页)
private boolean hasNext; // 是否有下一页(第2页 → 有)
private boolean hasPrev; // 是否有上一页(第2页 → 有)
}
✅ 如何计算这些值?
| 字段 | 计算方式 |
|---|---|
total |
执行 COUNT(*) 得到 |
pageNum |
传入参数,如 2 |
pageSize |
传入参数,如 10 |
totalPages |
(total + pageSize - 1) / pageSize (向上取整) → (25 + 10 - 1) / 10 = 3 |
hasNext |
pageNum < totalPages → 2 < 3 → true |
hasPrev |
pageNum > 1 → 2 > 1 → true |
✅ 最终返回的 PageResult<User> 对象内容
{
"records": [
{"id": 11, "name": "张三"},
{"id": 12, "name": "李四"},
...
],
"total": 25,
"pageNum": 2,
"pageSize": 10,
"totalPages": 3,
"hasNext": true,
"hasPrev": true
}
🚨 为什么要这么做?(重要!)
❌ 如果不封装,会发生什么?
List<User> users = userMapper.selectByPage(pageNum, pageSize);
// 你只能拿到 10 条数据,但不知道总共有多少条?
// 用户界面无法显示"共 25 条,共 3 页"
✅ 封装后的好处:
| 优点 | 说明 |
|---|---|
| ✅ 前端展示完整 | 显示"共 25 条,共 3 页,当前第 2 页" |
| ✅ 支持分页控件 | 可以实现"上一页"、"下一页"按钮 |
| ✅ 提高代码复用性 | 所有接口都返回统一格式的分页结果 |
| ✅ 更易维护 | 不需要每个接口都手动计算总页数 |
✅ 实际开发中的流程图
前端请求:/users?page=2&size=10
↓
Controller 接收参数
↓
Service 层调用 Mapper:
1. selectUsers(page, size) → 返回 List<User>
2. selectTotal() → 返回 long total
↓
封装成 PageResult<User>
↓
返回给前端
✅ 注意事项(文档里说的"注意")
"分页通常需要执行两条 SQL,一条查数据,一条查 COUNT(*) 总数。"
为什么不能只查一次?
因为:
LIMIT只能控制取多少条,不能告诉你总共有多少条。- 所以必须单独查一次
COUNT(*)。
优化方案(高级)
- 使用数据库的窗口函数(如 MySQL 8.0+ 的
ROW_NUMBER()) - 或者使用 MyBatis Plus、PageHelper 等工具自动处理
- 但底层还是两个 SQL,只是帮你封装了
✅ 总结:"扩展"到底是什么?
| 内容 | 说明 |
|---|---|
| 核心思想 | 把分页结果封装成一个对象,而不仅仅是 List |
| 关键对象 | PageResult<T> 包含:数据列表 + 总条数 + 分页信息 |
| 为什么需要 | 为了让前端能完整展示分页信息(总页数、是否有下一页等) |
| 如何实现 | 两条 SQL:一条查数据,一条查总数,然后封装 |
📌 记忆口诀
"分页不止要数据,还要总条数;封装成 PageResult,前端才好做!"
如果你还想看:
- 如何写
PageResult类 - 如何在 MyBatis 中实现分页查询
- 如何用 PageHelper 插件简化分页
也可以继续问我 😊
非常好!我们来 详细、清晰地 用一个完整的例子,带你理解"统一结果封装(Result Wrapper)"这个概念。
🎯 目标:理解 Result<T> 的作用与使用场景
在前后端分离开发中,后端不能直接返回数据,而是要返回一个 标准化的响应格式 ,让前端能准确判断请求是否成功,并获取提示信息。
这就是"统一结果封装"的核心目的。
✅ 先看一个真实场景
❌ 传统方式(不推荐)
假设你调用一个接口查询用户信息:
{
"name": "张三",
"email": "zhangsan@example.com"
}
但如果你登录失败了,返回的是:
{
"msg": "未登录"
}
或者系统出错了,返回:
{
"error": "数据库连接失败"
}
👉 前端怎么知道这是成功?还是失败?是登录问题?还是系统错误?
❌ 完全靠猜!容易出错!
✅ 正确做法:统一结果封装
我们定义一个通用的响应类:Result<T>
public class Result<T> {
private int code; // 状态码
private String msg; // 提示信息
private T data; // 实际数据
// 构造函数、getter/setter 略
}
然后所有接口都返回这种格式。
✅ 标准结构详解
| 字段 | 说明 | 示例 |
|---|---|---|
code |
业务状态码 | 200: 成功,-1: 未登录,500: 系统错误 |
msg |
提示信息 | "操作成功", "密码错误", "系统繁忙" |
data |
真正的数据 | 查询到的用户对象、列表等 |
🔧 举个完整例子:用户登录接口
1. 前端请求
POST /api/login
Content-Type: application/json
{
"username": "zhangsan",
"password": "123456"
}
2. 后端处理逻辑
@PostMapping("/login")
public Result<User> login(@RequestBody LoginRequest request) {
String username = request.getUsername();
String password = request.getPassword();
// 1. 检查是否登录
if (username == null || password == null) {
return Result.fail(-1, "用户名或密码不能为空");
}
// 2. 查询用户
User user = userService.findByUsername(username);
if (user == null) {
return Result.fail(404, "用户不存在");
}
// 3. 验证密码(简化)
if (!password.equals(user.getPassword())) {
return Result.fail(401, "密码错误");
}
// 4. 登录成功
return Result.success("登录成功", user);
}
3. 返回结果(JSON)
✅ 情况一:登录成功
{
"code": 200,
"msg": "登录成功",
"data": {
"id": 1,
"name": "张三",
"email": "zhangsan@example.com"
}
}
✅ 情况二:密码错误
{
"code": 401,
"msg": "密码错误",
"data": null
}
✅ 情况三:未登录(参数为空)
{
"code": -1,
"msg": "用户名或密码不能为空",
"data": null
}
✅ 如何实现 Result<T> 类?
public class Result<T> {
private int code;
private String msg;
private T data;
// 私有构造函数,防止外部随意创建
private Result(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
// 成功方法
public static <T> Result<T> success(String msg, T data) {
return new Result<>(200, msg, data);
}
// 失败方法
public static <T> Result<T> fail(int code, String msg) {
return new Result<>(code, msg, null);
}
// getter/setter 略
}
✅ 为什么需要统一封装?(重点!)
| 优点 | 说明 |
|---|---|
| ✅ 前端可以统一处理 | 不管哪个接口,只要看 code 就知道成功还是失败 |
| ✅ 避免歧义 | 不再猜测返回的是什么类型 |
| ✅ 支持国际化 | msg 可以支持多语言 |
| ✅ 易于扩展 | 后续可以加 token、时间戳等字段 |
| ✅ 便于调试 | 开发者一眼就能看出是成功还是失败 |
✅ 实际项目中的效果
前端代码示例(JavaScript)
axios.post('/api/login', formData)
.then(response => {
const { code, msg, data } = response.data;
if (code === 200) {
alert(msg); // "登录成功"
localStorage.setItem('user', JSON.stringify(data));
window.location.href = '/dashboard';
} else if (code === 401) {
alert(msg); // "密码错误"
} else if (code === -1) {
alert(msg); // "用户名或密码不能为空"
}
})
.catch(err => {
alert("网络异常,请稍后重试");
});
✅ 前端不需要关心具体接口返回什么,只需要判断 code!
✅ 扩展:常见状态码建议
| code | 含义 | 说明 |
|---|---|---|
200 |
成功 | 操作成功 |
-1 |
未登录 | 用户未登录或 Token 过期 |
400 |
参数错误 | 请求参数不合法 |
401 |
未授权 | 密码错误、权限不足 |
404 |
资源不存在 | 用户不存在、文章已删除 |
500 |
系统错误 | 数据库异常、服务崩溃 |
✅ 总结:"统一结果封装"到底是什么?
它是一种 约定俗成的 API 响应格式,确保前后端通信清晰、可靠。
💡 核心思想:
- 所有接口都返回
Result<T>对象 - 前端通过
code判断成功或失败 - 通过
msg给用户提示 - 通过
data获取实际数据
📌 记忆口诀
"统一封装很重要,code 决定成败;msg 告诉用户啥,data 才是真数据!"
如果你还想看:
- 如何在 Spring Boot 中全局返回
Result<T> - 如何用拦截器自动包装返回值
- 如何处理异常并返回标准格式
也可以继续问我 😊
非常好!我们来 详细、清晰地 用一个完整的例子,带你理解"统一结果封装(Result Wrapper)"这个概念。
🎯 目标:理解 Result<T> 的作用与使用场景
在前后端分离开发中,后端不能直接返回数据,而是要返回一个 标准化的响应格式 ,让前端能准确判断请求是否成功,并获取提示信息。
这就是"统一结果封装"的核心目的。
✅ 先看一个真实场景
❌ 传统方式(不推荐)
假设你调用一个接口查询用户信息:
{
"name": "张三",
"email": "zhangsan@example.com"
}
但如果你登录失败了,返回的是:
{
"msg": "未登录"
}
或者系统出错了,返回:
{
"error": "数据库连接失败"
}
👉 前端怎么知道这是成功?还是失败?是登录问题?还是系统错误?
❌ 完全靠猜!容易出错!
✅ 正确做法:统一结果封装
我们定义一个通用的响应类:Result<T>
public class Result<T> {
private int code; // 状态码
private String msg; // 提示信息
private T data; // 实际数据
// 构造函数、getter/setter 略
}
然后所有接口都返回这种格式。
✅ 标准结构详解
| 字段 | 说明 | 示例 |
|---|---|---|
code |
业务状态码 | 200: 成功,-1: 未登录,500: 系统错误 |
msg |
提示信息 | "操作成功", "密码错误", "系统繁忙" |
data |
真正的数据 | 查询到的用户对象、列表等 |
🔧 举个完整例子:用户登录接口
1. 前端请求
POST /api/login
Content-Type: application/json
{
"username": "zhangsan",
"password": "123456"
}
2. 后端处理逻辑
@PostMapping("/login")
public Result<User> login(@RequestBody LoginRequest request) {
String username = request.getUsername();
String password = request.getPassword();
// 1. 检查是否登录
if (username == null || password == null) {
return Result.fail(-1, "用户名或密码不能为空");
}
// 2. 查询用户
User user = userService.findByUsername(username);
if (user == null) {
return Result.fail(404, "用户不存在");
}
// 3. 验证密码(简化)
if (!password.equals(user.getPassword())) {
return Result.fail(401, "密码错误");
}
// 4. 登录成功
return Result.success("登录成功", user);
}
3. 返回结果(JSON)
✅ 情况一:登录成功
{
"code": 200,
"msg": "登录成功",
"data": {
"id": 1,
"name": "张三",
"email": "zhangsan@example.com"
}
}
✅ 情况二:密码错误
{
"code": 401,
"msg": "密码错误",
"data": null
}
✅ 情况三:未登录(参数为空)
{
"code": -1,
"msg": "用户名或密码不能为空",
"data": null
}
✅ 如何实现 Result<T> 类?
public class Result<T> {
private int code;
private String msg;
private T data;
// 私有构造函数,防止外部随意创建
private Result(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
// 成功方法
public static <T> Result<T> success(String msg, T data) {
return new Result<>(200, msg, data);
}
// 失败方法
public static <T> Result<T> fail(int code, String msg) {
return new Result<>(code, msg, null);
}
// getter/setter 略
}
✅ 为什么需要统一封装?(重点!)
| 优点 | 说明 |
|---|---|
| ✅ 前端可以统一处理 | 不管哪个接口,只要看 code 就知道成功还是失败 |
| ✅ 避免歧义 | 不再猜测返回的是什么类型 |
| ✅ 支持国际化 | msg 可以支持多语言 |
| ✅ 易于扩展 | 后续可以加 token、时间戳等字段 |
| ✅ 便于调试 | 开发者一眼就能看出是成功还是失败 |
✅ 实际项目中的效果
前端代码示例(JavaScript)
axios.post('/api/login', formData)
.then(response => {
const { code, msg, data } = response.data;
if (code === 200) {
alert(msg); // "登录成功"
localStorage.setItem('user', JSON.stringify(data));
window.location.href = '/dashboard';
} else if (code === 401) {
alert(msg); // "密码错误"
} else if (code === -1) {
alert(msg); // "用户名或密码不能为空"
}
})
.catch(err => {
alert("网络异常,请稍后重试");
});
✅ 前端不需要关心具体接口返回什么,只需要判断 code!
✅ 扩展:常见状态码建议
| code | 含义 | 说明 |
|---|---|---|
200 |
成功 | 操作成功 |
-1 |
未登录 | 用户未登录或 Token 过期 |
400 |
参数错误 | 请求参数不合法 |
401 |
未授权 | 密码错误、权限不足 |
404 |
资源不存在 | 用户不存在、文章已删除 |
500 |
系统错误 | 数据库异常、服务崩溃 |
✅ 总结:"统一结果封装"到底是什么?
它是一种 约定俗成的 API 响应格式,确保前后端通信清晰、可靠。
💡 核心思想:
- 所有接口都返回
Result<T>对象 - 前端通过
code判断成功或失败 - 通过
msg给用户提示 - 通过
data获取实际数据
📌 记忆口诀
"统一封装很重要,code 决定成败;msg 告诉用户啥,data 才是真数据!"
如果你还想看:
- 如何在 Spring Boot 中全局返回
Result<T> - 如何用拦截器自动包装返回值
- 如何处理异常并返回标准格式
也可以继续问我 😊