一、准备工作
1、开发规范-开发模式
当前最为主流的开发模式:前后端分离

开发流程

2、开发规范-Restful风格
REST(REpresentational State Transfer),表述性状态转换,它是一种软件架构风格。
(1)传统的接口风格
不同开发者使用不同的请求路径
/user/addUser /user/add /user/insertUser /user/insert /user/saveUser /user/save

(2)Restful风格
url固定,请求方式按规定给出

(3)注意
1.REST是风格,是约定方式,约定不是规定,可以打破。
2.描述功能模块通常使用复数形式(加s),表示此类资源,而非单个资源。如:users、books..
(4)小结
1.REST风格的特点?
URL定义资源
HTTP动词描述操作
2.REST风格中的四种请求方式及对应的操作?
GET:查询
POST:新增
PUT:修改
DELETE:删除
3、开发规范(Apifox)
(1)问题
前后端都在并行开发,
后端 开发完对应的接口之后,如何对接口进行请求测试呢?
前端 开发过程中,如何获取到数据,测试页面的渲染展示呢?
(2)Apifox
介绍:Apifox是一款集成了Api文档、Api调试、Api Mock、Api测试的一体化协作平台。
作用:接口文档管理、接口请求测试、Mock服务。
(3)为什么要使用Apifox
由于浏览器地址栏发起的请求,都是GET方式的请求,如果我们需要发起POST、PUT、DELETE方式的请求,就需要借助于这类工具
4、工程搭建
(1)搭建总过程
1.创建SpringBoot工程,并引入web开发起步依赖、mybatis、mysql驱动、lombok。
2.创建数据库表dept,并在application.yml中配置数据库的基本信息。
3.准备基础代码结构,并引入实体类Dept及统一的响应结果封装类Result。
(2)环境搭建
创建空项目,命名为web-ai-project02
检查JDK版本

检查Maven路径

编码字符集

(3)创建SpringBoot项目
创建并引入四项起步依赖

删掉没用的文件
(4)创建数据库表
sql
CREATE TABLE dept (
id int unsigned PRIMARY KEY AUTO_INCREMENT COMMENT 'ID,主键',
name varchar(10) NOT NULL UNIQUE COMMENT '部门名称',
create_time datetime DEFAULT NULL COMMENT '创建时间',
update_time datetime DEFAULT NULL COMMENT '修改时间'
) COMMENT '部门表';
INSERT INTO dept VALUES
(1,'学工部','2024-09-25 09:47:40','2024-09-25 09:47:40'),
(2,'教研部','2024-09-25 09:47:40','2024-09-09 15:17:04'),
(3,'咨询部','2024-09-25 09:47:40','2024-09-30 21:26:24'),
(4,'就业部','2024-09-25 09:47:40','2024-09-25 09:47:40'),
(5,'人事部','2024-09-25 09:47:40','2024-09-25 09:47:40'),
(6,'行政部','2024-11-30 20:56:37','2024-09-30 20:56:37');
(4)配置yml配置信息
TypeScript
# 配置数据库连接信息
spring:
application:
name: tlias-web-management
# 数据库连接信息
datasource:
url: jdbc:mysql://localhost:3306/tlias
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
# Mybatis的相关配置
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
(5)创建各包类
包类结构如下:

-
pojo包下
-
实体类Dept
javapackage com.itheima.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; @Data @NoArgsConstructor @AllArgsConstructor public class Dept { private Integer id; private String name; private LocalDateTime createTime; private LocalDateTime updateTime; } -
响应结果封装类Result
javapackage com.itheima.pojo; import lombok.Data; import java.io.Serializable; /** * 后端统一返回结果 */ @Data public class Result { private Integer code; //编码:1成功,0为失败 private String msg; //错误信息 private Object data; //数据 public static Result success() { Result result = new Result(); result.code = 1; result.msg = "success"; return result; } public static Result success(Object object) { Result result = new Result(); result.data = object; result.code = 1; result.msg = "success"; return result; } public static Result error(String msg) { Result result = new Result(); result.msg = msg; result.code = 0; return result; } }
-
-
业务层service
-
业务层接口DeptService
javapackage com.itheima.service; public interface DeptService { } -
接口实现类包impl
-
接口实现类DeptServiceImpl
javapackage com.itheima.service.impl; import com.itheima.service.DeptService; import org.springframework.stereotype.Service; @Service public class DeptServiceImpl implements DeptService { }
-
-
-
数据访问层mapper
-
数据访问接口DeptMapper
javapackage com.itheima.mapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface DeptMapper { }
-
-
控制层controller
-
DeptController文件
javapackage com.itheima.controller; import org.springframework.web.bind.annotation.RestController; @RestController public class DeptController { }
-
各层都需要引入不同的注解,总结如下:
java
//控制层交给IOC容器管理
//该注解包括两个注解
//@Controller将控制层交给IOC容器管理
//@ResponseBody将方法的返回值直接作为相应数据响应给前端,返回值是对象或集合会先转json
@RestController
//数据访问层交给IOC容器管理
@Mapper
//实体类中的注解代替了get、set方法;空参、有参构造
@Data
@NoArgsConstructor
@AllArgsConstructor
//业务层交给IOC容器管理
@Service
二、部门管理
1、列表查询-接口开发
(1)三层架构的每一层的职责

(2)功能实现

(3)具体功能实现代码
控制层:
java
@RestController
public class DeptController {
@Autowired
private DeptService deptService;
// @RequestMapping(value = "/depts", method = RequestMethod.GET)
//带请求方式的请求路径可以简化成以下方式
@GetMapping("/depts")
public Result list() {
System.out.println("查询全部部门数据");
List<Dept> deptList = deptService.findAll();
return Result.success(deptList);
}
}
业务层:
java
//接口
public interface DeptService {
/**
* 查询所有的部门数据
*/
List<Dept> findAll();
}
java
//实现类
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
public List<Dept> findAll() {
return deptMapper.findAll();
}
}
数据访问层:
java
@Mapper
public interface DeptMapper {
/**
* 查询所有的部门数据
* @return
*/
@Select("select id,name,create_time,update_time from dept order by update_time desc")
List<Dept> findAll();
}
(4)使用Apifox的快捷请求

使用GET请求获取JSON
2、列表查询-结果封装
(1)数据封装
实体类属性名 和数据库表查询返回的字段名一致,mybatis会自动封装。
如果实体类属性名和数据库表查询返回的字段名不一致 ,不能自动封装。
(2)存在的问题
实体类属性名和数据库表查询返回的字段名不一致

(3)解决方案
-
手动结果映射
java//方式一:使用@Results和@Result注解 @Results({ @Result(column = "create_time", property = "createTime"), @Result(column = "update_time", property = "updateTime") }) @Select("select id,name,create_time,update_time from dept order by update_time desc") List<Dept> findAll(); -
起别名
java@Select("select id,name,create_time createTime,update_time updateTime from dept order by update_time desc") List<Dept> findAll(); -
开启驼峰命名(推荐)
只需要在配置文件中输入camel回车即可,不需要修改数据访问层
但必须要遵守驼峰命名的规则(要求:xxx_abc-> xxxAbc)
TypeScript# Mybatis的相关配置 mybatis: configuration: # 开启驼峰命名转换 map-underscore-to-camel-case: true
3、列表查询-前后端联调测试
将资料中提供的前端工程文件夹中的压缩包,拷贝到一个没有中文不带空格的目录下,解压。
启动nginx ,访问测试:http://localhost:90
在浏览器开发者工具F12中,我们可以看到


