员工管理-批量删除
批量删除一是要利用<foreach></foreach>实现我们数组的sql语句拼接
并且逻辑外键中,我们不光要删除员工表的数据,还要删除员工工作经历表的数据(都是根据员工id)
1.请求参数的接收
既然是批量删除,所以请求参数一般都是ids=1,2,3...这种一般我们在Controller层拿数组或集合接
既然谈到批量,那么不然想到我们的xml配置文件中可以用循环<foreach></foreach>的方式
2.Controller层-数组/集合封装参数
方法一:
用数组来接收,如果前端参数名和形参数组名一样,则无需@RequestParam绑定
方法二(推荐):
用集合在来接收,无论如何都要加@RequestParam绑定
3.Service层
注意逻辑外键的处理即可,也就是员工表和员工经历表都是要协同着删除,并且他们处于一个事务@Transational当中,要么全成功要么全失败。
4.Mapper层
根据原sql语句配置xml
delete from emp where id in (1,2,3....)
xml配置如下(员工经历表类似)
<delete id="delete"> delete from emp where id in <foreach collection="ids" item="id" separator=","open="(" close=")"> #{id} </foreach> </delete>
员工管理-修改
1.查询回显:MyBatis 一对多查询:使用 ResultMap 实现员工信息 + 工作经历封装
(1)业务场景
一个员工对应多条工作经历(一对多关系)。
查询员工详细信息时,需要将员工基本信息和关联的工作经历列表(EmpExpr)封装到一个Emp对象中。
Emp对象中包含一个List<EmpExpr> exprList属性EmpExpr是独立的工作经历实体,外键emp_id指向emp.id
(2)SQL 查询的问题
sql
select e.*, ee.id ee_id, ee.emp_id ee_empid, ee.company ee_company, ee.job ee_job, ee.begin ee_begin, ee.end ee_end from emp e left join emp_expr ee on e.id = ee.emp_id where e.id = #{id}如果一个员工有两条工作经历,这条 SQL 会返回两行记录:
| e.id | e.name | ee.id | ee.company |
|---|---|---|---|
| 1 | 张三 | 10 | 阿里 |
| 1 | 张三 | 11 | 腾讯 |
但我们希望的Emp对象结构是:
json
{ "id": 1, "name": "张三", "exprList": [ { "id": 10, "company": "阿里" }, { "id": 11, "company": "腾讯" } ] }直接使用resultType无法自动完成这种一对多封装,因此需要自定义resultMap。
(3)解决方案:自定义 ResultMap
[1] Mapper 接口
java
@Mapper public interface EmpMapper { Emp getById(Integer id); }[2] XML 映射文件(核心)
xml
<resultMap id="empResultMap" type="com.itheima.pojo.Emp"><!-- 主键必须用 id 标签 --> <id column="id" property="id" /> <!-- 普通字段用 result 标签 --> <result column="username" property="username" /> <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> <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>
(4)关键点解释
[1]resultMap中的type
xml
<resultMap id="empResultMap" type="com.itheima.pojo.Emp">
表示最终要封装的 Java 对象类型是Emp。
[2] <id>vs<result>
| 标签 | 说明 |
|---|---|
<id> | 映射数据库主键列,MyBatis 会用它来区分不同对象 |
<result> | 映射普通字段 |
在一对多场景中,<id>帮助 MyBatis 判断哪些行属于同一个主对象,否则可能出现重复或死循环。
[3]<collection>标签
xml
<collection property="exprList" ofType="com.itheima.pojo.EmpExpr">
| 属性 | 说明 |
|---|---|
property | Emp 对象中存放集合的属性名 |
ofType | 集合中每个元素的类型(泛型) |
其内部子标签和普通字段一样,column是 SQL 返回的列名,property是EmpExpr的属性名。
[4] 为什么给工作经历字段起了别名?
sql
select ee.id ee_id, ee.company ee_company
emp表和emp_expr表中都有id字段,不取别名会冲突方便在
resultMap中通过ee_id、ee_company区分来源
(5)总结(resultType和resultMap)
| 场景 | 使用方式 |
|---|---|
| 单表查询 / 字段名和属性名一致 | resultType+ 开启驼峰映射 |
| 字段名和属性名不一致 | resultMap或@Results |
| 一对多 / 一对一 / 集合嵌套 | 必须使用resultMap+<collection> |
核心思想:
SQL 负责多表关联查出“扁平化”的数据,resultMap负责将扁平数据“重组”为带有集合的层次化对象。
(6)踩过的坑(仅供参考)
忘记设置
ofType→ 集合里是 Map,不是 EmpExpr 对象<id>标签没有定义→ 集合可能重复或顺序错乱SQL 列名和
resultMap的column不一致→ 值取不到,属性为 null主对象字段也用
<result>不用<id>→ 轻微性能问题,但通常无感
2.修改数据—<set>
{
"id": 2,
"username": "zhangwuji",
"name": "张无忌",
"gender": 1,
"image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg",
"job": 2,
"salary": 8000,
"entryDate": "2015-01-01",
"deptId": 2,
"exprList": [
{
"begin": "2012-07-01",
"end": "2015-06-20",
"company": "中软国际股份有限公司",
"job": "java开发"
},
{
"begin": "2015-07-01",
"end": "2019-03-03",
"company": "百度科技股份有限公司",
"job": "java开发"
}
]
}
对于正常员工表基本信息的修改不进行过多赘述
主要强调修改员工工作经历信息则分为-先删再加
(1)Controller层
@PutMapping public Result update(@RequestBody Emp emp){ log.info("修改员工信息:{}", emp); empService.update(emp); return Result.success(); }@RequestBody接受前端json格式数据
(2)Service层
@Override @Transactional(rollbackFor = Exception.class) public void update(Emp emp) { //1.根据ID修改员工基本信息 emp.setUpdateTime(LocalDateTime.now()); empMapper.update(emp); //2.根据ID修改员工工作经历信息 empExprMapper.deleteByEmpId(emp.getId());---先删 List<EmpExpr> exprList = emp.getExprList(); if (!CollectionUtils.isEmpty(exprList)) { //要先给每个员工工作经历的empId,因为前端传来的json数据是不包含empId的,这里需要我们业务层自己处理exprList.forEach(empExpr -> empExpr.setEmpId(emp.getId()));empExprMapper.insertBatch(exprList);--批量加入,调用之前我们写过的mapper接口 } }(3)Mapper层
<update id="update"> 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="createTime != null"> create_time = #{createTime}, </if> </set> where id = #{id} </update>我们在这里需要动态的根据传入进来的json数据来进行更新操作,这里类似于条件查询
所以我们需要引入<where></where>这个标签,并且对于String类型不光要判断是否为null,还需要判断是否为空字符串
(4)<where>vs<set>对比
| 标签 | 作用 | 解决的问题 |
|---|---|---|
<where> | 动态生成 WHERE 子句 | 自动去掉第一个AND或OR |
<set> | 动态生成 SET 子句 | 自动去掉最后一个多余的逗号 |
但这个有个非常有意思的点,就是where的确是可以根据情况删除的,因为where属于sql语句中的可选条件,但是set其实是DML语句中不可省略的即
update 表名 set 更新内容 where 条件筛选
解决办法这里有一种即在<set>里加一个“永远为 true 但无副作用”的条件:
我们的updateTime已经在Service层进行赋值了,所以无论如何都可以在这里加上
<set>update_time = #{updateTime},<if test="username != null and username != ''"> username = #{username}, </if> <!-- 其他字段 --> </set>全局异常处理器
为什么要使用全局异常处理器?
设置全局异常处理器,是为了把“异常处理”从业务代码中抽离出来,避免 try-catch 散落在各处,让代码专注于业务逻辑。
我们在Controller层统一的捕获异常,如果在每个Controller层的每个方法都是用try catch来捕获响应异常,那代码就会变得十分臃肿难以维护,因此我们使用全局异常处理器来捕获异常
如何定义全局异常处理器?
定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。
在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。
@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { // 处理异常 @ExceptionHandler public Result ex(Exception e){// 方法形参中指定能够处理的异常类型 log.error("程序出错",e); // 捕获到异常之后,响应一个标准的Result return Result.error("出错了请联系管理员"); } }核心注解
| 注解 | 作用 | 位置 |
|---|---|---|
@RestControllerAdvice | 标识这是一个全局异常处理类,同时自动将返回值转成 JSON | 类上 |
@ExceptionHandler | 标识方法处理哪种异常 | 方法上 |
记忆公式:
@RestControllerAdvice= @ControllerAdvice +@ResponseBody@ExceptionHandler(异常类.class)= 这个方法专门处理这种异常
执行流程
text
Controller 抛出异常 ↓ Spring 拦截异常,根据异常类型匹配 ↓ 找到对应的 @ExceptionHandler 方法 ↓ 执行该方法,返回 Result 对象 ↓ 自动转成 JSON 返回给前端
分析设计异常处理方法
我们需要分析需要捕获的异常类型并且给前端合适的提示信息
@ExceptionHandler(DuplicateKeyException.class) public Result handlerDuplicateKeyException(DuplicateKeyException e){ // 1. 记录错误日志,方便开发人员排查问题(控制台会输出红色的堆栈信息) log.error("数据库操作出错", e); // 2. 获取异常的详细消息 // 示例:Duplicate entry '13309090027' for key 'emp.phone' String msg = e.getMessage(); // 3. 找到 "Duplicate entry" 这个字符串开始的位置 //目的:截取从异常描述开始的部分,去掉前面的类名等信息int i = msg.indexOf("Duplicate entry"); // 4. 从 "Duplicate entry" 开始截取到最后 // 截取后:Duplicate entry '13309090027' for key 'emp.phone' String errMsg = msg.substring(i); // 5.按空格分割字符串// 分割后得到数组:["Duplicate", "entry", "'13309090027'", "for", "key", "'emp.phone'"] String[] s = errMsg.split(" "); // 6. 取数组的第3个元素(索引2),就是重复的具体值 // 注意:数组索引0是"Duplicate",1是"entry",2就是那个被单引号包着的重复值 // 取到:'13309090027' String phone = s[2]; // 7. 把重复的值拼接到提示语中,返回给前端用户 // 用户看到:'13309090027'已存在! return Result.error(phone + "已存在!"); }这里的处理方法只是一个样例,真实的业务逻辑肯定有更为细分的比如:用户名重复,身份证重复等等
异常捕获方法规则
异常处理器的捕获规则是从下往上(从子类到父类)查找是否有方法进行捕获
员工信息统计Echars
对于这些图形报表的开发,其实呢,都是基于现成的一些图形报表的组件开发的,比如:Echarts、HighCharts等。
而报表的制作,主要是前端人员开发,引入对应的组件(比如:ECharts)即可。 服务端开发人员仅为其提供数据即可。
官网:https://echarts.apache.org/zh/index.html
报表制作分析
在网站中找到我们要制作的报表类型,这里以基础柱状图为例
左侧为数据类型,不难看出来即x轴即数据目录名,y轴对应每列的数据值
给前端返回的json格式数据即
{
"code": 1,
"msg": "success",
"data": {
"jobList": ["教研主管","学工主管","其他","班主任","咨询师","讲师"],
"dataList": [1,1,2,6,8,13]
}
}
给前端的返回为两个集合,对应了我们基础柱状图的xy轴数据,所以我们需要再创建一个类进行数据封装
报表制作实现-sql语句-case
创建JobOption类封装数据
/**
* 员工职位人数统计
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JobOption {
private List jobList; //职位列表
private List dataList; //人数列表
}
由于我们的job在数据库层面记录的是Integer类型,而我们想返回给前端的是与之相对应的职称名字,所以我们可以使用sql语句提供的case语法,这里我们给出case语句的两种写法
| 写法 | 语法 | 适用场景 | 示例 |
|---|---|---|---|
| 简单 CASE | CASE字段WHEN 值 THEN 结果 ... (ELSE 值)可选END | 字段等于某个固定值 | CASEjobWHEN 1 THEN '班主任' WHEN 2 THEN '讲师' END |
| 搜索 CASE | CASE WHEN条件THEN 结果 ... (ELSE 值)可选END | 范围判断、复杂条件 | CASE WHENage < 18THEN '未成年' WHEN age < 60 THEN '成年' END |
编写sql语句统计每个职位的人数
-- 根据职位进行统计 简单CASE select casejobwhen 1 then '班主任' when 2 then '讲师' when 3 then '学工主管' when 4 then '教研主管' when 5 then '咨询师' else '其他' end, count(*) num from emp group by job order by num;搜索CASE
select case whenjob = 1then '班主任' when job = 2 then '讲师' when job = 3 then '学工主管' when job = 4 then '教研主管' when job = 5 then '咨询师' else '其他' end, count(*) num from emp group by job order by num;(1)Mapper层
/** * 统计各个职位的员工人数 */ List<Map<String,Object>> countEmpJobData()xml配置
<!-- 统计每个职位的数量--> <!-- 统计各个职位的员工人数 --> <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(*)totalfrom emp group by job order by total </select>
为什么是List<Map<String, Object>>?
List:SQL 查询出来的是多行数据,所以用List装每一行。Map:每一行数据有多个列,用Map的键值对来存——列名作 Key,列值作 Value
这里如果你的resultType是map集合,则每一项的(Key,value)都是(列名,值)
String:列名是字符串(比如"pos"、"total"),所以 Key 的类型是String。Object:列值可能是字符串(职位名),也可能是数字(人数),所以 Value 的类型用Object统一接收。
一句话总结:List管“行数”,Map管“列数”,String管“列名”,Object管“列值”。
这是一种通用的、灵活的数据结构,专门用来接收那些没有对应 Java 实体类的查询结果(比如统计报表)。
比如我们这项查询出来的List<Map<String,Object>>就是这样的
[ (pos,班主任), (total,7), (pos,讲师), (total,13), (pos,咨询师), (total,8) ]
(2)Service层
@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); } }这里用stream流提取,把list集合的每一项map集合拿出来,并且提取其列名所对应的值进行封装
(3)Controller层
@Slf4j @RestController @RequestMapping("/report") public class ReportController { @Autowired private ReportService reportService; @GetMapping("/empJobData") public Result getEmpJobData(){ log.info("生成报表"); JobOption empJobData = reportService.getEmpJobData(); return Result.success(empJobData); } }<!--统计每个性别的人数--> <select id="countEmpGenderData" resultType="java.util.Map"> selectif(gender = 1, '男', '女')as name, count(*) as value from emp group by gender ; </select>if(条件判断,true_value,false_value)
数据转换过程
| Java 对象 | Jackson 自动转换 | JSON 格式 |
|---|---|---|
List | → | [ ] |
Map | → | { } |
map.put("pos", "班主任") | → | "pos": "班主任" |
map.put("total", 7) | → | "total": 7 |
举例
java
// Java 中的 List<Map> [ {"pos": "班主任", "total": 7}, {"pos": "讲师", "total": 13} ]Jackson 自动转成前端收到的 JSON:
json
[ { "pos": "班主任", "total": 7 }, { "pos": "讲师", "total": 13 } ]总结
Spring Boot 默认集成了 Jackson,会自动把 Java 对象(List、Map、实体类)转成 JSON 格式返回给前端。你不需要写任何转换代码。