计划
删除员工(批量删除,一个接口兼容单个/多个)
修改员工(先查询回显 + 再保存修改)
异常处理(全局异常处理器 @RestControllerAdvice)
员工信息统计(职位统计 + 性别统计)
笔记 1.删除员工 需求分析 勾选列表复选框,点击”批量删除”按钮,删除这一批员工信息;也可以只勾选一个,仅删除一个员工。
结论:只需开发一个功能接口 ,批量删除接口天然包含”只删除一个”的场景,无需单独开发删除单个的接口。
接口要求: 请求路径 /emps、请求方式 DELETE、请求参数为逗号拼接的 id(如 ids=1,2,3)。
Controller 接收参数的两种方式 方式一:数组接收 (默认可直接封装到数组中)
1 2 3 4 5 6 @DeleteMapping public Result delete (Integer[] ids) { log.info("批量删除员工: ids={}" , Arrays.asList(ids)); empService.deleteByIds(ids); return Result.success(); }
方式二:集合接收 (封装到 List 集合,需加 @RequestParam)
1 2 3 4 5 6 @DeleteMapping public Result delete (@RequestParam List<Integer> ids) { log.info("批量删除员工: ids={}" , ids); empService.deleteByIds(ids); return Result.success(); }
推荐使用集合方式 (List),因为基于集合操作其中的元素更加方便(如增删改查遍历)。本项目实际使用数组方式 Integer[] ids。
Service 层(事务控制) 删除员工既要删 emp 表中的基本信息,又要删 emp_expr 表中工作经历信息,多次操作数据库必须加事务控制。
1 2 3 4 5 6 7 8 @Transactional @Override public void deleteByIds (List<Integer> ids) { empMapper.deleteByIds(ids); empExprMapper.deleteByEmpIds(ids); }
Mapper 层(<foreach> 实现 in 查询) EmpMapper.xml:
1 2 3 4 5 6 7 <delete id ="deleteByIds" > delete from emp where id in <foreach collection ="ids" item ="id" open ="(" close =")" separator ="," > #{id} </foreach > </delete >
EmpExprMapper.xml: 同理按 emp_id in (...) 批量删除工作经历。
<foreach> 属性回顾:
属性
说明
collection
集合名称(方法参数名)
item
遍历出来的每一项
open
遍历开始前拼接的片段(如 ()
close
遍历结束后拼接的片段(如 ))
separator
每项之间的分隔符(如 ,)
注意:open、close、separator 属于字符串拼接,不要写成 #{open} 的形式 。
2.修改员工 修改分为两步:
查询回显 :根据ID查询员工详细信息,回填到页面表单
保存修改 :用户修改后提交,更新数据库
2.1 查询回显 思路 回显时既要查出员工基本信息,又要查出该员工的工作经历信息(一对多)。
方式一:两次查询 —— 分别查 emp 和 emp_expr,再在 Service 中组装(项目中有注释掉的代码)。
方式二:一条SQL多表查询 + resultMap 手动封装 (推荐,本项目使用)
1 2 3 4 5 6 7 8 select e.* , ee.id ee_id, ee.begin ee_begin, ee.end ee_end, ee.company ee_company, ee.job ee_job from emp e left join emp_expr ee on e.id = ee.emp_idwhere e.id = #{id};
resultMap 封装一对多结果 EmpMapper.xml:
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 <resultMap id ="empResultMap" type ="com.itheima.pojo.Emp" > <id column ="id" property ="id" /> <result column ="username" property ="username" /> <result column ="password" property ="password" /> <result column ="name" property ="name" /> <result column ="gender" property ="gender" /> <result column ="phone" property ="phone" /> <result column ="job" property ="job" /> <result column ="salary" property ="salary" /> <result column ="image" property ="image" /> <result column ="entry_date" property ="entryDate" /> <result column ="dept_id" property ="deptId" /> <result column ="create_time" property ="createTime" /> <result column ="update_time" property ="updateTime" /> <collection property ="exprList" ofType ="com.itheima.pojo.EmpExpr" > <id column ="ee_id" property ="id" /> <result column ="ee_company" property ="company" /> <result column ="ee_job" property ="job" /> <result column ="ee_begin" property ="begin" /> <result column ="ee_end" property ="end" /> <result column ="ee_empid" property ="empId" /> </collection > </resultMap > <select id ="getById" resultMap ="empResultMap" > select e.*, ee.id ee_id, ee.emp_id ee_empid, ee.begin ee_begin, ee.end ee_end, ee.company ee_company, ee.job ee_job from emp e left join emp_expr ee on e.id = ee.emp_id where e.id = #{id} </select >
要点:
一对多查询结果必须用 <resultMap> 手动封装
<resultMap> 中 <id> 标签映射主键(用于去重),<result> 映射普通字段
<collection> 用于封装一对多中的”多”,ofType 指定集合元素的类型
SQL 中给子表字段起别名(如 ee_id),与 resultMap 的 column 对应,避免与主表字段冲突
联查的员工基本信息在 e.* 中,工作经历通过别名列封装进 exprList
resultType vs resultMap 如何选择
场景
选择
查询返回字段名与实体属性名能直接对应
resultType
字段名与属性名对应不上,或实体属性复杂(如一对多、嵌套)
resultMap 手动封装
Controller / Service 1 2 3 4 5 6 @GetMapping("/{id}") public Result getInfo (@PathVariable Integer id) { log.info("根据id查询员工的详细信息" ); Emp emp = empService.getInfo(id); return Result.success(emp); }
2.2 保存修改 实现思路(三步)
根据ID更新员工基本信息(动态SQL,只更新有值的字段)
根据员工ID删除旧的工作经历信息
新增新的工作经历信息
为什么要”先删旧再增新”? 页面提交的是编辑后的整份工作经历列表,无法精确得知哪些是被删改的,最简单可靠的方式就是全删旧 + 全增新 。
Service 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Transactional @Override public void update (Emp emp) { emp.setUpdateTime(LocalDateTime.now()); empMapper.updateById(emp); empExprMapper.deleteByEmpIds(Arrays.asList(emp.getId())); Integer empId = emp.getId(); List <EmpExpr> exprList = emp.getExprList(); if (!CollectionUtils.isEmpty(exprList)) { exprList.forEach(empExpr -> empExpr.setEmpId(empId)); empExprMapper.insertBatch(exprList); } }
动态SQL更新(<set> + <if>) EmpMapper.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <update id ="updateById" > update emp <set > <if test ="username != null and username != ''" > username = #{username},</if > <if test ="password != null and password != ''" > password = #{password},</if > <if test ="name != null and name != ''" > name = #{name},</if > <if test ="gender != null" > gender = #{gender},</if > <if test ="phone != null and phone != ''" > phone = #{phone},</if > <if test ="job != null" > job = #{job},</if > <if test ="salary != null" > salary = #{salary},</if > <if test ="image != null and image != ''" > image = #{image},</if > <if test ="entryDate != null" > entry_date = #{entryDate},</if > <if test ="deptId != null" > dept_id = #{deptId},</if > <if test ="updateTime != null" > update_time = #{updateTime},</if > </set > where id = #{id} </update >
<set> 标签作用: 自动去掉更新语句中最后一个多余的逗号 (<where> 是去掉多余的 and/or)。
数字类型(Integer)判空只需 != null,字符串类型要同时判空字符串 != ''。
Controller:
1 2 3 4 5 6 @PutMapping public Result update (@RequestBody Emp emp) { log.info("修改员工信息, {}" , emp); empService.update(emp); return Result.success(); }
3.异常处理 问题分析 修改员工时手机号重复(违反唯一约束),服务端抛异常返回框架默认的错误JSON ,而不是我们约定的统一响应结果 Result,导致前端无法解析。
未做任何异常处理时,异常在分层架构中的传递:Mapper → Service → Controller → 框架 ,最终由框架返回不符合规范的JSON。
两种解决方案
方案
做法
评价
方案一
所有Controller所有方法 try…catch
代码臃肿,不推荐
方案二
全局异常处理器
简单、优雅,推荐
全局异常处理器 步骤:
定义一个类,类上加 @RestControllerAdvice —— 代表全局异常处理器
定义一个方法捕获异常,方法上加 @ExceptionHandler,通过 value 属性(或方法形参)指定捕获的异常类型
1 2 3 4 5 6 7 8 9 10 11 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler public Result ex (Exception e) { e.printStackTrace(); return Result.error("对不起,操作失败,请联系管理员" ); } }
关键注解:
@RestControllerAdvice = @ControllerAdvice + @ResponseBody,处理异常方法的返回值会转换为 JSON 响应给前端
@ExceptionHandler:指定可以捕获哪种类型的异常
项目中的增强版:处理具体异常(DuplicateKeyException) 本项目在全局异常处理器中重载了两个方法,Spring会按异常类型精确匹配 ,优先调用参数类型最匹配的处理方法(更精确的异常会覆盖通用 Exception 的处理)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler public Result handleException (Exception e) { log.error("全局异常处理:{}" , e.getMessage()); return Result.error("出错了,请联系管理员" ); } @ExceptionHandler public Result handleException (DuplicateKeyException e) { log.error("全局异常处理:{}" , e.getMessage()); String message = e.getMessage(); int index = message.indexOf("Duplicate entry" ); String duplicateKey = message.substring(index).split(" " )[2 ]; return Result.error(duplicateKey + "已存在" ); } }
要点: 出现异常后,全局异常处理器将异常捕获并统一包装成 Result 返回,前端可以正常解析并提示错误信息。
4.员工信息统计 报表(图形)制作主要靠前端引入 ECharts 等组件,服务端只负责提供数据 。
官网:https://echarts.apache.org/zh/index.html
4.1 职位统计 接口 GET /report/empJobData,返回封装好的 JobOption{jobList, dataList},分别对应 ECharts 的 X 轴分类和数据。
JobOption 封装类 1 2 3 4 5 6 7 @Data @NoArgsConstructor @AllArgsConstructor public class JobOption { private List jobList; private List dataList; }
Controller / Service 1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController @RequestMapping("/report") public class ReportController { @Autowired private ReportService reportService; @GetMapping("/empJobData") public Result getEmpJobData () { log.info("统计各个职位的员工人数" ); JobOption jobOption = reportService.getEmpJobData(); return Result.success(jobOption); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Service public class ReportServiceImpl implements ReportService { @Autowired private EmpMapper empMapper; @Override public JobOption getEmpJobData () { List <Map<String,Object>> list = empMapper.countEmpJobData(); List <Object> jobList = list.stream().map(dataMap -> dataMap.get("pos" )).toList(); List <Object> dataList = list.stream().map(dataMap -> dataMap.get("total" )).toList(); return new JobOption (jobList, dataList); } }
Mapper(查询结果封装为 Map) 1 2 @MapKey("pos") List <Map<String,Object>> countEmpJobData () ;
EmpMapper.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 <select id ="countEmpJobData" resultType ="java.util.Map" > select (case job when 1 then '班主任' when 2 then '讲师' when 3 then '学工主管' when 4 then '教研主管' when 5 then '咨询师' else '其他' end) pos, count(*) total from emp group by job order by total </select >
要点:
查询结果每行是一个 Map,resultType="java.util.Map",字段别名即 Map 的 key(pos、total)
@MapKey 用于指定返回 Map 集合中每条记录的标识字段(可不指定)
4.2 性别统计 接口 GET /report/empGenderData,返回 List<Map>(ECharts 饼图需要的数据格式)。
Mapper 1 2 @MapKey("name") List <Map> countEmpGenderData () ;
EmpMapper.xml:
1 2 3 4 5 6 7 <select id ="countEmpGenderData" resultType ="java.util.Map" > select if(gender = 1, '男', '女') as name, count(*) as value from emp group by gender </select >
SQL 函数总结
函数
语法
作用
case语法一
case when cond1 then res1 [when cond2 then res2] else res end
条件成立取对应值,都不成立取 else
case语法二(等值匹配)
case expr when val1 then res1 [when val2 then res2] else res end
expr 等于 val1 取 res1,以此类推
if
if(条件, 条件为true取值, 条件为false取值)
三元判断,如 if(gender=1,'男','女')
ifnull
ifnull(expr, val1)
expr 不为 null 取自身,否则取 val1
5.核心知识点总结
知识点
说明
批量删除设计
一个接口兼容单个/多个,无需分开开发
数组接收参数
Integer[] ids,前端参数名与形参名一致即可
集合接收参数
@RequestParam List<Integer> ids,推荐使用
<foreach>
动态拼接 in 查询,collection/item/open/close/separator
修改员工两步
① 根据ID查询回显 ② 保存修改
查询回显SQL
left join emp_expr 一次查出基本信息+工作经历
resultMap + collection
一对多结果手动封装,ofType 指定集合元素类型
resultType vs resultMap
字段能对上用 resultType;字段对不上或结构复杂用 resultMap
动态更新 <set>
自动去掉最后一个多余的逗号
更新经历三步
更新基本信息 + 删除旧经历 + 新增新经历
@RestControllerAdvice
全局异常处理器 = @ControllerAdvice + @ResponseBody
@ExceptionHandler
指定捕获的异常类型,可按异常类型重载多个处理方法
DuplicateKeyException
主键/唯一约束冲突异常,可提取重复值提示”xxx已存在”
报表开发模式
前端用 ECharts,后端只提供数据
JobOption
职位统计封装类(jobList + dataList)
@MapKey
指定返回Map集合的标识字段(可不指定)
case 函数
流程控制,两种语法(条件式/等值式)
if 函数
if(条件, true值, false值),如性别转男/女
ifnull 函数
expr 为 null 时取默认值
count + group by
统计场景的核心 SQL 组合