(1)使用Nginx的反向代理
前端工程请求服务器的地址为http://localhost:90/是如何访问到后端的tomcat服务器的?

反向代理是一种网络架构,通过代理服务器为后端的服务器做代理 ,客户端的请求直接请求代理服务器 ,然后转发给后端的服务器(安全、灵活、负载均衡)
(2)具体代理方式
在Nginx服务器的conf文件中有这段代码

1.location:用于定义匹配路径匹配的规则。
2.^~/api/:表示精确匹配 ,即只匹配以/api/开头的路径。
3.rewrite:该指令用于重写匹配到的路径。
4.proxy_pass:该指令用于代理转发 ,它将匹配到的请求转发给位于后端的指令服务器。
Nginx中反向代理的配置

4、删除部门-接口开发(参数接收)
如何接收前端传递过来的简单参数
(1)方式一:使用HttpServletRequest获取参数
java
@DeleteMapping("/depts")
public Result delete(HttpServletRequest request) {
String idStr = request.getParameter("id");
Integer id = Integer.parseInt(idStr);
System.out.println("删除部门:" + id);
return Result.success();
}
(2)方式二:使用@RequestParam获取参数
注意事项:一旦声明了@RequestParam,该参数在请求时必须传递,如果不传递将会报错,(默认required为true)
java
@DeleteMapping("/depts")
public Result delete(@RequestParam(value = "id", required = false) Integer deptId){
System.out.println("删除部门:" + deptId);
return Result.success();
}
(3)方式三:直接定义方法形参(省略@RequestParam)(推荐)
如果请求参数名 与形参变量名相同
java
@DeleteMapping("/depts")
public Result delete(Integer id){
System.out.println("删除部门:" + id);
return Result.success();
}
5、删除部门-接口开发(功能实现)
业务层接口
java
void deleteById(Integer id);
业务层实现类
java
@Override
public void deleteById(Integer id) {
deptMapper.deleteById(id);
}
数据访问层
java
@Delete("delete from dept where id = #{id}")
void deleteById(Integer id);
6、新增部门-接口开发
控制层
java
/**
* 新增部门
*/
@PostMapping("/depts")
public Result add(@RequestBody Dept dept){//通过实体对象接收JSON传递过来的参数
System.out.println("新增部门:" + dept);
deptService.add(dept);
return Result.success();
}
业务层接口
java
/**
* 添加部门数据
*/
void add(Dept dept);
业务层实现类
实现接口并补全基础属性
java
@Override
public void add(Dept dept) {
//补全基础属性createTime,updateTime
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.insert(dept);
}
数据访问层
java
@Insert("insert into dept(name,create_time,update_time) values(#{name},#{createTime},#{updateTime})")
void insert(Dept dept);
1.如何接收JSON格式的请求参数?
通常通过实体对象接收,保证json格式的键名与对象属性名保持一致,并添加@RequestBody注解
2.json格式的请求参数适用场景?
主要在POST、PUT请求 中,在请求体传递请求参数
7、修改部门-查询回显
控制层
若请求参数名与形参变量名相同,则可以省略
java
@PathVariable("id")-->@PathVariable
java
/**
* 根据id查询部门
*/
@GetMapping("/depts/{id}")
public Result getInfo(@PathVariable("id") Integer deptId){
System.out.println("根据id查询部门:" + deptId);
Dept dept = deptService.getById(id);
return Result.success(dept);
}
业务层
java
/**
* 根据id查询部门数据
*/
Dept getById(Integer deptId);
java
@Override
public Dept getById(Integer deptId) {
return deptMapper.getById(deptId);
}
数据访问层
java
/**
* 根据id查询部门数据
*/
@Select("select id,name,create_time createTime,update_time updateTime from dept where id = #{id}")
Dept getById(Integer deptId);
如何接收路径参数?
@PathVariable注解来声明获取的是路径参数
在url中是否可以携带多个路径参数呢,如:/depts/1/0??
可以,接收方式如下
java
@GetMapping("/depts/{id}/{sta}")
public Result getInfo(@PathVariable Integer id, @Pathvariable Integer sta){
//...
}
8、修改部门-修改数据
控制层
java
/**
* 修改部门
*/
@PutMapping
public Result update(@RequestBody Dept dept){//该注解用于将请求体中的json数据自动转换为java对象
// System.out.println("修改部门:" + dept);
log.info("修改部门:{}", dept);
deptService.update(dept);
return Result.success();
}
业务层
java
/**
* 修改部门数据
*/
void update(Dept dept);
java
@Override
public void update(Dept dept) {
//补全基础属性updateTime
dept.setUpdateTime(LocalDateTime.now());
//调用Mapper层的update方法
deptMapper.update(dept);
}
数据访问层
java
/**
* 修改部门数据
*/
@Update("update dept set name = #{name},update_time = #{updateTime} where id = #{id}")
void update(Dept dept);
9、修改部门-公共路径抽取
抽取前

抽取后

@RequestMapping注解可以加在哪儿?
-
类上(可选的)
-
方法上
-
一个完整的请求路径=类上+方法上的
三、日志技术
好比生活中的日记,可以记录你生活中的点点滴滴。
程序中的日志,是用来记录应用程序的运行信息、状态信息、错误信息等。
日志的功能:数据追踪、性能优化、问题排查、系统监控
常用的日志技术:
JUL:这是JavaSE平台提供的官方日志框架,也被称为JUL。配置相对简单,但不够灵活,性能较差。
Log4j:一个流行的日志框架,提供了灵活的配置选项,支持多种输出目标。
Logback:基于Log4j升级而来,提供了更多的功能和配置选项,性能优于Log4j。
slf4j(Simple Logging Facade for Java):简单日志门面,提供了一套日志操作的标准接口及抽象类,允许应用程序使用不同的底层日志框架。

1、Logback入门程序
(1)准备工作
引入logback的依赖(springboot项目中该依赖已传递)、配置文件logback.xml。
依赖已引入

resources文件夹下创建logback.xml文件
其中,将<root level="debug">改为<root level="off">即可关闭日志记录
该日志配置文件为简易配置文件,详细配置文件在下一节
XML
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %logger{50}: 最长50个字符(超出.切割) %msg:日志消息,%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 日志输出级别 -->
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
(2)记录日志
定义日志记录对象Logger,记录日志。
在test文件夹下创建LogTest.java文件
java
package com.itheima;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
public class LogTest {
private static final Logger log = LoggerFactory.getLogger(LogTest.class);
@Test
public void testLog(){
//System.out.println(LocalDateTime.now() + " : 开始计算...");
log.debug("开始计算...");
int sum = 0;
int[] nums = {1, 5, 3, 2, 1, 4, 5, 4, 6, 7, 4, 34, 2, 23};
for (int num : nums) {
sum += num;
}
//System.out.println("计算结果为: "+sum);
log.info("计算结果为: ", sum);
//System.out.println(LocalDateTime.now() + "结束计算...");
log.debug("结束计算...");
}
}
用private static final Logger log = LoggerFactory.getLogger(LogTest.class);来创建日志记录器;
用log.debug("开始计算...");、log.info("计算结果为: ", sum);来记录日志
2、Logback配置文件
配置文件名:logback.xml
该配置文件是对Logback日志框架输出的日志进行控制的,可以来配置输出的格式、位置及日志开关等。
常用的两种输出日志的位置:控制台、系统文件
完整的日志配置文件
XML
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d 表示日期,%thread 表示线程名,%-5level表示级别从左显示5个字符宽度,%logger显示日志记录器的名称,50代表名称长度, %msg表示日志消息,%n表示换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern>
</encoder>
</appender>
<!-- 系统文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件输出的文件名, %i表示序号 -->
<FileNamePattern>D:/tlias-%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<!-- 最多保留的历史日志文件数量 -->
<MaxHistory>30</MaxHistory>
<!-- 最大文件大小,超过这个大小会触发滚动到新文件,默认为 10MB -->
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d 表示日期,%thread 表示线程名,%-5level表示级别从左显示5个字符宽度,%msg表示日志消息,%n表示换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern>
</encoder>
</appender>
<!-- 日志输出级别 -->
<root level="ALL">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>
在"日志输出级别"块中修改要输出哪些日志(控制台或文件)
日志输出效果

