DTO包解释 先整体说一下这个类的作用 这是一个 DTO(Data Transfer Object,数据传输对象) ,专门用来接收前端传来的“保存商品”的完整数据 。
类比一下:
就像你在电商后台“发布新商品”时,填的那张大表单 (包含商品基本信息、颜色/尺码属性、库存价格等)
这个类就是把那张表单的所有数据,打包成一个Java对象 ,方便后端接收和处理。
1. 包声明(第一行) 1 package com.easymall.entity.dto;
作用 :告诉Java这个类放在哪个“文件夹”里(逻辑上的文件夹)。
含义拆解 :
com.easymall:公司/项目名(这里叫“易商城”)
entity:实体相关的大目录
dto:专门放“数据传输对象”的子目录(和数据库表对应的PO分开)
小白理解 :就像你的文件整理在 D:\易商城项目\实体类\传输数据包\ 下面。
2. 导入包(import 部分) 1 2 3 import com.easymall.entity.po.ProductInfo;import com.easymall.entity.po.ProductPropertyValue;import com.easymall.entity.po.ProductSku;
作用 :把别的“工具/类”借过来用。
含义拆解 :
这三个都是 po 包下的类(PO = Persistent Object,持久对象,也就是和数据库表一一对应的entity类 ):
ProductInfo:商品基本信息类(对应数据库的“商品主表”,存商品名、描述、图片等)
ProductPropertyValue:商品属性值类(对应“商品属性表”,存颜色、尺码等具体属性)
ProductSku:商品库存类(对应“商品SKU表”,存“红色+L码”对应的价格、库存等)
小白理解 :就像你要填表单,先把“基本信息栏”“属性栏”“库存栏”这三个小模块的模板拿过来。
1 2 import jakarta.validation.Valid;import jakarta.validation.constraints.Size;
作用 :导入参数校验 的注解(用来检查前端传的数据合不合法)。
含义拆解 :
@Valid:用来做级联校验 (比如检查商品基本信息里的“商品名”有没有填)
@Size:用来检查集合/字符串的长度 (比如规定属性列表至少得有1个)
小白理解 :就像表单里的“必填项提示”“至少选一个规格”的规则。
4. 字段部分(核心数据) 字段1:商品基本信息 1 2 @Valid private ProductInfo productInfo;
逐词解释 :
@Valid:级联校验注解 。意思是:不仅要检查 productInfo 这个对象本身有没有传,还要检查它里面的字段(比如 ProductInfo 里的“商品名”有没有空)。
private:私有的,只有这个类自己能直接访问(别的类要通过Getter/Setter访问)。
ProductInfo:类型是“商品基本信息类”(就是刚才import的PO)。
productInfo:字段名(小驼峰命名),存具体的商品基本信息数据。
小白理解 :这是表单里的“商品基本信息栏 ”,而且这一栏里的每一项(比如商品名)都必须填对。
字段2:商品属性列表 1 2 3 @Valid @Size(min = 1) private List<ProductPropertyValue> productPropertyList;
逐词解释 :
@Valid:同样级联校验,检查每个属性里的字段(比如“属性值”有没有空)。
@Size(min = 1):长度校验 。意思是:这个列表(List)里至少得有1个元素 (也就是至少得填1个商品属性,比如颜色)。
List<ProductPropertyValue>:类型是“ProductPropertyValue的列表”,可以存多个商品属性(比如颜色、尺码、材质等)。
productPropertyList:字段名,存具体的属性列表数据。
小白理解 :这是表单里的“商品属性表格 ”,而且你至少得填一行属性(不能空着)。
字段3:商品SKU列表 1 2 3 @Valid @Size(min = 1) private List<ProductSku> skuList;
逐词解释 :
和上面几乎一样,@Valid 级联校验,@Size(min = 1) 至少1个SKU。
List<ProductSku>:类型是“ProductSku的列表”,存多个SKU组合(比如“红色+L码”“蓝色+M码”等,每个组合对应一个价格和库存)。
skuList:字段名,存具体的SKU列表数据。
小白理解 :这是表单里的“库存价格表格 ”,至少得有一个规格组合(不然商品没法卖)。
总结:这个类完整的工作流程
前端 :在后台填好“发布商品”的表单(基本信息+属性+SKU),点击“保存”。
后端 :接收到前端传来的JSON数据,自动转换成这个 ProductSaveDTO 对象。
校验 :Java自动检查:
基本信息里的必填项填了吗?(靠 @Valid)
属性列表至少有1个吗?(靠 @Size(min = 1))
SKU列表至少有1个吗?(靠 @Size(min = 1))
处理 :校验通过后,后端再把这个DTO里的数据,拆成对应的PO,存到数据库里。
注解详细解释 1. @NotEmpty(核心校验注解) 出现位置 :productName、productDesc、cover、categoryId、pCategoryId 字段
1 2 @NotEmpty private String productName;
来源包 :jakarta.validation.constraints.NotEmpty
核心作用 :强制校验“不能为空” 。
不仅不能是 null,而且字符串长度不能为0 (即不能是 "" 空字符串),集合/数组的话不能没有元素。
在这个类里的场景 :
比如 productName(商品名):发布商品时,商品名必须填,而且不能只打个空格就提交,这个注解就是卡这个的。
同理,cover(封面图)、categoryId(分类)都是发布商品的必填项。
2. @NotEmpty(groups = {UpdateGroup.class})(分组校验) 出现位置 :productId 字段
1 2 @NotEmpty(groups = {UpdateGroup.class}) private String productId;
核心作用 :“分情况校验” ——只有在特定场景下才校验。
参数拆解 :
groups = {UpdateGroup.class}:指定校验组 。
UpdateGroup 是一个自定义的空接口 (专门用来标记“更新操作”的场景)。
在这个类里的场景(非常经典,小白必懂) :
场景1:新增商品 (不需要 productId):
新增时,商品ID是后端自动生成的,前端不用传,所以此时不校验 productId。
场景2:更新商品 (必须要 productId):
更新时,必须告诉后端“更新的是哪个商品”,所以此时强制校验 productId 必须有值。
这个注解就是实现:新增时不管,更新时必须填 。
出现位置 :createTime 字段
1 2 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date createTime;
来源包 :com.fasterxml.jackson.annotation.JsonFormat
核心作用 :把Java的 Date 对象,转换成前端想要的“字符串格式” (用于后端返回数据给前端时)。
参数拆解 :
pattern = "yyyy-MM-dd HH:mm:ss":指定时间格式。
yyyy:4位年(如2026)
MM:2位月(如04)
dd:2位日(如02)
HH:24小时制小时(如14)
mm:分
ss:秒
最终效果:2026-04-02 14:30:00
timezone = "GMT+8":指定时区。
中国在东八区,必须加这个,不然返回的时间会少8小时 (时差问题)。
场景 :
后端存的是 Date 对象(一串计算机能懂的数字),前端要显示给用户看“2026-04-02 14:30:00”,这个注解就是做这个转换的。
出现位置 :createTime 字段
1 2 @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime;
来源包 :org.springframework.format.annotation.DateTimeFormat
核心作用 :把前端传来的“时间字符串”,转换成Java的 Date 对象 (用于前端提交数据给后端时)。
参数拆解 :
pattern = "yyyy-MM-dd HH:mm:ss":告诉后端,前端传过来的时间字符串长这个样子 ,请按这个格式解析。
场景 :
前端传了个字符串 "2026-04-02 14:30:00",后端要把它存到数据库(需要 Date 类型),这个注解就是做这个解析的。
和 @JsonFormat 的区别(小白速记) :
@JsonFormat:后端 → 前端 (返回给用户看)
@DateTimeFormat:前端 → 后端 (接收用户提交的)
5. @Override(重写标记) 出现位置 :toString() 方法上
1 2 @Override public String toString () { ... }
来源包 :java.lang.Override(Java核心包,不用import)
核心作用 :标记这个方法是“重写”了父类/接口的方法 。
在这个类里的场景 :
所有Java类都默认继承 Object 类,Object 类里有个原生的 toString() 方法(打印出来是“类名@哈希值”,很难懂)。
这里重写了它,改成打印商品的具体信息(如“商品ID:xxx,商品名称:xxx”),方便调试时看数据。
附加作用 :加了这个注解,如果你不小心把方法名写错了(比如写成 tostring 小写),编译器会报错提醒你,防止写错。
总结:这个类里注解的“分工”
注解
分工
场景
@NotEmpty
普通必填校验
商品名、描述、封面等新增时就必须填 的字段
@NotEmpty(groups=...)
分组必填校验
商品ID只有更新时才必须填
@JsonFormat
时间转字符串(后端→前端)
把创建时间格式化后显示给用户看
@DateTimeFormat
字符串转时间(前端→后端)
接收前端传来的时间字符串
@Override
标记重写
方便调试打印商品信息
图片上传 好的!这是一个标准的 Spring Boot 文件上传与图片读取控制器 。我们将代码分成 类定义、依赖注入、上传接口、读取接口、工具方法 5 个模块,逐行/逐块用通俗的话讲清楚。
一、 类定义与注解(从上往下看) 1 2 3 4 5 6 7 @Validated @Slf4j @RestController @RequestMapping("/file") public class FileController extends ABaseController {
二、 依赖注入(类内部的成员变量) 1 2 3 4 5 @Resource private AppConfig appConfig; @Resource private FileUtils fileUtils;
三、 接口1:上传图片 /file/uploadImage 1 2 3 4 5 6 7 8 9 10 11 12 @RequestMapping("/uploadImage") public ResponseVO uploadCover ( @NotNull MultipartFile file, // 参数1 :上传的文件。@NotNull 规定这个参数不能传空 Boolean createThumbnail // 参数2 :是否生成缩略图(可选,Boolean 可以为 null ) ) throws IOException { String filePath = fileUtils.uploadImage(file, createThumbnail); return getSuccessResponseVO(filePath); }
四、 接口2:读取/显示图片 /file/getResource 这是给浏览器访问图片用的(比如 <img src="/file/getResource?sourceName=xxx.jpg">)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RequestMapping("/getResource") public void getResource ( HttpServletResponse response, // 参数1 :HTTP 响应对象,用来把图片数据写回给浏览器 @NotEmpty String sourceName // 参数2 :文件名。@NotEmpty 规定文件名不能为空字符串 ) { if (!StringTools.pathIsOk(sourceName)) { throw new BusinessException (ResponseCodeEnum.CODE_600); } String suffix = StringTools.getFileSuffix(sourceName); response.setContentType("image/" + suffix.replace("." , "" )); response.setHeader("Cache-Control" , "max-age=2592000" ); readFile(response, sourceName); }
五、 核心工具方法:readFile(真正读取文件的逻辑) 这是一个 protected 方法,供本类或子类调用,使用了经典的 Java IO 流操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 protected void readFile (HttpServletResponse response, String filePath) { if (!StringTools.pathIsOk(filePath)) { return ; } File file = new File (appConfig.getProjectFolder() + Constants.FILE_FOLDER_FILE + filePath); if (!file.exists()) { return ; } try ( OutputStream out = response.getOutputStream(); FileInputStream in = new FileInputStream (file) ) { byte [] byteData = new byte [1024 ]; int len = 0 ; while ((len = in.read(byteData)) != -1 ) { out.write(byteData, 0 , len); } out.flush(); } catch (Exception e) { log.error("读取文件异常" , e); } }
总结:这个类的完整工作流
上传 :前端 POST 图片到 /file/uploadImage -> FileUtils 保存到磁盘 -> 返回文件路径。
访问 :前端 <img> 标签访问 /file/getResource?sourceName=路径 -> 校验安全 -> 设置响应头 -> 从磁盘读取文件流 -> 写回给浏览器显示。
Mapper 接口 好的!这是一个 MyBatis 持久层(数据库操作)的 Mapper 接口 。
一、 先搞懂:这个文件是干嘛的? 在 Java 企业级开发中,MyBatis 是用来操作数据库的框架 。
这个 ProductPropertyValueMapper 接口,相当于一个「数据库操作说明书 」,它只定义“我们要对数据库做什么”(比如:更新、删除、查询)。
具体的 SQL 语句(比如 UPDATE ...、DELETE ...),通常写在一个和它配套的 XML 文件里(比如 ProductPropertyValueMapper.xml)。
二、 逐行/逐块代码详解 1. 包声明与导入 1 import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Param; :导入 MyBatis 的 @Param 注解。这是一个非常重要的注解 ,作用是给方法的参数“起名字”,方便在 XML 里通过名字引用参数。
2. 接口定义 1 2 3 4 public interface ProductPropertyValueMapper <T, P> extends BaseMapper <T, P> {
public interface ... :声明这是一个接口 (不是普通的类),MyBatis 的 Mapper 必须是接口。
<T, P> :泛型 。
T:代表「实体类类型」(比如 ProductPropertyValue 类,对应数据库里的表)。
P:代表「主键/参数类型」(比如主键是 String 类型还是 Integer 类型)。
用泛型的好处是:这个接口可以复用,不用写死具体的类。
extends BaseMapper<T, P> :继承通用基础接口 。
这是代码里的“偷懒”技巧。BaseMapper 里肯定已经写好了最通用的方法(比如:insert() 插入、selectById() 根据ID查)。
继承它之后,这个接口就自动拥有了那些通用方法,不用再重复写一遍。我们只需要在这里写业务特有的复杂方法 。
3. 方法1:根据双ID更新 1 2 3 4 Integer updateByProductIdAndPropertyValueId (@Param("bean") T t, @Param("productId") String productId, @Param("propertyValueId") String propertyValueId) ;
方法名含义 :updateByProductIdAndPropertyValueId → 根据「商品ID」和「属性值ID」来更新数据。
返回值 Integer :返回一个整数,表示「这次操作影响了数据库里的几行数据」(比如成功更新了 1 行,就返回 1)。
参数详解 :
@Param("bean") T t :
t 是一个实体类对象,里面装着「要更新成什么新数据」。
@Param("bean") 给它起个名叫 bean,在 XML 里写 SQL 时,就可以用 #{bean.属性名} 来取对象里的值了。
@Param("productId") String productId :
@Param("propertyValueId") String propertyValueId :
业务逻辑大白话 :“去数据库里,找到 product_id 是 XXX 且 property_value_id 是 YYY 的那一行,把它的数据更新成 bean 对象里的样子。”
4. 方法2:根据双ID删除 1 2 3 4 Integer deleteByProductIdAndPropertyValueId (@Param("productId") String productId, @Param("propertyValueId") String propertyValueId) ;
方法名含义 :根据「商品ID」和「属性值ID」删除数据。
返回值 Integer :返回删除了几行。
参数 :两个 ID 作为删除的条件。
业务逻辑 :“把数据库里同时匹配这两个 ID 的那一行删掉。”
5. 方法3:批量更新 1 void updateBatch (@Param("productId") String productId, @Param("dataList") List<T> dataList) ;
方法名 updateBatch :批量更新。
参数 :dataList 里装着好几个要更新的对象。
业务逻辑 :“根据商品 ID,把 dataList 里的这些对象对应的数据库记录,一次性全部更新。”
三、 总结:这个接口的特点
不写 SQL :它只定义“做什么”,具体的 SQL 语句(UPDATE ... WHERE ...)写在配套的 XML 文件里。
@Param 很关键 :只要方法有多个参数,或者参数是对象/集合,一定要用 @Param 起名字,否则 XML 里找不到参数。
双 ID 操作 :这个表的设计很可能是一个关联表 (商品和属性值的关联),所以需要用两个 ID 才能唯一确定一条记录。
需要我给你简单看一眼配套的 XML 文件大概长什么样 吗?这样你就能理解接口和 SQL 是怎么配合的了。
MyBatis XML 映射文件 一、 文件头部与根标签(固定配置) 这部分是 MyBatis XML 的身份证 ,告诉程序“我是谁、我要绑定哪个接口”。
1 2 3 4 5 6 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.easymall.mappers.ProductPropertyValueMapper" >
<?xml ... ?> :XML 文件的声明,固定写法,说明版本和编码。
<!DOCTYPE ...> :文档类型定义(DTD),固定写法,用来校验 XML 语法是否符合 MyBatis 规范,IDE 也会根据这个给你做代码提示。
<mapper namespace="..."> :
根标签 :所有 SQL 都必须写在这个标签里面。
namespace :命名空间 ,必须和对应的 Mapper 接口的“全限定类名”(包名+类名)完全一致,这是 XML 和 Java 接口绑定的唯一凭证。
二、 核心映射配置(基础建设) 这部分是数据库表和 Java 实体类之间的翻译官 ,以及常用 SQL 片段的“仓库”。
1. 结果映射 <resultMap>(最重要的翻译官) 作用:告诉 MyBatis,数据库表里的列名,怎么对应到 Java 实体类的属性名 (比如下划线 product_id 怎么转成驼峰 productId)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <resultMap id ="base_result_map" type ="com.easymall.entity.po.ProductPropertyValue" > <result column ="product_id" property ="productId" /> <result column ="property_id" property ="propertyId" /> <result column ="property_name" property ="propertyName" /> <result column ="property_sort" property ="propertySort" /> <result column ="cover_type" property ="coverType" /> <result column ="property_value_id" property ="propertyValueId" /> <result column ="property_cover" property ="propertyCover" /> <result column ="property_value" property ="propertyValue" /> <result column ="property_remark" property ="propertyRemark" /> <result column ="sort" property ="sort" /> </resultMap >
<resultMap id="..." type="..."> :
id="base_result_map":给这个映射规则起个唯一的名字,后面查询标签用 resultMap 属性引用它。
type="...":指定要映射成哪个 Java 实体类(PO:Persistent Object,持久化对象,对应数据库表)。
<result column="..." property="..."/> :
核心翻译规则 :一行对应一个字段。
column:数据库表里的列名 (通常是下划线命名,如 product_id)。
property:Java 实体类里的属性名 (通常是驼峰命名,如 productId)。
作用:MyBatis 查询出数据后,会自动把 product_id 列的值,塞进对象的 productId 属性里。
2. SQL 片段复用 <sql>(代码仓库,避免重复写) 作用:把常用的字段列表、查询条件抽出来,起个名字,后面用 <include> 引用,改一处就能全文件生效 。
(1) 通用查询列 1 2 3 4 5 <sql id ="base_column_list" > p.product_id,p.property_id,p.property_name,p.property_sort,p.cover_type, p.property_value_id,p.property_cover,p.property_value,p.property_remark,p.sort </sql >
id="base_column_list" :给这段 SQL 起个名字。
内容 :列出了表中所有需要查询的字段,并且给表加了别名 p(后面查询语句会给表起别名 p)。
为什么不用 SELECT * :
用 * 性能稍差(数据库要先解析有哪些列)。
有时候表加了新字段但不想查出来,用明确的字段列表更安全。
(2) 基础精确查询条件(IF 判断入门) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <sql id ="base_condition_filed" > <if test ="query.productId != null and query.productId!=''" > and p.product_id = #{query.productId} </if > <if test ="query.propertyId != null and query.propertyId!=''" > and p.property_id = #{query.propertyId} </if > <if test ="query.propertyName != null and query.propertyName!=''" > and p.property_name = #{query.propertyName} </if > <if test ="query.propertySort != null" > and p.property_sort = #{query.propertySort} </if > <if test ="query.coverType != null" > and p.cover_type = #{query.coverType} </if > <if test ="query.propertyValueId != null and query.propertyValueId!=''" > and p.property_value_id = #{query.propertyValueId} </if > <if test ="query.propertyCover != null and query.propertyCover!=''" > and p.property_cover = #{query.propertyCover} </if > <if test ="query.propertyValue != null and query.propertyValue!=''" > and p.property_value = #{query.propertyValue} </if > <if test ="query.propertyRemark != null and query.propertyRemark!=''" > and p.property_remark = #{query.propertyRemark} </if > <if test ="query.sort != null" > and p.sort = #{query.sort} </if > </sql >
<if test="..."> :动态 SQL 的核心 。
逻辑:如果 test 里的表达式成立(为 true),就把 <if> 标签包裹的 SQL 拼接到最终语句里;如果不成立,就忽略这行。
query.productId :这里的 query 是 Mapper 接口方法里的参数对象(通常叫 Query 或 VO,封装了查询条件),productId 是它的属性。
判断规则 :
字符串类型(String):要判断 != null 且 !=''(不为空且不为空字符串)。
数字类型(Integer/Double):只需要判断 != null(因为数字没有空字符串的概念)。
and :注意这里条件前都加了 and,后面配合 <where> 标签使用,<where> 会自动处理掉多余的 and。
(3) 完整查询条件(包含模糊查询和 IN 查询) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <sql id ="query_condition" > <where > <include refid ="base_condition_filed" /> <if test ="query.productIdFuzzy!= null and query.productIdFuzzy!=''" > and p.product_id like concat('%', #{query.productIdFuzzy}, '%') </if > <if test ="query.propertyIdFuzzy!= null and query.propertyIdFuzzy!=''" > and p.property_id like concat('%', #{query.propertyIdFuzzy}, '%') </if > <if test ="query.propertyNameFuzzy!= null and query.propertyNameFuzzy!=''" > and p.property_name like concat('%', #{query.propertyNameFuzzy}, '%') </if > <if test ="query.propertyValueIdFuzzy!= null and query.propertyValueIdFuzzy!=''" > and p.property_value_id like concat('%', #{query.propertyValueIdFuzzy}, '%') </if > <if test ="query.propertyCoverFuzzy!= null and query.propertyCoverFuzzy!=''" > and p.property_cover like concat('%', #{query.propertyCoverFuzzy}, '%') </if > <if test ="query.propertyValueFuzzy!= null and query.propertyValueFuzzy!=''" > and p.property_value like concat('%', #{query.propertyValueFuzzy}, '%') </if > <if test ="query.propertyRemarkFuzzy!= null and query.propertyRemarkFuzzy!=''" > and p.property_remark like concat('%', #{query.propertyRemarkFuzzy}, '%') </if > <if test ="query.productIdList!=null and query.productIdList.size()>0" > and p.product_id in <foreach item ="item" collection ="query.productIdList" index ="index" open ="(" close =")" separator ="," > #{item} </foreach > </if > </where > </sql >
<where> 标签 :
超级智能 :
如果 <where> 里面没有任何条件(所有 <if> 都不成立),它就不会加 WHERE 关键字,避免 SQL 语法错误。
如果里面的条件是以 and 或 or 开头的,它会自动把开头多余的 and/or 去掉。
<include refid="..."/> :引用上面定义好的 SQL 片段,把 base_condition_filed 的内容复制粘贴到这里。
模糊查询 LIKE :
concat('%', #{query.productIdFuzzy}, '%') :
concat 是 MySQL 的字符串拼接函数。
% 是通配符,代表任意字符。
意思是:只要字段里包含传入的参数,就算匹配(比如搜“张”,能查到“张三”、“张三丰”)。
<foreach> 循环(IN 查询) :
作用:遍历一个集合(List/Set),生成 SQL 里 IN (1,2,3) 的部分。
属性大白话:
collection="query.productIdList":要遍历的集合参数名。
item="item":给集合里的每一个元素起个临时名字。
open="(":循环开始前拼接一个左括号。
separator=",":元素之间用逗号分隔。
close=")":循环结束后拼接一个右括号。
三、 查询操作(SELECT) 1. 查询集合(带分页、排序) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <select id ="selectList" resultMap ="base_result_map" > SELECT <include refid ="base_column_list" /> FROM product_property_value p <include refid ="query_condition" /> <if test ="query.orderBy!=null" > order by ${query.orderBy} </if > <if test ="query.simplePage!=null" > limit #{query.simplePage.start},#{query.simplePage.end} </if > </select >
<select id="..." resultMap="..."> :
id:必须和 Mapper 接口里的方法名一致。
resultMap:引用上面定义的 base_result_map,把查询结果转换成实体类对象列表。
FROM product_property_value p :给表起个别名叫 p,前面的字段列表 p.product_id 就是这么来的。
order by ${query.orderBy} :
重要区别 :这里用了 ${} 而不是 #{}。
原因 :orderBy 传的是字段名 (比如 "product_id desc"),不是参数值。${} 是直接把字符串拼接到 SQL 里,而 #{} 是预编译(会加引号,变成 'product_id desc',那就错了)。
注意 :${} 有 SQL 注入风险,使用时要在后端校验 orderBy 的值,不能让前端随便传。
limit ... , ... :MySQL 的分页语法。
第一个参数:起始行(从 0 开始)。
第二个参数:查询多少条。
2. 查询总数(COUNT) 1 2 3 4 5 <select id ="selectCount" resultType ="java.lang.Integer" > SELECT count(1) FROM product_property_value p <include refid ="query_condition" /> </select >
resultType="java.lang.Integer" :
因为返回的是一个简单的数字(总条数),不需要映射成对象,直接指定返回类型为 Integer 即可。
count(1) :统计行数,和 count(*) 效果一样,性能也差不多,count(1) 是程序员的习惯写法。
四、 插入操作(INSERT) 1. 单条插入(动态字段,只插有值的列) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <insert id ="insert" parameterType ="com.easymall.entity.po.ProductPropertyValue" > INSERT INTO product_property_value <trim prefix ="(" suffix =")" suffixOverrides ="," > <if test ="bean.productId != null" > product_id, </if > <if test ="bean.propertyId != null" > property_id, </if > <if test ="bean.sort != null" > sort, </if > </trim > <trim prefix ="values (" suffix =")" suffixOverrides ="," > <if test ="bean.productId!=null" > #{bean.productId}, </if > <if test ="bean.propertyId!=null" > #{bean.propertyId}, </if > <if test ="bean.sort!=null" > #{bean.sort}, </if > </trim > </insert >
<trim> 标签 :
这是插入语句的神器 ,专门用来处理动态 SQL 的括号和逗号。
两个 <trim> 分别处理“列名部分”和“值部分”,结构必须保持一致。
suffixOverrides="," :最关键的属性。如果最后一个字段后面多了一个逗号,它会自动帮你去掉,避免 SQL 语法错误。
2. 单条插入或更新(MySQL 特有:有就更新,没有就插入) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <insert id ="insertOrUpdate" parameterType ="com.easymall.entity.po.ProductPropertyValue" > INSERT INTO product_property_value <trim prefix ="(" suffix =")" suffixOverrides ="," > </trim > <trim prefix ="values (" suffix =")" suffixOverrides ="," > </trim > on DUPLICATE key update <trim prefix ="" suffix ="" suffixOverrides ="," > <if test ="bean.productId!=null" > product_id = VALUES(product_id), </if > <if test ="bean.propertyId!=null" > property_id = VALUES(property_id), </if > </trim > </insert >
on DUPLICATE key update :
MySQL 特有功能 。
逻辑:如果插入的数据违反了主键约束 或唯一索引约束 (也就是表里已经有这条数据了),就不执行插入,转而执行后面的 UPDATE 语句。
VALUES(product_id) :表示“使用刚才 INSERT 语句里打算插入的那个 product_id 的值来更新”。
3. 批量插入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <insert id ="insertBatch" parameterType ="com.easymall.entity.po.ProductPropertyValue" > INSERT INTO product_property_value( product_id, property_id, property_name, property_sort, cover_type, property_value_id, property_cover, property_value, property_remark, sort )values <foreach collection ="list" item ="item" separator ="," > ( #{item.productId}, #{item.propertyId}, #{item.propertyName}, #{item.propertySort}, #{item.coverType}, #{item.propertyValueId}, #{item.propertyCover}, #{item.propertyValue}, #{item.propertyRemark}, #{item.sort} ) </foreach > </insert >
批量逻辑 :
列名部分是固定的(因为是批量,通常要求所有数据的字段都一致)。
<foreach> 循环 list 集合,每一个对象生成一组 (...),组与组之间用逗号 , 分隔。
最终生成 SQL:INSERT INTO table (col) VALUES (1), (2), (3)。
4. 批量插入或更新 逻辑同“单条插入或更新” + “批量插入”的结合体,利用 foreach 生成多组 values,最后跟上 on DUPLICATE key update。
五、 更新操作(UPDATE) 1. 多条件动态更新 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <update id ="updateByParam" parameterType ="com.easymall.entity.query.ProductPropertyValueQuery" > UPDATE product_property_value p <set > <if test ="bean.productId != null" > product_id = #{bean.productId}, </if > <if test ="bean.propertyId != null" > property_id = #{bean.propertyId}, </if > </set > <include refid ="query_condition" /> </update >
<set> 标签 :
专门用于 UPDATE 语句,作用和 <trim> 类似,会自动去掉 SET 子句中最后多余的逗号。
2. 根据双 ID 更新(你之前接口里的方法) 1 2 3 4 5 6 7 8 9 10 11 12 13 <update id ="updateByProductIdAndPropertyValueId" parameterType ="com.easymall.entity.po.ProductPropertyValue" > UPDATE product_property_value <set > <if test ="bean.propertyId != null" > property_id = #{bean.propertyId}, </if > </set > where product_id=#{productId} and property_value_id=#{propertyValueId} </update >
3. 批量更新 1 2 3 4 5 6 7 8 9 10 <update id ="updateBatch" > <foreach item ="item" collection ="dataList" separator =";" > update product_property_value set property_cover = #{item.propertyCover}, property_value=#{item.propertyValue}, property_remark=#{item.propertyRemark} where product_id=#{item.productId} and property_value_id=#{item.propertyValueId} </foreach > </update >
注意 :这种用 ; 分隔多条 SQL 的批量更新方式,需要在数据库连接配置(JDBC URL)中开启 allowMultiQueries=true,否则会报错。
六、 删除操作(DELETE) 1. 多条件删除 1 2 3 4 5 <delete id ="deleteByParam" > delete p from product_property_value p <include refid ="query_condition" /> </delete >
2. 根据双 ID 删除 + 批量删除 逻辑同查询和更新,利用 WHERE 条件或 <foreach> 循环生成 IN 子句。
七、 总结(小白必记核心点)
#{} vs ${} :
#{}:预编译,防注入,传参数值 (99% 场景用它)。
${}:字符串拼接,有风险,传字段名 或表名 (如 order by)。
动态标签三剑客 :
<if>:判断条件,成立才拼 SQL。
<where> / <set> / <trim>:处理 SQL 拼接后的语法问题(多余的 and、逗号)。
<foreach>:循环集合,做批量操作或 IN 查询。
映射方式 :
返回对象列表用 resultMap(复杂映射)。
返回简单类型(Integer、String)用 resultType。