JavaEE进阶——MyBatis动态SQL与图书管理系统实战

目录

[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:元素之间的分隔符)

示例1:逗号分隔

[示例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 类?)

[✅ 为什么需要统一封装?(重点!)](#✅ 为什么需要统一封装?(重点!))

[✅ 实际项目中的效果](#✅ 实际项目中的效果)

前端代码示例(JavaScript)

[✅ 扩展:常见状态码建议](#✅ 扩展:常见状态码建议)

[✅ 总结:"统一结果封装"到底是什么?](#✅ 总结:“统一结果封装”到底是什么?)

[💡 核心思想:](#💡 核心思想:)

[📌 记忆口诀](#📌 记忆口诀)

[🎯 目标:理解 Result 的作用与使用场景](#🎯 目标:理解 Result 的作用与使用场景)

[✅ 先看一个真实场景](#✅ 先看一个真实场景)

[❌ 传统方式(不推荐)](#❌ 传统方式(不推荐))

[✅ 正确做法:统一结果封装](#✅ 正确做法:统一结果封装)

[✅ 标准结构详解](#✅ 标准结构详解)

[🔧 举个完整例子:用户登录接口](#🔧 举个完整例子:用户登录接口)

[1. 前端请求](#1. 前端请求)

[2. 后端处理逻辑](#2. 后端处理逻辑)

[3. 返回结果(JSON)](#3. 返回结果(JSON))

[✅ 情况一:登录成功](#✅ 情况一:登录成功)

[✅ 情况二:密码错误](#✅ 情况二:密码错误)

[✅ 情况三:未登录(参数为空)](#✅ 情况三:未登录(参数为空))

[✅ 如何实现 Result 类?](#✅ 如何实现 Result 类?)

[✅ 为什么需要统一封装?(重点!)](#✅ 为什么需要统一封装?(重点!))

[✅ 实际项目中的效果](#✅ 实际项目中的效果)

前端代码示例(JavaScript)

[✅ 扩展:常见状态码建议](#✅ 扩展:常见状态码建议)

[✅ 总结:"统一结果封装"到底是什么?](#✅ 总结:“统一结果封装”到底是什么?)

[💡 核心思想:](#💡 核心思想:)

[📌 记忆口诀](#📌 记忆口诀)


这是一份非常详细的 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 关键字。

    • 如果标签内有内容,它会自动去掉开头多余的 ANDOR

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 代码中的 updateBookbatchDeleteBook 方法比较复杂,通常配合 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>

学习建议与总结

对于新手小白,阅读完这份代码后,建议按照以下步骤练习:

  1. 理解分层架构:明白 Controller 负责接收请求,Service 负责业务逻辑,Mapper 负责和数据库说话。不要把 SQL 语句写在 Controller 里。

  2. 多写动态 SQL :手动敲一遍 XML 中的 <if><foreach> 标签,故意写错一点(比如多加个逗号),看看 MyBatis 的报错,这样印象最深。

  3. 调试分页 :启动项目,在浏览器输入不同的 currentPage 参数,观察控制台打印出的 SQL 语句中 LIMIT 后面的数字是如何变化的。

  4. 安全意识 :始终记住使用 #{} 也就是预编译参数,防止 SQL 注入。虽然文档中没有详细展开 $ 符号,但新手要尽量避免使用 ${}

这份代码和解释基本涵盖了 MyBatis 进阶操作的核心内容,如果能完全看懂并跑通,说明你已经跨过了 MyBatis 的新手门槛!


我们来详细解释一下 <set> 标签在 MyBatis 中的作用,并通过一个具体的例子帮助你理解。


🌟 背景知识

在 MyBatis 的动态 SQL 中,<set> 标签专门用于 UPDATE 语句。它的作用是:

  1. 自动插入 SET 关键字
  2. 自动去掉行尾多余的逗号(,)

这解决了我们在写动态更新语句时常见的两个问题:

  • 如果某些字段没被修改,我们不想把它们加进去;
  • 但又不能让 SQL 最后多出一个逗号,比如:SET name='张三', age=25,

✅ 场景说明

假设有一个用户表 user,包含字段:

sql 复制代码
id, name, email, age

现在我们要做一个"只更新用户修改过的字段,未修改的保持不变"的功能。

例如:

  • 用户只改了 nameemail,那么 age 不变;

  • 但我们希望生成的 SQL 是:

    sql 复制代码
    UPDATE user SET name = '李四', email = 'lisi@example.com' WHERE id = 1;

    而不是错误地写成:

    sql 复制代码
    UPDATE 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 中的 ielement
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)
  • 你可以换成任意名字,比如 itemvalueuid 都行
XML 复制代码
<foreach collection="ids" item="userId" ...>
    #{userId}
</foreach>

🔹 openclose:控制括号

示例: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')

✅ 成功批量插入!


⚠️ 注意事项(避坑指南)

  1. 防止 SQL 注入

    • 使用 #{} 而不是 ${},避免拼接出问题。
    • 例如:#{id} 是安全的,${id} 不安全。
  2. 空集合处理

    • 如果集合为空,<foreach> 不会生成任何内容,可能导致 SQL 错误。

    • 建议加判断:

      XML 复制代码
      <if test="ids != null and ids.size() > 0">
          <foreach collection="ids" item="id" open="(" close=")" separator=",">
              #{id}
          </foreach>
      </if>
  3. 性能注意

    • 批量操作建议不要超过 1000 条,否则可能超限。
    • MySQL 默认 max_allowed_packet 大小限制了单条 SQL 的长度。
  4. 集合类型命名规范

    • 如果参数是 List<?> listcollection"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 = 0LIMIT 0, 10
  • 第 2 页:(2-1)*10 = 10LIMIT 10, 10
  • 第 3 页:(3-1)*10 = 20LIMIT 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 < totalPages2 < 3 → true
hasPrev pageNum > 12 > 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>
  • 如何用拦截器自动包装返回值
  • 如何处理异常并返回标准格式

也可以继续问我 😊

相关推荐
谷哥的小弟2 小时前
Spring Framework源码解析——ConfigurableApplicationContext
java·spring·源码
麒qiqi2 小时前
【Linux 系统编程】文件 IO 与 Makefile 核心实战:从系统调用到工程编译
java·前端·spring
en-route3 小时前
Spring 框架下 Redis 会话存储应用实践
java·redis·spring
y1y1z3 小时前
Spring MVC教程
java·spring·mvc
CodersCoder4 小时前
SpringBoot整合Spring-AI并使用Redis实现自定义上下文记忆对话
人工智能·spring boot·spring
vx_bisheyuange4 小时前
基于SpringBoot的在线互动学习网站设计
java·spring boot·spring·毕业设计
雨中飘荡的记忆4 小时前
Spring状态机深度解析:从入门到生产实战
java·spring
在坚持一下我可没意见4 小时前
Spring 后端安全双剑(下篇):JWT 无状态认证 + 密码加盐加密实战
java·开发语言·spring boot·后端·安全·spring
期待のcode4 小时前
MyBatis-Plus通用枚举
java·数据库·后端·mybatis·springboot