3、Logback日志级别
日志级别指的是日志信息的类型,日志都会分级别,常见的日志级别如下(级别由低到高):

可以在配置文件中,灵活的控制输出那些类型的日志。
XML
<!-- 日志输出级别 -->
<!--比info级别高的日志可以输出-->
<root level="info">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
回归项目,在类上加上该行代码,引入日志记录器
java
private static final Logger log = LoggerFactory.getLogger(LogTest.class);
或直接在类上加上@Slf4j注解
四、多表关系
1、一对多
在多的一方加一个字段,关联一的一方的主键即可

2、外键约束
(1)问题与解决方案
现象:部门数据可以直接删除,然而还有部分员工归属于该部门下,此时就出现了数据的不完整、不一致问题。
原因:目前上述的两张表,在数据库层面,并未建立关联,所以是无法保证数据的一致性和完整性的。
解决方案:外键约束。

数据库中的外键约束的作用?
多表操作中保证数据的一致性、完整性和正确性
(2)SQL语法
sql
-- 创建表时指定
create table 表名(
字段名 数据类型,
...
[constraint] [外键名称] foreign key (外键字段名) references 主表 (字段名)
);
-- 建表示例,并添加唯一约束
user_id int unsigned not null unique comment '用户ID',
constraint fk_user_id foreign key (user_id) references tb_user(id)
sql
-- 建完表后,添加外键
alter table 表名 add constraint 外键名称 foreign key (外键字段名) references 主表 (字段名);
alter table emp add constraint fk_emp_dept_id foreign key (dept_id) references dept(id);
(3)图形化界面添加外键约束


看到表字段前有一个小钥匙标识,代表外键添加成功

在这里可以看到外键关联情况的图标


(4)企业需求
-
物理外键
-
概念:使用 foreign key定义外键关联另外一张表。
-
缺点:
-
影响增、删、改的效率(需要检查外键关系)。
-
仅用于单节点数据库,不适用与分布式、集群场景。
-
容易引发数据库的死锁问题,消耗性能。
-
-
-
逻辑外键
-
概念:在业务层逻辑中,解决外键关联。
-
通过逻辑外键,就可以很方便的解决上述问题。
-
企业开发中不用物理外键,而是在业务逻辑的代码中实现外键约束
3、一对一
案例:用户与身份证信息的关系
关系:一对一关系,多用于单表拆分 ,将一张表的基础字段 放在一张表中,其他字段放在另一张表中,以提升操作效率
实现:在任意一方加入外键 ,关联另外一方的主键,并且设置外键为唯一的(UNIQUE)

sql
-- 建表时创建唯一约束和外键
user_id int unsigned not null unique comment '用户ID',
constraint fk_user_id foreign key (user_id) references tb_user(id)
4、多对多
案例:学生与课程的关系
关系:一个学生可以选修多门课程,一门课程也可以供多个学生选择
实现:建立第三张中间表 ,中间表至少包含两个外键,分别关联两方主键

sql
-- 在学生-课程关系表中
constraint fk_courseid foreign key (course_id) references tb_course (id),
constraint fk_studentid foreign key (student_id) references tb_student (id)
5、案例
在新增员工的表单 中

可以发现,员工和工作经历是一对多的关系
创建员工工作经历表
sql
create table emp_expr(
id int unsigned primary key auto_increment comment 'ID,主键',
begin date comment '开始时间',
end date comment '结束时间',
company varchar(50) comment '公司名称',
job varchar(50) comment '职位',
emp_id int unsigned comment '关联的员工ID'
)comment '工作经历表';
五、多表查询
1、概述
多表查询:指从多张表中查询数据
笛卡尔积:指在数学中,两个集合(A集合和B集合)的所有组合情况。(在多表查询时,需要消除无效的笛卡尔积)

-
连接查询
-
内连接:相当于查询A、B交集部分数据
-
外连接
-
左外连接:查询左表所有数据(包括两张表交集部分数据)
-
右外连接:查询右表所有数据(包括两张表交集部分数据)

-
-
-
子查询
2、内连接
内连接查询的是两张表交集部分的数据。具体语法为:
sql
-- 1.隐式内连接(常见)
select 字段列表 from 表1,表2 where 连接条件...;
-- 2.显式内连接
select 字段列表 from 表1 [inner] join 表2 on 连接条件...;
-- 1.给表起别名,来简化书写
select 字段列表 from 表1 [as] 别名1,表2 [as] 别名2 where 条件...;
语法中的\[\]表示可以省略的语法
注意: 一旦为表指定了别名,就要通过别名来指定字段名,而不能再使用表名了。
sql
-- ============================= 内连接 ==========================
-- A. 查询所有员工的ID, 姓名 , 及所属的部门名称 (隐式、显式内连接实现)
-- 隐式
select emp.id,emp.name,dept.name from emp,dept where emp.dept_id = dept.id;
-- 显式
select emp.id,emp.name,dept.name from emp inner join dept on emp.dept_id = dept.id;
select emp.id,emp.name,dept.name from emp join dept on emp.dept_id = dept.id;
-- B. 查询 性别为男, 且工资 高于8000 的员工的ID, 姓名, 及所属的部门名称 (隐式、显式内连接实现)
-- 隐式
select emp.id,emp.name,dept.name from emp,dept where emp.dept_id = dept.id and emp.gender = 1 and emp.salary > 8000;
-- 显式
select emp.id,emp.name,dept.name from emp inner join dept on emp.dept_id = dept.id where emp.gender = 1 and emp.salary > 8000;
select emp.id,emp.name,dept.name from emp join dept on emp.dept_id = dept.id where emp.gender = 1 and emp.salary > 8000;
-- 起别名
select e.id,e.name,d.name from emp as e inner join dept as d on e.dept_id = d.id where e.gender = 1 and e.salary > 8000;
3、外连接
sql
-- 1.左外连接(常见)
select 字段列表 from 表1 left [outer] join 表2 on 连接条件...;
-- 2.右外连接
select 字段列表 from 表1 right [outer] join 表2 on 连接条件 ...;
提示: 对于外连接,常用的是左外连接,因为右外连接的SQL也可以改造成为左外连接(两张表调换个顺序)。
左外连接就是保留左表的全部记录,右表无匹配数据时对应字段填NULL;
实现左外连接就把左表 写在left join左边
实现右外连接就把右表 写在right join左边
sql
-- =============================== 外连接 ============================
-- A. 查询员工表 所有 员工的姓名, 和对应的部门名称 (左外连接)
select e.name,d.name from emp e left join dept d on d.id = e.dept_id;
-- B. 查询部门表 所有 部门的名称, 和对应的员工名称 (右外连接)
select d.name,e.name from emp e right outer join dept d on e.dept_id = d.id;
select d.name,e.name from emp e right join dept d on e.dept_id = d.id;
-- C. 查询工资 高于8000 的 所有员工的姓名, 和对应的部门名称 (左外连接)
select e.name, d.name from emp e left join dept d on d.id = e.dept_id where salary > 8000;
4、子查询
介绍:SQL语句中嵌套select语句,称为嵌套查询,又称子查询。
形式:select * from t1 where column1 = (select column1 from t2..);
说明:子查询外部的语句可以是insert/update/delete/select的任何一个,最常见的是select。
分类:
1.标量子查询:子查询返回的结果为单个值
2.列子查询:子查询返回的结果为一列
3.行子查询:子查询返回的结果为一行
4.表子查询:子查询返回的结果为多行多列

