深入解析:JavaEE进阶——MyBatis动态SQL与图书管理系统实战

目录

MyBatis 进阶详解与图书管理系统实战

第一部分:核心知识点深度解析

1. 什么是动态 SQL?为什么需要它?

2. 动态 SQL 标签详解(文档核心点扩展)

2.1 标签:最常用的判断逻辑

2.2 标签:万能的“修剪工”

2.3 标签:智能的 WHERE 子句

2.4 标签:用于 UPDATE 更新语句

2.5 标签:循环遍历

3. 项目实战扩展知识点

3.1 分页查询的原理 (Pagination)

3.2 统一结果封装 (Result Wrapper)

3.3 拦截器与强制登录 (Session & Interceptor)

第二部分:完整代码识别与深度注释

XML 映射文件详解(MyBatis 核心)

学习建议与总结

背景知识

✅ 场景说明

✅ 使用 标签的例子

1. Mapper XML 文件中的写法

✅ 执行结果示例

情况一:只修改了 name 和 email

情况二:只修改了 age

情况三:所有字段都不改(全为空)

总结 的核心功能

小贴士

✅ 补充:为什么不用手动拼接?

目标:理解 的作用与属性

✅ 典型场景:批量删除用户

的基本语法

✅ 实际例子:批量删除用户

1. Java 代码准备

2. Mapper XML 写法

3. 生成的 SQL 效果

各个属性详解(带例子)

collection:要遍历的集合

示例1:参数是 List 类型

示例2:参数是 Map,集合在 map 中

示例3:参数是 POJO,集合是字段

item:当前元素的别名

open 和 close:控制括号

示例:IN 查询需要括号

示例:没有括号的情况(比如批量插入)

separator:元素之间的分隔符

示例1:逗号分隔

示例2:AND 连接条件

✅ 更复杂的例子:批量插入

Mapper XML

生成的 SQL

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

✅ 总结: 的核心功能

记忆口诀

目标:理解“分页查询的扩展”——为什么需要 PageResult?怎么用?

✅ 先回顾一下基础:什么是分页?

那么,“扩展”到底是什么意思?

✅ 举个实际例子说明

假设数据库中有 25 条用户数据

第一步:执行 SQL 查询数据(带 LIMIT)

第二步:执行另一个 SQL 查询总数

然后,我们把这两部分结果封装成一个对象:PageResult

✅ 如何计算这些值?

✅ 最终返回的 PageResult 对象内容

为什么要这么做?(重要!)

❌ 如果不封装,会发生什么?

✅ 封装后的好处:

✅ 实际开发中的流程图

✅ 注意事项(文档里说的“注意”)

为什么不能只查一次?

优化方案(高级)

✅ 总结:“扩展”到底是什么?

记忆口诀

目标:理解 Result 的作用与使用场景

✅ 先看一个真实场景

❌ 传统方式(不推荐)

✅ 正确做法:统一结果封装

✅ 标准结构详解

举个完整例子:用户登录接口

1. 前端请求

2. 后端处理逻辑

3. 返回结果(JSON)

✅ 情况一:登录成功

✅ 情况二:密码错误

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

✅ 如何实现 Result 类?

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

✅ 实际项目中的效果

前端代码示例(JavaScript)

✅ 扩展:常见状态码建议

✅ 总结:“统一结果封装”到底是什么?

核心思想:

记忆口诀

目标:理解 Result 的作用与使用场景

✅ 先看一个真实场景

❌ 传统方式(不推荐)

✅ 正确做法:统一结果封装

✅ 标准结构详解

举个完整例子:用户登录接口

1. 前端请求

2. 后端处理逻辑

3. 返回结果(JSON)

✅ 情况一:登录成功

✅ 情况二:密码错误

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

✅ 如何实现 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



    gender,
  • 新手扩展知识

    • 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

