day14 员工管理实战-删除修改员工,异常处理,员工统计

计划

  1. 删除员工(批量删除,一个接口兼容单个/多个)
  2. 修改员工(先查询回显 + 再保存修改)
  3. 异常处理(全局异常处理器 @RestControllerAdvice)
  4. 员工信息统计(职位统计 + 性别统计)

笔记

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) {
// 1. 根据ID批量删除员工基本信息
empMapper.deleteByIds(ids);
// 2. 根据员工ID批量删除工作经历信息
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 每项之间的分隔符(如 ,

注意:opencloseseparator 属于字符串拼接,不要写成 #{open} 的形式


2.修改员工

修改分为两步:

  1. 查询回显:根据ID查询员工详细信息,回填到页面表单
  2. 保存修改:用户修改后提交,更新数据库

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_id
where 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-->
<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" />

<!--封装一对多中的"多":exprList-->
<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>

<!--根据ID查询员工的详细信息-->
<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 保存修改

实现思路(三步)
  1. 根据ID更新员工基本信息(动态SQL,只更新有值的字段)
  2. 根据员工ID删除旧的工作经历信息
  3. 新增新的工作经历信息

为什么要”先删旧再增新”? 页面提交的是编辑后的整份工作经历列表,无法精确得知哪些是被删改的,最简单可靠的方式就是全删旧 + 全增新

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) {
// 1. 根据ID更新员工基本信息
emp.setUpdateTime(LocalDateTime.now());
empMapper.updateById(emp);

// 2. 根据员工ID删除员工的工作经历信息 【删除老的】
empExprMapper.deleteByEmpIds(Arrays.asList(emp.getId()));

// 3. 新增员工的工作经历数据 【新增新的】
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
<!--根据ID更新员工信息-->
<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 代码臃肿,不推荐
方案二 全局异常处理器 简单、优雅,推荐
全局异常处理器

步骤:

  1. 定义一个类,类上加 @RestControllerAdvice —— 代表全局异常处理器
  2. 定义一个方法捕获异常,方法上加 @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(); // 打印堆栈中的异常信息
// 捕获到异常之后,响应一个标准的Result
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 + "已存在"); // 例如:13309090027已存在
}
}

要点: 出现异常后,全局异常处理器将异常捕获并统一包装成 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; // X轴:职位名称列表
private List dataList; // Y轴:对应人数列表
}
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();
// 用Stream分别提取"职位名"和"人数"两个列表
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")   // 指定返回Map的唯一标识字段【也可以不指定】
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(postotal
  • @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 组合