提示:子查询的要点是,先对需求做拆分 ,明确具体的步骤 ,然后再逐步编写SQL语句。
sql
-- ========================= 子查询 ================================
-- 标量子查询
-- A. 查询 最早入职 的员工信息
-- a.获取到最早入职时间
select min(emp.entry_date) from emp;
-- b.查询 最早入职 的员工信息
select * from emp where entry_date = (select min(emp.entry_date) from emp);
-- B. 查询在 "阮小五" 入职之后入职的员工信息
-- a.查询"阮小五"的入职时间
select emp.entry_date from emp where name = '阮小五';
-- b.查询在 该时间之后 入职的员工信息
select * from emp where entry_date > (select emp.entry_date from emp where name = '阮小五');
-- 列子查询
-- A. 查询 "教研部" 和 "咨询部" 的所有员工信息
-- a.查询"教研部"和"咨询部"的部门ID
select id from dept where name = '教研部' or name = '咨询部';
-- b.查询指定部门ID的员工信息
select * from emp where dept_id in (select id from dept where name = '教研部' or name = '咨询部');
-- 行子查询
-- A. 查询与 "李忠" 的薪资 及 职位都相同的员工信息 ;
-- a.查询"李忠" 的薪资及职位
select emp.salary,emp.job from emp where name = '李忠';
-- b.查询指定薪资和职位的员工信息
select * from emp where salary = (select emp.salary from emp where name = '李忠') and job = (select emp.job from emp where name = '李忠');
-- 优化
select * from emp where (salary,job) = (select emp.salary,emp.job from emp where name = '李忠');
-- 表子查询
-- A. 获取每个部门中薪资最高的员工信息
-- a.获取每个部门的最高薪资
select dept_id,max(salary) from emp group by dept_id;
-- b.查询每个部门中薪资最高的员工信息
select * from emp e,(select dept_id,max(salary) max_sal from emp group by dept_id) a
where e.dept_id = a.dept_id and e.salary = a.max_sal;
5、案例
sql
-- 需求:
-- 1.查询 "教研部" 性别为 男,且在 "2011-05-01" 之后入职的员工信息。
-- 表: dept , emp
select e.* from emp e , dept d where e.dept_id = d.id and d.name = '教研部' and e.gender = 1 and e.entry_date > '2011-05-01';
-- 2.查询工资 低于公司平均工资的 且 性别为男 的员工信息。
-- 表: emp
-- 2.1 查询 公司平均工资
select avg(salary) from emp;
-- 2.2 查询工资 低于公司平均工资的 且 性别为男 的员工信息。
select * from emp where salary < 7548.2759 and gender = 1;
select * from emp where salary < (select avg(salary) from emp) and gender = 1;
-- 3.查询部门人数超过 10 人的部门名称。
-- 表: dept, emp
select d.name, count(*) from emp e, dept d where e.dept_id = d.id group by d.name having count(*) > 10;
-- 4.查询在 "2010-05-01" 后入职,且薪资高于 10000 的 "教研部" 员工信息,并根据薪资倒序排序。
-- 表: dept, emp
select e.* from emp e , dept d where e.dept_id = d.id and e.entry_date > '2010-05-01'
and e.salary > 10000 and d.name = '教研部' order by e.salary desc;
-- 5.查询工资 低于本部门平均工资 的员工信息。
-- 5.1 查询每个部门的平均工资
select dept_id, avg(salary) avg_sal from emp group by dept_id;
-- 5.2 查询工资 低于本部门平均工资 的员工信息。
select e.* from emp e , (select dept_id, avg(salary) avg_sal from emp group by dept_id) as a
where e.dept_id = a.dept_id and e.salary < a.avg_sal;
六、员工管理
1、准备工作
准备员工表和员工经历表,准备pojo实体类
其中数据库表使用下划线命名,实体类使用驼峰命名

准备数据访问层接口EmpMapper、EmpExprMapper
加上@Mapper注解

准备业务层接口和实现类EmpService、EmpServiceImpl
实现类加上@Service注解并继承接口
java
@Autowired
private EmpMapper empMapper;

准备控制层EmpController
加上@Slf4j、@RestController注解
java
@Autowired
private EmpService empService;

sql
-- 查询所有员工信息,体积该员工归属的部门名称
select e.*,d.name from emp e left join dept d on e.id = d.id
2、分页查询-原理分析
sql
-- 分页查询
-- 查询第1页,每页展示5条
select e.*,d.name from emp e left join dept d on e.id = d.id limit 0,5;
-- 总数据量
select count(*) from emp left join dept d on e.dept_id =d.id;
前后端需要传递的数据

后端创建实体类PageResult
java
@Data
public class PageResult {
private Long total;//总记录数
private List<T> rows;//当前页数据
}
3、分页查询-代码实现