/**
 * ==================================================================================
 * 模块一:实体类 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 {
    // 数据库中的总记录数(用于前端计算有多少页)
    private int total;
    // 当前页的数据列表
    private java.util.List records;
    // 回传请求的分页参数,方便前端核对
    private PageRequest pageRequest;
    // 构造函数
    public PageResult(Integer total, PageRequest pageRequest, java.util.List 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 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 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 getBookListByPage(PageRequest pageRequest) {
        // 1. 获取总记录数
        Integer count = bookInfoMapper.count();
        // 2. 获取当前页的数据列表
        List 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 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 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 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





    
    
        update book_info
        
            
                book_name = #{bookName},
            
            
                author = #{author},
            
            
                count = #{count},
            
            
                price = #{price},
            
            
                publish = #{publish},
            
            
                status = #{status},
            
        
        where id = #{id}
    
    
    
        update book_info set status = 0
        where id in
        
            #{id}
        
    


学习建议与总结

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

  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,包含字段:

id, name, email, age

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

例如:

  • 用户只改了 nameemail,那么 age 不变;
  • 但我们希望生成的 SQL 是:
    UPDATE user SET name = '李四', email = 'lisi@example.com' WHERE id = 1;
    而不是错误地写成:
    UPDATE user SET name = '李四', email = 'lisi@example.com',  WHERE id = 1;  -- 多了个逗号!

✅ 使用 <set> 标签的例子

1. Mapper XML 文件中的写法

    UPDATE user
    
        
            name = #{name},
        
        
            email = #{email},
        
        
            age = #{age}
        
    
    WHERE id = #{id}

注意:每个 <if> 条件里我们都写了 ,,包括最后一个字段也写了,但这没关系 —— <set> 会自动处理掉多余的逗号!


✅ 执行结果示例

情况一:只修改了 name 和 email

传入参数:

User user = new User();
user.setId(1);
user.setName("李四");
user.setEmail("lisi@example.com");
// age 没设置(null)

生成的 SQL:

UPDATE user
SET name = '李四', email = 'lisi@example.com'
WHERE id = 1

✅ 正确!没有多余逗号,也没有无效字段。


情况二:只修改了 age

传入参数:

user.setAge(30);

生成的 SQL:

UPDATE user
SET age = 30
WHERE id = 1

✅ 只有 age 字段被更新。


情况三:所有字段都不改(全为空)

生成的 SQL:

UPDATE user
SET
WHERE id = 1

❌ 这样会报错!

⚠️ 所以通常我们会加个判断,确保至少有一个字段要改,或者用其他方式避免空 SET。


总结 <set> 的核心功能

功能说明
✅ 自动添加 SET即使你没写 SET,它也会帮你加上
✅ 自动去掉末尾逗号即使你在每个条件后面都加了 ,,它也会自动删掉最后一个
✅ 支持动态字段只有满足条件的字段才会被加入 SQL

小贴士

  • <set> 标签只能用于 UPDATE 语句;
  • 它内部会处理多个 <if><choose> 等标签的内容;
  • 推荐在每个字段后都加上逗号(除了最后一个),这样逻辑更清晰,MyBatis 会自动清理。

✅ 补充:为什么不用手动拼接?

如果不使用 <set>,你可能得自己判断是否要加 SET,还要判断是否有字段被修改,以及是否要加逗号。代码复杂且容易出错。

而用了 <set> 后,你可以放心地写:


    xxx = #{xxx},
    yyy = #{yyy},

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> 的基本语法


    
属性说明
collection要遍历的集合对象(List、Set、Array 等),对应 Java 参数中的集合变量名
item每次遍历时当前元素的别名(相当于 Java 中的 ielement
open循环开始前添加的内容(例如 "("
close循环结束后添加的内容(例如 ")"
separator元素之间的分隔符(例如 ","

✅ 实际例子:批量删除用户

1. Java 代码准备

List ids = new ArrayList<>();
ids.add(1);
ids.add(2);
ids.add(3);
// 调用 Mapper 方法
userMapper.deleteUsersByIds(ids);

2. Mapper XML 写法


    DELETE FROM user
    WHERE id IN
    
        #{id}
    

3. 生成的 SQL 效果

MyBatis 会自动把 ids 列表里的每个值都拿出来,拼成:

DELETE FROM user WHERE id IN (1, 2, 3)

✅ 完美!


各个属性详解(带例子)

collection:要遍历的集合

示例1:参数是 List 类型
  • ids 是你传入的 List<Integer> 对象的名字。
示例2:参数是 Map,集合在 map 中
Map params = new HashMap<>();
params.put("ids", Arrays.asList(1, 2, 3));
示例3:参数是 POJO,集合是字段
class UserBatch {
    private List userIds;
    // getter/setter
}

⚠️ 注意:如果集合是数组,collection 可以写为 array(如果是数组类型)或直接写数组名。


item:当前元素的别名


    #{id}
  • 每次循环时,id 就代表当前的数字(如 1、2、3)
  • 你可以换成任意名字,比如 itemvalueuid 都行

    #{userId}

openclose:控制括号

示例:IN 查询需要括号

    #{id}

→ 输出:(1, 2, 3)

示例:没有括号的情况(比如批量插入)
INSERT INTO user(name) VALUES

    (#{name})

→ 输出:(张三), (李四), (王五)


separator:元素之间的分隔符

效果
,最常用,用于 IN 查询
;用于多条语句
AND用于多个条件连接
示例1:逗号分隔

    #{id}

→ 输出:1, 2, 3

示例2:AND 连接条件

    
        #{cond}
    

✅ 更复杂的例子:批量插入

假设我们要批量插入多个用户:

List users = Arrays.asList(
    new User("张三", "zhangsan@example.com"),
    new User("李四", "lisi@example.com")
);

Mapper XML


    INSERT INTO user(name, email) VALUES
    
        (#{user.name}, #{user.email})
    

生成的 SQL

INSERT INTO user(name, email) VALUES
('张三', 'zhangsan@example.com'),
('李四', 'lisi@example.com')

✅ 成功批量插入!


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

  1. 防止 SQL 注入

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

    • 如果集合为空,<foreach> 不会生成任何内容,可能导致 SQL 错误。
    • 建议加判断:
      
          
              #{id}
          
      
  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 {
    private List 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
hasNextpageNum < totalPages2 < 3 → true
hasPrevpageNum > 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 users = userMapper.selectByPage(pageNum, pageSize);
// 你只能拿到 10 条数据,但不知道总共有多少条?
// 用户界面无法显示“共 25 条,共 3 页”

✅ 封装后的好处:

优点说明
✅ 前端展示完整显示“共 25 条,共 3 页,当前第 2 页”
✅ 支持分页控件可以实现“上一页”、“下一页”按钮
✅ 提高代码复用性所有接口都返回统一格式的分页结果
✅ 更易维护不需要每个接口都手动计算总页数

✅ 实际开发中的流程图

前端请求:/users?page=2&size=10
↓
Controller 接收参数
↓
Service 层调用 Mapper:
   1. selectUsers(page, size) → 返回 List
   2. selectTotal() → 返回 long total
↓
封装成 PageResult
↓
返回给前端

✅ 注意事项(文档里说的“注意”)

“分页通常需要执行两条 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 {
    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 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 {
    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  Result success(String msg, T data) {
        return new Result<>(200, msg, data);
    }
    // 失败方法
    public static  Result 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 {
    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 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 {
    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  Result success(String msg, T data) {
        return new Result<>(200, msg, data);
    }
    // 失败方法
    public static  Result 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>
  • 如何用拦截器自动包装返回值
  • 如何处理异常并返回标准格式

也可以继续问我

posted on 2026-02-02 10:02  ljbguanli  阅读(0)  评论(0)    收藏  举报