修改分类、Vue 数据绑定回显与 updateById 公共字段回填
纲要
- 需求特点 :点击「修改」按钮弹出窗口并回显当前分类信息,改完点确定提交
- 回显机制 :纯前端实现,靠
Vue的v-model双向绑定,编辑阶段不与后端交互 - 请求特征 :
PUT /category,请求体为{"id":..., "name":..., "sort":...}的JSON - 后端实现 :
@PutMapping+@RequestBody Category+updateById(category),一行核心代码 - 公共字段 :
updateTime与updateUser由MyMetaObjecthandler的updateFill自动填充,无需手动set - 窗口复用 :新增与修改共用同一个
el-dialog,靠action变量区分标题与提交逻辑 SQL验证 :UPDATE category SET name=?, sort=?, update_time=?, update_user=? WHERE id=?
需求分析
分类管理列表每行都有一个「修改」按钮。点击后的行为分两个阶段:
后端服务 Vue 页面 用户 后端服务 Vue 页面 用户 #mermaid-svg-vCIHtatevIOciUkA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vCIHtatevIOciUkA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vCIHtatevIOciUkA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vCIHtatevIOciUkA .error-icon{fill:#552222;}#mermaid-svg-vCIHtatevIOciUkA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vCIHtatevIOciUkA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vCIHtatevIOciUkA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vCIHtatevIOciUkA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vCIHtatevIOciUkA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vCIHtatevIOciUkA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vCIHtatevIOciUkA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vCIHtatevIOciUkA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vCIHtatevIOciUkA .marker.cross{stroke:#333333;}#mermaid-svg-vCIHtatevIOciUkA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vCIHtatevIOciUkA p{margin:0;}#mermaid-svg-vCIHtatevIOciUkA .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vCIHtatevIOciUkA text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-vCIHtatevIOciUkA .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vCIHtatevIOciUkA .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-vCIHtatevIOciUkA .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-vCIHtatevIOciUkA .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-vCIHtatevIOciUkA #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-vCIHtatevIOciUkA .sequenceNumber{fill:white;}#mermaid-svg-vCIHtatevIOciUkA #sequencenumber{fill:#333;}#mermaid-svg-vCIHtatevIOciUkA #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-vCIHtatevIOciUkA .messageText{fill:#333;stroke:none;}#mermaid-svg-vCIHtatevIOciUkA .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vCIHtatevIOciUkA .labelText,#mermaid-svg-vCIHtatevIOciUkA .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-vCIHtatevIOciUkA .loopText,#mermaid-svg-vCIHtatevIOciUkA .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-vCIHtatevIOciUkA .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vCIHtatevIOciUkA .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-vCIHtatevIOciUkA .noteText,#mermaid-svg-vCIHtatevIOciUkA .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-vCIHtatevIOciUkA .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vCIHtatevIOciUkA .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vCIHtatevIOciUkA .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vCIHtatevIOciUkA .actorPopupMenu{position:absolute;}#mermaid-svg-vCIHtatevIOciUkA .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-vCIHtatevIOciUkA .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vCIHtatevIOciUkA .actor-man circle,#mermaid-svg-vCIHtatevIOciUkA line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-vCIHtatevIOciUkA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 此阶段无网络请求 点击「修改」按钮 editHandle(scope.row) 拿到整行数据 classData.title = '修改分类' action = 'edit' classData.name = row.name classData.sort = row.sort classData.id = row.id v-model 自动回显到输入框 修改内容,点击确定 submitForm() 校验 PUT /category {id, name, sort} {"code":1,"data":"修改分类信息成功"} 提示成功,关闭窗口,刷新列表
关键点 :回显阶段没有发请求。这是很多初学者会误解的地方------以为点修改时后端要提供「根据 id 查询」接口。实际上列表分页查询时已经把整行数据拉到前端了,editHandle(scope.row) 直接把这一行对象传给弹窗。
前端实现
修改按钮
html
<el-button
type="text"
size="small"
class="blueBug"
@click="editHandle(scope.row)"
>
修改
</el-button>
scope.row 是 Element UI 表格插槽提供的当前行数据对象,包含 id、type、name、sort 等全部字段。
editHandle 方法
js
editHandle(row) {
this.classData.title = '修改分类'
this.action = 'edit'
this.classData.id = row.id
this.classData.name = row.name
this.classData.sort = row.sort
this.dialogVisible = true
}
对比新增方法:
js
addClass(st) {
this.classData.title = '新增菜品分类'
this.action = 'add'
this.classData.name = ''
this.classData.sort = ''
this.dialogVisible = true
}
同一个窗口,两种用途
#mermaid-svg-YtOI7ll1PIXgwpnS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YtOI7ll1PIXgwpnS .error-icon{fill:#552222;}#mermaid-svg-YtOI7ll1PIXgwpnS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YtOI7ll1PIXgwpnS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YtOI7ll1PIXgwpnS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YtOI7ll1PIXgwpnS .marker.cross{stroke:#333333;}#mermaid-svg-YtOI7ll1PIXgwpnS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YtOI7ll1PIXgwpnS p{margin:0;}#mermaid-svg-YtOI7ll1PIXgwpnS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YtOI7ll1PIXgwpnS .cluster-label text{fill:#333;}#mermaid-svg-YtOI7ll1PIXgwpnS .cluster-label span{color:#333;}#mermaid-svg-YtOI7ll1PIXgwpnS .cluster-label span p{background-color:transparent;}#mermaid-svg-YtOI7ll1PIXgwpnS .label text,#mermaid-svg-YtOI7ll1PIXgwpnS span{fill:#333;color:#333;}#mermaid-svg-YtOI7ll1PIXgwpnS .node rect,#mermaid-svg-YtOI7ll1PIXgwpnS .node circle,#mermaid-svg-YtOI7ll1PIXgwpnS .node ellipse,#mermaid-svg-YtOI7ll1PIXgwpnS .node polygon,#mermaid-svg-YtOI7ll1PIXgwpnS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YtOI7ll1PIXgwpnS .rough-node .label text,#mermaid-svg-YtOI7ll1PIXgwpnS .node .label text,#mermaid-svg-YtOI7ll1PIXgwpnS .image-shape .label,#mermaid-svg-YtOI7ll1PIXgwpnS .icon-shape .label{text-anchor:middle;}#mermaid-svg-YtOI7ll1PIXgwpnS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YtOI7ll1PIXgwpnS .rough-node .label,#mermaid-svg-YtOI7ll1PIXgwpnS .node .label,#mermaid-svg-YtOI7ll1PIXgwpnS .image-shape .label,#mermaid-svg-YtOI7ll1PIXgwpnS .icon-shape .label{text-align:center;}#mermaid-svg-YtOI7ll1PIXgwpnS .node.clickable{cursor:pointer;}#mermaid-svg-YtOI7ll1PIXgwpnS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YtOI7ll1PIXgwpnS .arrowheadPath{fill:#333333;}#mermaid-svg-YtOI7ll1PIXgwpnS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YtOI7ll1PIXgwpnS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YtOI7ll1PIXgwpnS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YtOI7ll1PIXgwpnS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YtOI7ll1PIXgwpnS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YtOI7ll1PIXgwpnS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YtOI7ll1PIXgwpnS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YtOI7ll1PIXgwpnS .cluster text{fill:#333;}#mermaid-svg-YtOI7ll1PIXgwpnS .cluster span{color:#333;}#mermaid-svg-YtOI7ll1PIXgwpnS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YtOI7ll1PIXgwpnS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YtOI7ll1PIXgwpnS rect.text{fill:none;stroke-width:0;}#mermaid-svg-YtOI7ll1PIXgwpnS .icon-shape,#mermaid-svg-YtOI7ll1PIXgwpnS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YtOI7ll1PIXgwpnS .icon-shape p,#mermaid-svg-YtOI7ll1PIXgwpnS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YtOI7ll1PIXgwpnS .icon-shape .label rect,#mermaid-svg-YtOI7ll1PIXgwpnS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YtOI7ll1PIXgwpnS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YtOI7ll1PIXgwpnS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YtOI7ll1PIXgwpnS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
el-dialog 弹窗
action === 'add' ?
标题:新增菜品分类 / 新增套餐分类
标题:修改分类
提交:POST /category
提交:PUT /category
清空表单
表单中携带 id
这种做法的好处是页面结构只维护一份 ,缺点是弹窗的 data 模型要同时容纳两种场景的字段。
模型数据与双向绑定
js
data() {
return {
action: '',
dialogVisible: false,
classData: {
title: '',
id: '',
name: '',
sort: ''
}
}
}
模板中的输入框:
html
<el-form-item label="分类名称:" prop="name">
<el-input
v-model="classData.name"
placeholder="请输入分类名称"
maxlength="14"
/>
</el-form-item>
<el-form-item label="排序:" prop="sort">
<el-input
v-model="classData.sort"
placeholder="请输入排序"
type="number"
/>
</el-form-item>
v-model="classData.name" 建立了双向绑定:
- 数据 → 视图 :修改
classData.name时,输入框内容自动更新(这就是回显的原理) - 视图 → 数据 :用户在输入框键入时,
classData.name自动同步
所以 editHandle 里给 classData 赋值,输入框就自动显示了,不需要任何 DOM 操作。
提交逻辑
js
submitForm() {
const classData = this.classData
const valid = classData.name === '' || classData.sort === ''
if (this.action === 'add') {
if (!valid) {
delete this.classData.id
addCategory(this.classData).then(res => {
if (res.code === 1) {
this.$message.success('分类添加成功!')
this.dialogVisible = false
this.handleQuery()
} else {
this.$message.error(res.msg || '操作失败')
}
})
}
} else {
if (!valid) {
editCategory(this.classData).then(res => {
if (res.code === 1) {
this.$message.success('分类修改成功!')
this.dialogVisible = false
this.handleQuery()
} else {
this.$message.error(res.msg || '操作失败')
}
})
}
}
}
新增分支里 delete this.classData.id 是因为新增时不应携带 id(虽然服务端雪花算法会覆盖,但传了脏数据不优雅)。
接口封装
js
// 修改分类
function editCategory(params) {
return $axios({
url: '/category',
method: 'put',
data: { ...params }
})
}
data 会被 axios 序列化成 JSON 请求体,对应后端的 @RequestBody。
后端实现
Controller 方法
java
/**
* 根据id修改分类信息
* @param category
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category);
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
请求与响应
浏览器 Network:
txt
Request URL: http://localhost:8080/category
Request Method: PUT
Content-Type: application/json;charset=UTF-8
Request Payload:
{"id":"1397844303408570369","name":"123","sort":"11"}
Response:
{"code":1,"data":"修改分类信息成功","msg":null,"map":{}}
RESTful 语义对照
| 操作 | 方法 | 路径 | 请求体 / 参数 |
|---|---|---|---|
| 新增 | POST |
/category |
{"name":"川菜","type":1,"sort":10} |
| 修改 | PUT |
/category |
{"id":"1397844303408570369","name":"123","sort":"11"} |
| 删除 | DELETE |
/category?id=xxx |
无 |
| 查询 | GET |
/category/page |
?page=1&pageSize=10 |
POST 与 PUT 路径相同但语义不同:POST 表示在集合中创建新资源,PUT 表示更新已有资源。区分依据是 HTTP 方法,而不是 URL。
updateById 的行为
MyBatis-Plus 的 updateById(T entity) 有一个重要特性:只对非 null 字段生成 SET 子句。
默认策略是 FieldStrategy.NOT_NULL,即:
java
Category category = new Category();
category.setId(1397844303408570369L);
category.setName("川菜");
categoryService.updateById(category);
生成的 SQL:
sql
UPDATE category SET name='川菜' WHERE id=1397844303408570369
type、sort 因为没有赋值(保持 null),不会出现在 SET 里。这避免了把未修改的字段覆盖成 null。
如何显式更新为 null
如果确实需要把某个字段置空,默认策略下做不到。有三种办法:
| 方案 | 写法 | 影响范围 |
|---|---|---|
| 字段级注解 | @TableField(updateStrategy = FieldStrategy.IGNORED) |
仅该字段 |
| 全局配置 | update-strategy: ignored |
全部字段 |
用 UpdateWrapper |
new LambdaUpdateWrapper<Category>().set(Category::getName, null) |
单次调用 |
推荐第三种,影响面最小:
java
LambdaUpdateWrapper<Category> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Category::getId, id).set(Category::getName, null);
categoryService.update(wrapper);
公共字段的自动回填
不写一行 set 的奥秘
修改分类时,updateTime 和 updateUser 需要更新为当前时间与当前操作人。但 Controller 里没有 category.setUpdateTime(...),为什么生效?
答案在第 28 篇实现的 MyMetaObjecthandler:
java
package com.itheima.reggie.common;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* 自定义元数据对象处理器
*/
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
/**
* 插入操作时自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
/**
* 更新操作时自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
log.info(metaObject.toString());
long id = Thread.currentThread().getId();
log.info("线程id为:{}", id);
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
触发条件
自动填充生效需要两个条件同时满足:
#mermaid-svg-mtNy4lOTdToJvTVn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mtNy4lOTdToJvTVn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mtNy4lOTdToJvTVn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mtNy4lOTdToJvTVn .error-icon{fill:#552222;}#mermaid-svg-mtNy4lOTdToJvTVn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mtNy4lOTdToJvTVn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mtNy4lOTdToJvTVn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mtNy4lOTdToJvTVn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mtNy4lOTdToJvTVn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mtNy4lOTdToJvTVn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mtNy4lOTdToJvTVn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mtNy4lOTdToJvTVn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mtNy4lOTdToJvTVn .marker.cross{stroke:#333333;}#mermaid-svg-mtNy4lOTdToJvTVn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mtNy4lOTdToJvTVn p{margin:0;}#mermaid-svg-mtNy4lOTdToJvTVn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mtNy4lOTdToJvTVn .cluster-label text{fill:#333;}#mermaid-svg-mtNy4lOTdToJvTVn .cluster-label span{color:#333;}#mermaid-svg-mtNy4lOTdToJvTVn .cluster-label span p{background-color:transparent;}#mermaid-svg-mtNy4lOTdToJvTVn .label text,#mermaid-svg-mtNy4lOTdToJvTVn span{fill:#333;color:#333;}#mermaid-svg-mtNy4lOTdToJvTVn .node rect,#mermaid-svg-mtNy4lOTdToJvTVn .node circle,#mermaid-svg-mtNy4lOTdToJvTVn .node ellipse,#mermaid-svg-mtNy4lOTdToJvTVn .node polygon,#mermaid-svg-mtNy4lOTdToJvTVn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mtNy4lOTdToJvTVn .rough-node .label text,#mermaid-svg-mtNy4lOTdToJvTVn .node .label text,#mermaid-svg-mtNy4lOTdToJvTVn .image-shape .label,#mermaid-svg-mtNy4lOTdToJvTVn .icon-shape .label{text-anchor:middle;}#mermaid-svg-mtNy4lOTdToJvTVn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mtNy4lOTdToJvTVn .rough-node .label,#mermaid-svg-mtNy4lOTdToJvTVn .node .label,#mermaid-svg-mtNy4lOTdToJvTVn .image-shape .label,#mermaid-svg-mtNy4lOTdToJvTVn .icon-shape .label{text-align:center;}#mermaid-svg-mtNy4lOTdToJvTVn .node.clickable{cursor:pointer;}#mermaid-svg-mtNy4lOTdToJvTVn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mtNy4lOTdToJvTVn .arrowheadPath{fill:#333333;}#mermaid-svg-mtNy4lOTdToJvTVn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mtNy4lOTdToJvTVn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mtNy4lOTdToJvTVn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mtNy4lOTdToJvTVn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mtNy4lOTdToJvTVn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mtNy4lOTdToJvTVn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mtNy4lOTdToJvTVn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mtNy4lOTdToJvTVn .cluster text{fill:#333;}#mermaid-svg-mtNy4lOTdToJvTVn .cluster span{color:#333;}#mermaid-svg-mtNy4lOTdToJvTVn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mtNy4lOTdToJvTVn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mtNy4lOTdToJvTVn rect.text{fill:none;stroke-width:0;}#mermaid-svg-mtNy4lOTdToJvTVn .icon-shape,#mermaid-svg-mtNy4lOTdToJvTVn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mtNy4lOTdToJvTVn .icon-shape p,#mermaid-svg-mtNy4lOTdToJvTVn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mtNy4lOTdToJvTVn .icon-shape .label rect,#mermaid-svg-mtNy4lOTdToJvTVn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mtNy4lOTdToJvTVn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mtNy4lOTdToJvTVn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mtNy4lOTdToJvTVn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 实体字段上有 @TableField(fill = ...)
自动填充生效
存在实现 MetaObjectHandler 的 Bean
调用的是 MyBatis-Plus 的 insert/update 方法
Category 实体中的声明:
java
//更新时间
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
//修改人
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
FieldFill.INSERT_UPDATE 表示插入和更新时都填充。如果这里漏了注解,字段就会是 null,触发 update_time 的非空约束报错。
FieldFill 取值对照
| 取值 | 插入时填充 | 更新时填充 |
|---|---|---|
DEFAULT |
否 | 否 |
INSERT |
是 | 否 |
UPDATE |
否 | 是 |
INSERT_UPDATE |
是 | 是 |
PUT 请求不传 updateTime 也没关系
前端请求体只有 id、name、sort,没有 updateTime。但因为 updateFill 是在服务端执行的,与前端传什么无关,所以字段依然被正确填充。这也是自动填充相对于「前端传时间戳」的明显优势------客户端时间不可信。
功能测试
修改流程
启动项目,进入分类管理,点击第一行的「修改」,把名称改成「123」,点确定。
控制台 SQL:
txt
==> Preparing: UPDATE category SET name=?, sort=?, update_time=?, update_user=? WHERE id=?
==> Parameters: 123(String), 11(Integer), 2026-09-11T09:28:15(LocalDateTime), 1(Long), 1397844303408570369(Long)
<== Updates: 1
注意 SET 子句中出现了 update_time 和 update_user,尽管 Controller 里一个字都没写。这两个值来自 updateFill。
验证 updateUser 的来源
update_user 为 1,是 BaseContext.getCurrentId() 的返回值。这个值由 LoginCheckFilter 在校验通过时写入:
java
// 4、判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("employee") != null) {
log.info("用户已登录,用户id为:{}", request.getSession().getAttribute("employee"));
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
filterChain.doFilter(request, response);
return;
}
如果你用另一个员工账号登录再修改,update_user 会变成那个员工的 id。
改回原值
测试完把「123」改回「香菜」,再次提交,控制台再发一条 UPDATE。数据恢复。
常见异常:没有 id
如果提交时 classData.id 为空(比如前端忘了在 editHandle 里赋值),updateById 会生成:
sql
UPDATE category SET name=?, sort=?, update_time=?, update_user=?
没有 WHERE 条件------全表更新 。MyBatis-Plus 对这种情况会抛出 MybatisPlusException: 无法执行. 未找到更新的条件,但如果是手写 XML 就可能酿成事故。
防御措施:
java
@PutMapping
public R<String> update(@RequestBody Category category){
if (category.getId() == null) {
return R.error("分类id不能为空");
}
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
完整可运行代码
目录结构
txt
reggie_take_out/
└── src/
├── main/
│ ├── java/com/itheima/reggie/
│ │ ├── common/
│ │ │ ├── R.java
│ │ │ ├── BaseContext.java
│ │ │ └── MyMetaObjecthandler.java 自动填充
│ │ ├── entity/
│ │ │ └── Category.java
│ │ ├── mapper/
│ │ │ └── CategoryMapper.java
│ │ ├── service/
│ │ │ ├── CategoryService.java
│ │ │ └── impl/CategoryServiceImpl.java
│ │ └── controller/
│ │ └── CategoryController.java
│ └── resources/
│ └── backend/page/category/list.html 分类管理页面
└── test/
CategoryController.java(含修改方法)
java
package com.itheima.reggie.controller;
import com.itheima.reggie.common.R;
import com.itheima.reggie.entity.Category;
import com.itheima.reggie.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 分类管理
*/
@RestController
@RequestMapping("/category")
@Slf4j
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 根据id修改分类信息
* @param category
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category);
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
}
Category.java
java
package com.itheima.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 分类
*/
@Data
public class Category implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//类型 1 菜品分类 2 套餐分类
private Integer type;
//分类名称
private String name;
//顺序
private Integer sort;
//创建时间
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
//更新时间
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
//创建人
@TableField(fill = FieldFill.INSERT)
private Long createUser;
//修改人
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}
MyMetaObjecthandler.java
java
package com.itheima.reggie.common;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* 自定义元数据对象处理器
*/
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
/**
* 插入操作时自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]...");
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
/**
* 更新操作时自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
long id = Thread.currentThread().getId();
log.info("线程id为:{}", id);
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
BaseContext.java
java
package com.itheima.reggie.common;
/**
* 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 设置值
* @param id
*/
public static void setCurrentId(Long id){
threadLocal.set(id);
}
/**
* 获取值
* @return
*/
public static Long getCurrentId(){
return threadLocal.get();
}
}
前端 category/list.html 弹窗与提交
html
<el-dialog
:title="classData.title"
:visible.sync="dialogVisible"
width="30%"
>
<el-form
ref="classData"
:model="classData"
label-width="100px"
>
<el-form-item label="分类名称:" prop="name">
<el-input
v-model="classData.name"
placeholder="请输入分类名称"
maxlength="14"
/>
</el-form-item>
<el-form-item label="排序:" prop="sort">
<el-input
v-model="classData.sort"
placeholder="请输入排序"
type="number"
/>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitForm()">确 定</el-button>
</span>
</el-dialog>
前端 category.js
js
// 修改分类
function editCategory(params) {
return $axios({
url: '/category',
method: 'put',
data: { ...params }
})
}
API 速览
API |
所属框架 | 作用 |
|---|---|---|
@PutMapping |
Spring MVC |
限定 PUT 方法 |
@RequestBody |
Spring MVC |
将 JSON 请求体反序列化为对象 |
IService.updateById(T) |
MyBatis-Plus |
根据主键更新,只更新非 null 字段 |
IService.update(Wrapper) |
MyBatis-Plus |
按条件更新,可显式 set 为 null |
MetaObjectHandler |
MyBatis-Plus |
元对象处理器接口,实现自动填充 |
MetaObject.setValue(String, Object) |
MyBatis |
为实体属性设值 |
FieldFill.INSERT_UPDATE |
MyBatis-Plus |
插入与更新时均填充 |
@TableField(fill = ...) |
MyBatis-Plus |
标记字段的填充时机 |
FieldStrategy.NOT_NULL |
MyBatis-Plus |
默认更新策略,忽略 null 字段 |
R.success(String) |
本项目 | 返回成功响应 |
v-model |
Vue |
表单元素与数据的双向绑定 |
scope.row |
Element UI |
表格插槽暴露的当前行数据 |
官方文档
Spring MVC@PutMapping:官方文档MyBatis-Plus自动填充功能:官方文档MyBatis-PlusupdateById:官方文档MyBatis-PlusFieldStrategy:官方文档Vue表单输入绑定:官方文档
总结
这篇文章主要讲了分类管理里「修改」功能的前后端实现,核心就三件事:
前端回显不靠后端 。点「修改」时,editHandle(scope.row) 直接把列表里已有的整行数据塞给弹窗,靠 v-model 双向绑定自动显示,全程没有发请求。很多人以为要写个「根据 id 查询」的接口,其实不需要。
后端一行代码搞定更新 。@PutMapping + @RequestBody 接住 JSON,updateById(category) 就完事了。这里有个坑要注意:updateById 默认只更新非 null 字段,所以前端没传的字段不会被覆盖成空值,这是好事。但如果真想把某个字段置空,默认策略做不到,得用 LambdaUpdateWrapper 显式 set。
公共字段自动填充 。updateTime 和 updateUser 不用手动 set,MyMetaObjecthandler 的 updateFill 会在更新时自动补上。前提是实体字段上标了 @TableField(fill = FieldFill.INSERT_UPDATE),两个条件缺一不可。
最后提醒一句:提交时一定要保证 id 有值,否则 updateById 没有 WHERE 条件,会全表更新,这是很危险的操作。