数据访问层
java
@Select("select count(*) from emp e left join dept d on e.dept_id =d.id")
public Long count();
@Select("select e.*,d.name deptName from emp e left join dept d on e.id = d.id "+
"order by e.update_time desc limit #{start},#{pageSize}")
public List<Emp> list(Integer start,Integer pageSize);
控制层
@RequestParam注解可设默认值
java
@GetMapping
public Result page(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize){
log.info("分页查询:{},{}",page,pageSize);
PageResult<Emp> pageResult = empService.page(page,pageSize);
return Result.success(pageResult);
}
业务层实现类
java
@Override
public PageResult page(Integer page, Integer pageSize) {
//1、调用mapper接口,查询总记录数
Long total = empMapper.count();
//2、调用mapper接口,查询结果列表
Integer start = (page - 1)*pageSize;
List<Emp> rows = empMapper.list(start, pageSize);
//3、封装结果 PageResult
return new PageResult<Emp>(total,rows);
}
4、分页查询-PageHelper分页插件
使用步骤:
1.引入PageHelper的依赖
XML
<!--分页插件PageHelper-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.7</version>
</dependency>
2.定义Mapper接口的查询方法(无需考虑分页)
去掉了limit #{start},#{pageSize}"
java
@Select("select e.*,d.name deptName from emp e left join dept d on e.id = d.id "+
"order by e.update_time desc")
public List<Emp> list();
3.在Service方法中实现分页查询
java
@Override
public PageResult page(Integer page, Integer pageSize) {
//1. 设置分页参数
PageHelper.startPage(page, pageSize);
//2. 执行查询
List<Emp> empList = empMapper.list();//调用数据访问层查询
Page<Emp> p = (Page<Emp>) empList;//list类型强转为page类型
//3. 封装分页结果
return new PageResult<>(p.getTotal(), p.getResult());
}
5、分页查询-PageHelper实现机制
1.PageHelper实现机制
sql
select from emp e ...
-- 对sql语句做了以下改造
a.select count(o) from emp e ...
-- 和
b. select... from emp e...limit ?,?
2.注意事项
a.SQL语句结尾不要加分号(;)
b.PageHelper只会对紧跟在其后的第一条SQL语句进行分页处理
6、条件分页查询-基本实现
实现条件查询的sql语句样例
sql
select e.*,d.name deptName from emp e left join dept d on e.id = d.id
where e.name like '%阮%' and e.gender = 1 and entry_date between '2010-01-01' and '2020-01-01'
order by e.update_time desc
控制层
@DateTimeFormat用于指定传入的日期时间格式
java
@GetMapping
public Result page(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
String name, Integer gender,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
log.info("分页查询:,{},{},{},{},{},{}",page, pageSize, name, gender, begin, end);
PageResult<Emp> pageResult = empService.page(page, pageSize, name, gender, begin, end);
return Result.success(pageResult);
}
业务层
java
//加上控制层传递下来的形参
public PageResult<Emp> page(Integer page, Integer pageSize, String name, Integer gender, LocalDate begin, LocalDate end)
//调用数据访问层查询加上向下传递的实参
List<Emp> empList = empMapper.list(name, gender, begin, end);
数据访问层
可以用Mapper的映射文件写较为复杂的SQL语句
resources下创建目录com/itheima/mapper(创建目录用/分隔)
目录下创建EmpMapper.xml
XML
<?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.itheima.mapper.EmpMapper">
<select id="list" resultType="com.itheima.pojo.Emp">
select e.*,d.name deptName from emp e left join dept d on e.id = d.id
where e.name like concat('%',#{name},'%') and e.gender = #{gender} and entry_date between #{begin} and #{end}
order by e.update_time desc
</select>
</mapper>
由于#{}不能出现在''之间,编译器不可解析,所以
'%#{}%'占位符应替换为sql语句的一个函数concat('%',#{name},'%')
这样就可以拼接为占位符了
7、条件分页查询-程序优化一(参数接收)
实际开发中,参数过多不利于项目维护
可以将参数封装到一个实体类中
java
@Data
public class EmpQueryParam {
private Integer page = 1; //页码
private Integer pageSize = 10; //每页展示记录数
private String name; //姓名
private Integer gender; //性别
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate begin; //入职时间-开始
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate end; //入职时间-结束
}
从而控制层
java
@GetMapping
public Result page(EmpQueryParam empQueryParam){
log.info("分页查询:{}",empQueryParam);
PageResult<Emp> pageResult = empService.page(empQueryParam);
return Result.success(pageResult);
}
业务层
java
PageResult page(EmpQueryParam empQueryParam);
java
@Override
public PageResult<Emp> page(EmpQueryParam empQueryParam) {
//1. 设置分页参数
PageHelper.startPage(empQueryParam.getPage(),empQueryParam.getPageSize());
//2. 执行查询
List<Emp> empList = empMapper.list(empQueryParam);//调用数据访问层查询
数据访问层
java
public List<Emp> list(EmpQueryParam empQueryParam);
8、条件分页查询-程序优化二(动态SQL)
Mybatis中动态SQL的使用场景?
如果SQL语句是不固定的,是随着用户的输入或外部条件的变化而变化的
在数据访问层的映射文件中
XML
<?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.itheima.mapper.EmpMapper">
<select id="list" resultType="com.itheima.pojo.Emp">
select e.*,d.name deptName from emp e left join dept d on e.id = d.id
<where>
<if test = "name != null and name != ''">
e.name like concat('%',#{name},'%')
</if>
<if test="gender != null">
and e.gender = #{gender}
</if>
<if test="begin != null and end != null">
and entry_date between #{begin} and #{end}
</if>
</where>
order by e.update_time desc
</select>
</mapper>
MyBatis中动态SQL的<if>及<where>标签的作用?
<if>标签:条件判断,如果条件成立,则拼接对应的sql片段
<where>:根据查询条件,来生成where关键字,并会自动去除条件前面多 余的and或or。
9、新增员工-思路分析
前端提供json数据,业务层调用两次数据访问层,分别向emp和emp_expr表中新增数据

10、新增员工-保存员工基本信息
在Emp实体类中加入这个字段
java
//封装工作经历信息
private List<EmpExpr> exprList;
控制层
java
/**
* 新增员工
*/
@PostMapping
public Result save(@RequestBody Emp emp){
log.info("新增员工:{}",emp);
empService.save(emp);
return Result.success();
}
业务层
java
@Override
public void save(Emp emp) {
//1、保存员工基本信息
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
empMapper.insert(emp);
//2、保存员工工作经历信息
}
数据访问层
记得属性名要改成字段名的驼峰命名形式
java
@Insert("insert into emp(username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time)" +
" values (#{username}, #{name}, #{gender},#{phone},#{job},#{salary},#{image},#{entryDate},#{deptId},#{createTime},#{updateTime})")
void insert(Emp emp);
11、新增员工-批量保存员工工作经历
控制层
不变(已有新增操作)
业务层
java
@Override
public void save(Emp emp) {
//1、保存员工基本信息
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
empMapper.insert(emp);
//2、保存员工工作经历信息
List<EmpExpr> exprList = emp.getExprList();
if(!CollectionUtils.isEmpty(exprList)){//判断集合是否为空
empExprMapper.insertBatch(exprList);
}
}
数据访问层
java
/**
* 批量保存员工的工作经历信息
*/
void insertBatch(List<EmpExpr> exprList);
创建数据访问层的配置文件EmpExprMapper.xml
在数据访问层的方法名上alt回车回车
<foreach>动态sQL标签的作用?其中属性的含义?
-
作用:遍历集合/数组
-
属性:
-
collection:集合名称
-
item:集合遍历出来的元素/项
-
separator:每一次遍历使用的分隔符(可选)
-
open:遍历开始前拼接的片段(可选)
-
close:遍历结束后拼接的片段(可选)
-
XML
<mapper namespace="com.itheima.mapper.EmpMapper">
<!-- 批量保存员工工作经历信息
foreach标签:
collection:遍历的集合
item:遍历出来的元素
separator:每次循环之间的分隔符-->
<insert id="insertBatch">
insert into emp_expr(emp_id, begin, end, company, job)
<foreach collection="exprList" item="expr" separator=",">
(#{expr.empId}, #{expr.begin}, #{expr.end}, #{expr.company}, #{expr.job})
</insert>
</mapper>
(2)获取员工ID
在新增员工信息的数据访问层上的插入语句上加上这样一行注解
在业务层执行到insert方法之后,id就有值了
java
@Options(useGeneratedKeys = true, keyProperty = "id")//获取到生成的主键 --主键返回
在业务层插入工作经历之前加上这样一段,为empId赋值
java
//遍历集合,为empId赋值
exprList.forEach(empExpr -> {
empExpr.setEmpId(emp.getId());
});
(3)导入接口文档


(4)接口文档的使用

右上角管理环境


选择"开发环境"之后就可以使用本地的tomcat服务器了
七、事务管理
在添加员工信息和员工工作经历信息时,有可能会发生网络波动,导致提交信息失败,因此引入事务
1、介绍与操作
1.概念:事务是一组操作 的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体 一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
注意:默认MySQL的事务是自动提交的 ,也就是说,当执行一条DML语句,MySQL会立即隐式的提交事务。
当执行多条SQL语句,操作多次数据库时,建议使用事务。
2.如何控制事务?
开启事务:start transaction/begin;
提交事务:commit;(全部成功)
回滚事务:rollback;(只要有一项失败)
sql
-- 开启事务
start transaction ;
-- 保存员工基本信息 - emp
insert into emp(id, username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time)
values (49, 'duanyu', '段誉', 1, '13356560022', 1, 6000, '1.jpg', '2020-01-01', 1, now(), now());
-- 批量保存员工工作经历信息 - emp_expr
insert into emp_expr(emp_id, begin, end, company, job)
values(49,'2020-01-01','2021-01-01','百度','java开发'),
(49,'2021-01-01','2023-01-01','字节','java开发');
-- 提交事务
commit ;
-- 回滚事务
rollback ;
3.场景? 银行转账; 下单扣减库存;
2、Spring事务管理-介绍
注解:@Transactional
作用:将当前方法交给spring进行事务管理,
方法执行前,开启事务;
成功执行完毕,提交事务;
出现异常,回滚事务
位置:业务(service)层的方法上、类上、接口上

同时,在application.yml中加上这样一项配置,将事务管理日志级别提高到Debug级别
TypeScript
#配置事务管理日志级别
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
(2)在业务层上
手动创建除0异常,并创建事务管理
java
@Transactional//事务管理控制
@Override
public void save(Emp emp) {
//1、保存员工基本信息
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
empMapper.insert(emp);
//int i = 1/0;
//2、保存员工工作经历信息
List<EmpExpr> exprList = emp.getExprList();
if(!CollectionUtils.isEmpty(exprList)){//判断集合是否为空
//遍历集合,为empId赋值
exprList.forEach(empExpr -> {
empExpr.setEmpId(emp.getId());
});
empExprMapper.insertBatch(exprList);
}
}
3、Spring事务管理-rollbackFor
Transactional异常默认出现运行时异常才会回滚
此时注解属性应该这样写
java
@Transactional(rollbackFor = {Exception.class})//事务管理控制,默认出现运行时异常才会回滚
4、Spring事务管理-propagation
(1)介绍
事务传播行为:指的就是当一个事务方法 被另一个事务方法调用时,这个事务方法应该如何进行事务控制。

该属性的几个值。前两个为常用值

(2)案例
需求:在新增员工信息时,无论是成功还是失败,都要记录操作日志。
首先
创建 员工日志表
sql
-- 创建员工日志表
create table emp_log(
id int unsigned primary key auto_increment comment 'ID, 主键',
operate_time datetime comment '操作时间',
info varchar(2000) comment '日志信息'
) comment '员工日志表';
创建 员工日志实体类
java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmpLog {
private Integer id; //ID
private LocalDateTime operateTime; //操作时间
private String info; //详细信息
}
创建 员工日志数据访问层
java
@Mapper
public interface EmpLogMapper {
@Insert("insert into emp_log (operate_time, info) values (#{operateTime}, #{info})")
public void insert(EmpLog empLog);
}
创建 员工日志业务层
java
public interface EmpLogService {
public void insertLog(EmpLog empLog);
}
java
@Service
public class EmpLogServiceImpl implements EmpLogService {
@Autowired
private EmpLogMapper empLogMapper;
@Override
public void insertLog(EmpLog empLog) {
empLogMapper.insert(empLog);
}
}
再 在 员工管理的业务层 调用 员工日志管理业务层
为了使记录日志的方法 无论员工信息保存成功与否都运行,需要环绕try...finally
按住ctrl+alt+T,选择surround with try...finally
java
@Autowired
private EmpLogService empLogService;
@Transactional(rollbackFor = {Exception.class})//事务管理控制,默认出现运行时异常才会回滚
@Override
public void save(Emp emp) {
try{
//1、保存员工基本信息
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
empMapper.insert(emp);
int i = 1/0;
//2、保存员工工作经历信息
List<EmpExpr> exprList = emp.getExprList();
if(!CollectionUtils.isEmpty(exprList)){//判断集合是否为空
//遍历集合,为empId赋值
exprList.forEach(empExpr -> {
empExpr.setEmpId(emp.getId());
});
empExprMapper.insertBatch(exprList);
}
}finally{
//记录操作日志
EmpLog empLog = new EmpLog(null,LocalDateTime.now(),"新增员工:" + emp);
empLogService.insertLog(empLog);
}
}
此时运行会发现,调用该方法的新增员工方法 会连带记录日志的方法 一起回滚,因此要在 日志管理的业务层的实现类上 也要加上 事务管理的propagation属性的REQUIRES_NEW值
使该方法在新的事务中运行
java
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void insertLog(EmpLog empLog) {
empLogMapper.insert(empLog);
}
5、事务四大特征(ACID)
(1)原子性 Atomicity
事务是不可分割的最小单元,要么全部成功,要么全部失败
(2)一致性 Consistency
事务完成时,必须使所有的数据都保持一致状态
(3)隔离性 Isolation
数据库系统提供的隔离机制,保证事务在不受外部并发操作影响 的独立环境下运行
(4)持久性 Durability
事务一旦提交或回滚,它对数据库中的数据的改变就是永久的
八、文件上传
1、介绍
简介 文件上传:是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程。 文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。
前端三要素
若未设置enctype,只能提交文件的名字不能提交文件的内容

后端的控制层
java
@PostMapping("/upload")
public Result upload(String name, Integer age, MultipartFile file){
log.info("接收参数:{},{},{}",name,age,file);
return Result.success();
}
2、本地存储
在控制层,使用这两行代码获取并保存文件
java
//获取原始文件名
String originalFilename = file.getOriginalFilename();
//保存文件
file.transferTo(new File("D:/images/" + originalFilename));
为了上传的文件名不重复,需要重新赋文件名
java
@Slf4j
@RestController
public class UploadController {
@PostMapping("/upload")
public Result upload(String name, Integer age, MultipartFile file){
log.info("接收参数:{},{},{}",name,age,file);
//获取原始文件名
String originalFilename = file.getOriginalFilename();
//新的文件名
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));//获取最后一个"."及其后面的字符串
String newFileName = UUID.randomUUID().toString() + extension;
//保存文件
file.transferTo(new File("D:/images/" + newFileName));
return Result.success();
}
}
上传文件大小限制 配置文件中
TypeScript
spring:
servlet:
multipart:
# 最大单个文件大小
max-file-size: 10MB
# 最大请求大小(包括所有文件和表单数据)
max-request-size: 100MB
如何将上传的文件存储在服务器本地?
multipartFile.getoriginalFilename():获取原始文件名
multipartFile.transferTo(Filedest):将文件转存到磁盘文件
3、阿里云OSS-准备工作
阿里云是阿里巴巴集团旗下全球领先的云计算公司,也是国内最大的云服务提供商。

阿里云对象存储OSS(Object StorageService),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。

SDK:Software Development Kit的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。
具体的准备工作

Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。
4、阿里云OSS-入门程序
注意:在使用第三方提供的云服务或技术时,一定要参照对应的官方文档进行开发和测试。
在阿里云官方找到SDK
找到Maven依赖
XML
<!--阿里云OSS依赖-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.3</version>
</dependency>
测试代码,上传到阿里云OSS
java
package com.itheima;
import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
import com.aliyun.oss.common.comm.SignVersion;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.nio.file.Files;
public class Demo {
public static void main(String[] args) throws Exception {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-beijing.aliyuncs.com";
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// 填写Bucket名称,例如examplebucket。
String bucketName = "java-ai-swb";
// 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
String objectName = "001.jpg";
// 填写Bucket所在地域。以华东1(杭州)为例,Region填写为cn-hangzhou。
String region = "cn-beijing";
// 创建OSSClient实例。
ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
OSS ossClient = OSSClientBuilder.create()
.endpoint(endpoint)
.credentialsProvider(credentialsProvider)
.clientConfiguration(clientBuilderConfiguration)
.region(region)
.build();
try {
File file = new File("D:\\真图片\\桌面\\1\\ (2).jpg");
byte[] content = Files.readAllBytes(file.toPath());//将文件读取成字节数组
//传递到阿里云OSS
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
}
5、阿里云OSS-案例集成
在com.itheima目录下创建工具类包utils
java
package com.itheima.utils;
import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
import com.aliyun.oss.common.comm.SignVersion;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
@Component//交给他管理,若以后想用到这个工具类,只需要Autowired注入进来即可
public class AliyunOSSOperator {
private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
private String bucketName = "java-ai-swb";
private String region = "cn-beijing";
//参数为文件的字节数组和文件的原始名称
public String upload(byte[] content, String originalFilename) throws Exception {
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// 填写Object完整路径,例如202406/1.png。Object完整路径中不能包含Bucket名称。
//获取当前系统日期的字符串,格式为 yyyy/MM
String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
//生成一个新的不重复的文件名
String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
String objectName = dir + "/" + newFileName;
// 创建OSSClient实例。官方代码,不用动,也不用懂
ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
OSS ossClient = OSSClientBuilder.create()
.endpoint(endpoint)
.credentialsProvider(credentialsProvider)
.clientConfiguration(clientBuilderConfiguration)
.region(region)
.build();
try {//将文件上传到bucket中
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));
} finally {
ossClient.shutdown();
}
//组装成文件访问路径
return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;
}
}
上传员工信息的控制层代码
java
@Autowired
private AliyunOSSOperator aliyunOSSOperator;
/**
* 阿里云OSS存储方案
* @param file
* @return
*/
@PostMapping("/upload")
public Result upload(MultipartFile file) throws Exception {
log.info("文件上传:{}",file);
//将文件交给OSS存储管理
String url = aliyunOSSOperator.upload(file.getBytes(), file.getOriginalFilename());
log.info("文件上传成功:{}",url);
return Result.success(url);
}
6、阿里云OSS-程序优化(@Value)
为了使这些灵活变动的参数便于管理,应该把他们放到配置文件中
TypeScript
# 阿里云OSS
aliyun:
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com
bucketName: java-ai-swb
region: cn-beijing
使用Value注解导入参数
java
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.bucketName}")
private String bucketName;
@Value("${aliyun.oss.region}")
private String region;
7、阿里云OSS-程序优化(@ConfigurationProperties)
在utils包下创建AliyunOSSProperties类,封装三个实体属性
java
@Data
@Component//交给IOC容器管理,可以AutoWired注入
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOSSProperties {
private String endpoint;
private String bucketName;
private String region;
}
在方法外注入该类,并在方法内声明类中的属性
java
//方式二:通过@ConfigurationProperties注解批量注入
@Autowired
private AliyunOSSProperties aliyunOSSProperties;
//参数为文件的字节数组和文件的原始名称
public String upload(byte[] content, String originalFilename) throws Exception {
String endpoint = aliyunOSSProperties.getEndpoint();
String bucketName = aliyunOSSProperties.getBucketName();
String region = aliyunOSSProperties.getRegion();
九、员工管理
1、删除员工-请求参数接收
通过数组和集合(推荐)两种方式请求参数接收
java
/**
* 删除员工-数组
*/
@DeleteMapping
public Result delete(Integer[] ids){
log.info("删除员工:{}", Arrays.toString(ids));
empService.delete(ids);
return Result.success();
}
/**
* 删除员工-集合
*/
@DeleteMapping
public Result delete(@RequestParam List<Integer> ids){
log.info("删除员工:{}", ids);
empService.delete(ids);
return Result.success();
}
2、删除员工-逻辑实现
业务层逻辑
java
@Transactional(rollbackFor = {Exception.class})
@Override
public void delete(List<Integer> ids) {
empMapper.deleteByIds(ids);
empMapper.deleteByEmpIds(ids);
}
数据访问层逻辑
java
void deleteByIds(List<Integer> ids);
//根据员工id批量删除员工工作经历
void deleteByEmpIds(List<Integer> empIds);
在EmpMapper.xml文件中,实现数据访问的SQL语句
XML
<!-- 批量删除员工的基本信息-->
<delete id="deleteByIds">
delete from emp where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
<!-- 根据员工id批量删除员工工作经历-->
<delete id="deleteByEmpIds">
delete from emp_expr where emp_id in
<foreach collection="empIds" item="empId" separator="," open="(" close=")">
#{empId}
</foreach>
</delete>
3、修改员工-查询回显
控制层逻辑
java
/**
* 根据id查询员工
*/
@GetMapping("/{id}")
public Result getOne(@PathVariable Integer id){//路径参数注解
log.info("根据id查询员工:{}", id);
Emp emp = empService.getInfo(id);
return Result.success(emp);
}
业务层逻辑
java
/**
* 根据ID查询员工信息
*/
@Override
public Emp getInfo(Integer id) {
return empMapper.getById(id);
}
数据访问层逻辑
java
Emp getById(Integer id);
数据访问层配置文件的逻辑
XML
<!--定义ResultMap-->
<!--基于ResultMap来完成复杂属性的封装-->
<resultMap id="empResultMap" type="com.itheima.pojo.Emp">
<!-- column代表数据库字段名,property代表实体类属性名,字段映射到属性-->
<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"/>
<!--封装工作经历信息-->
<!--property属性名 ofType泛型-->
<collection property="exprList" ofType="com.itheima.pojo.EmpExpr">
<id column="ee_id" property="id"/>
<result column="ee_empid" property="empId"/>
<result column="ee_begin" property="begin"/>
<result column="ee_end" property="end"/>
<result column="ee_company" property="company"/>
<result column="ee_job" property="job"/>
</collection>
</resultMap>
<!-- 根据ID查询员工基本信息以及工作经历信息-->
<select id="getById" resultType="com.itheima.pojo.Emp">
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>
Mybatis中封装查询结果,什么时候用resultType,什么时候用resultMap?
如果查询返回的字段名与实体的属性名 可以直接对应上,用resultType。
如果查询返回的字段名与实体的属性名对应不上 ,或实体属性比较复杂,可以通过resultMap手 动封装。
4、修改员工-修改数据
先删再加
控制层逻辑
java
/**
* 修改员工
*/
@PutMapping
public Result update(@RequestBody Emp emp){
log.info("修改员工:{}", emp);
empService.update(emp);
return Result.success();
}
业务层逻辑
java
@Transactional(rollbackFor = {Exception.class})
@Override
public void update(Emp emp) {
//1、根据ID修改员工的基本信息
emp.setUpdateTime(LocalDateTime.now());
empMapper.updateById(emp);
//2、根据ID修改员工的工作经历信息
//2.1、先删除
empExprMapper.deleteByEmpIds(Arrays.asList(emp.getId()));
//2.2 再添加
List<EmpExpr> exprList = emp.getExprList();
//为exprList赋值,设置empId
if(!CollectionUtils.isEmpty(exprList)){
exprList.forEach(empExpr -> {
empExpr.setEmpId(emp.getId());
});
empExprMapper.insertBatch(exprList);
}
}
数据访问层
java
/**
* 根据ID修改员工信息
* @param emp
*/
void updateById(Emp emp);
数据访问映射文件

5、修改员工-程序优化(动态更新)
修改更新语句
XML
<!-- 根据ID更新员工基本信息-->
<!-- set标签:会自动生成set关键字;会自动删除最后一个逗号-->
<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>
十、全局异常处理器
在项目包下创建exception包,再创建GlobalExceptionHandler.java文件
@RestControllerAdvice = @ControllerAdvice + @ResponseBody
java
/**
* 全局异常处理器
*/
@Slf4j
@RestControllerAdvice//表示这是一个全局异常处理器
public class GlobalExceptionHandler {
@ExceptionHandler//表示这是一个异常处理器
public Result handleException(Exception e) {
log.error("程序出错啦~", e);
return Result.error("出错啦,请联系管理员~");
}
//异常会按照继承关系,从下往上匹配
//当数据库中违反了唯一约束就会报DuplicateKeyException异常
@ExceptionHandler
public Result handleDuplicateKeyException(DuplicateKeyException e) {
log.error("程序出错啦~", e);
String message = e.getMessage();//获取错误信息
int i = message.indexOf("Duplicate entry");//获取异常信息中的"Duplicate entry"位置
String errMsg = message.substring(i);//获取异常信息中的"Duplicate entry"之后的内容
String[] arr = errMsg.split(" ");//将异常信息中的"Duplicate entry"之后的内容按空格分割
return Result.error(arr[2] + "已存在");//返回值包括错误信息的重复的值
}
}
十一、员工信息统计
1、职位统计-分析
创建实体类jobOption
java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JobOption {
private List jobList;
private List dataList;
}
统计每一种职位的人数的SQL语句
sql
-- 统计每一种职位对应的人数
-- case函数:case表达式 when val1 then result1 when val2 then result2... else...end
-- 只能进行等值匹配
select
(case job
when 1 then '班主任'
when 2 then '讲师'
when 3 then '学工主管'
when 4 then '教研主管'
when 5 then '咨询师'
else '其他' end) pos,
count(*) num from emp group by job order by num;
-- case函数2:case表达式 when val1 then result1 when val2 then result2... else...end
-- 可以进行范围匹配
select
(case
when job=1 then '班主任'
when job=2 then '讲师'
when job=3 then '学工主管'
when job=4 then '教研主管'
when job=5 then '咨询师'
else '其他' end) pos,
count(*) num from emp group by job order by num;
2、职位统计-实现
数据访问层逻辑
java
/**
* 统计员工职位数据
* @return
*/
@MapKey("pos")//可以不用,只是标注主键
List<Map<String,Object>> countEmpJobData();
数据访问配置文件逻辑
XML
<select id="countEmpJobData" resultType="java.util.Map">
select
(case
when job=1 then '班主任'
when job=2 then '讲师'
when job=3 then '学工主管'
when job=4 then '教研主管'
when job=5 then '咨询师'
else '其他' end) pos,
count(*) num from emp group by job order by num;
</select>
新建一个控制层ReportController,因为访问路径不一样
java
@Slf4j
@RequestMapping("/report")
@RestController
public class ReportController
{
@Autowired
private ReportService reportService;
/**
* 统计员工职位人数
*/
@GetMapping("/empJobData")
public Result empJobData()
{
log.info("统计员工职位人数");
JobOption jobOption = reportService.getEmpJobData();
return Result.success(jobOption);
}
}
业务层逻辑
java
public interface ReportService {
/**
* 统计员工职位人数
* @return
*/
JobOption getEmpJobData();
}
java
@Service
public class ReportServiceImpl implements ReportService {
@Autowired
private EmpMapper empMapper;
@Override
public JobOption getEmpJobData() {
//调用mapper接口,获取统计数据
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("num")).toList();
return new JobOption(jobList, dataList);
}
}
3、性别统计
数据库操作逻辑
sql
-- 统计员工性别人数
-- if(条件,true_value,false_value)
select if(gender = 1,'男性员工','女性员工') name ,count(*) value from emp group by gender;
-- ifnull(expr,val1):如果expr不为null,取自身,否则取val1
控制层逻辑
java
@GetMapping("/empGenderData")
public Result gettEmpGenderData(){
log.info("统计员工性别人数");
List<Map<String, Object>> genderList = reportService.getEmpGenderData();
return Result.success(genderList);
}
业务层逻辑
java
/**
* 统计员工性别人数
* @return
*/
List<Map<String, Object>> getEmpGenderData();
}
java
@Override
public List<Map<String, Object>> getEmpGenderData() {
return empMapper.countEmpGenderData();
}
数据访问层逻辑
java
/**
* 统计员工性别人数
* @return
*/
@MapKey("name")
List<Map<String, Object>> countEmpGenderData();
数据访问层实现类逻辑
XML
<select id="countEmpGenderData" resultType="java.util.Map">
select if(gender = 1,'男性员工','女性员工') name ,count(*) value from emp group by gender;
</select>
十二、注解总结
java
//IOC容器
//自动从IOC容器注入
@Autowired
//将这几层交给IOC容器管理
@RestController
@Service
@Mapper
//控制层注解
//总访问路径
@RequestMapping
@GetMapping
@DeleteMapping
//参数注解:路径参数
@PathVariable
//参数注解:请求时其后参数必须传递
@RequestParam
//参数注解:设默认值(分页参数)
@RequestParam(defaultValue = "1")
//参数注解:设日期形式
@DateTimeFormat(pattern = "yyyy-MM-dd")
//业务层注解
//用于业务层的实现类覆盖业务层的方法
@Override
//获取到生成的主键 --主键返回
@Options(useGeneratedKeys = true, keyProperty = "id")
//事务管理控制
@Transactional(rollbackFor = {Exception.class})
//数据访问层注解
//为数据访问层的属性名起别名
@Results
@Result
//可以不用,只是标注主键
@MapKey("pos")
//数据访问操作
@Select
@Delete
//实体类
//引入Lombok依赖后
//自动创建get、set方法
@Data
//自动创建无参构造
@NoArgsConstructor
//自动创建带参构造
@AllArgsConstructor
//工具类(阿里云)
//将该工具类交给IOC容器管理
@Component
//使用该注解从配置文件导入参数(阿里云配值)
@Value
通过该注解批量注入三个实体属性endpoint、桶名、地区
@ConfigurationProperties
//异常处理类
//表示这是一个全局异常处理器
@RestControllerAdvice
//表示这是一个异常处理器
@ExceptionHandler