day13 计划
新增员工功能(保存emp表 + 批量保存工作经历)
事务管理(@Transactional、rollbackFor、propagation)
文件上传(前端三要素、本地存储、阿里云OSS)
笔记 1.新增员工 需求分析 新增员工时,表单中既要录入员工基本信息,又要录入工作经历信息:
员工基本信息 → 存入 emp 表
工作经历信息 → 存入 emp_expr 表(一个员工可有多段经历)
接口要求: 请求路径 /emps、请求方式 POST、请求参数 JSON格式(@PostMapping + @RequestBody)
Emp实体类增加工作经历属性 1 2 3 4 5 6 @Data public class Emp { ... private String deptName; private List<EmpExpr> exprList; }
保存员工基本信息(主键返回) EmpMapper:
1 2 3 4 5 @Options(useGeneratedKeys = true, keyProperty = "id") @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) ;
为什么需要主键返回? 保存员工后还要保存其工作经历,而工作经历表的 emp_id 字段需要关联员工的ID。@Options(useGeneratedKeys = true, keyProperty = "id") 会在insert执行后,把自增主键值回填到 emp.getId()。
批量保存工作经历(<foreach>标签) EmpExprMapper接口:
1 2 3 4 @Mapper public interface EmpExprMapper { void insertBatch (List<EmpExpr> exprList) ; }
EmpExprMapper.xml:
1 2 3 4 5 6 <insert id ="insertBatch" > insert into emp_expr (emp_id, begin, end, company, job) values <foreach collection ="exprList" item ="expr" separator ="," > (#{expr.empId}, #{expr.begin}, #{expr.end}, #{expr.company}, #{expr.job}) </foreach > </insert >
<foreach> 标签常用属性:
属性
说明
collection
集合名称(方法参数名)
item
遍历出来的元素/项
separator
每次遍历之间的分隔符(如逗号)
open
遍历开始前拼接的片段
close
遍历结束后拼接的片段
Service完整逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public void save (Emp emp) { emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); empMapper.insert(emp); Integer empId = emp.getId(); List <EmpExpr> exprList = emp.getExprList(); if (!CollectionUtils.isEmpty(exprList)) { exprList.forEach(expr -> expr.setEmpId(empId)); empExprMapper.insertBatch(exprList); } }
2.事务管理 为什么需要事务 新增员工涉及两次数据库操作 (插emp表 + 插emp_expr表)。如果保存员工成功、保存经历失败,就会造成数据不完整不一致。事务能让多个操作要么全部成功,要么全部失败 。
事务概念与操作
概念: 一组操作的集合,是不可分割的工作单位,要么同时成功,要么同时失败
SQL层面三步操作:
开启事务:start transaction; / begin;
全部成功:commit;
有失败:rollback;
注意:MySQL默认事务是自动提交的,执行一条DML语句会立即隐式提交。
Spring事务管理:@Transactional注解 作用: 方法执行前开启事务,执行完毕提交事务,出现异常则回滚事务。
1 2 3 4 5 6 7 @Transactional @Override public void save (Emp emp) { empMapper.insert(emp); int i = 1 /0 ; empExprMapper.insertBatch(exprList); }
可加的位置:
位置
作用
方法上
当前方法交给Spring事务管理
类上
当前类所有方法都进行事务管理
接口上
接口下所有实现类的所有方法都进行事务管理
为什么放在业务层? 一个业务功能可能包含多个数据访问操作,在业务层控制事务可以将多个操作控制在同一个事务范围内。
事务日志配置 1 2 3 logging: level: org.springframework.jdbc.support.JdbcTransactionManager: debug
rollbackFor属性(异常回滚) 默认规则: 只有出现**运行时异常(RuntimeException)**才回滚事务;抛出编译时异常(Exception)不会回滚。
1 2 3 4 5 6 @Transactional public void save (Emp emp) throws Exception { empMapper.insert(emp); throw new Exception ("出现异常了~~~" ); }
解决: 通过 rollbackFor 指定回滚的异常类型。
1 2 3 4 5 @Transactional(rollbackFor = Exception.class) public void save (Emp emp) throws Exception { empMapper.insert(emp); throw new Exception ("出现异常了~~~" ); }
结论:
默认只有 RuntimeException 会回滚
需要回滚其他异常时,用 rollbackFor 属性指定
propagation属性(事务传播行为) 概念: 一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
属性值
含义
使用场景
REQUIRED (默认)
需要事务,有则加入,无则新建
大部分情况
REQUIRES_NEW
总是创建新事务
不希望事务相互影响(如记日志)
SUPPORTS
有则加入,无则无事务运行
少用
NOT_SUPPORTED
无事务状态下运行
少用
MANDATORY
必须有事务,否则抛异常
少用
NEVER
必须没有事务,否则抛异常
少用
案例:新增员工时无论成功失败都记录日志
问题:save方法有事务,insertLog默认REQUIRED会加入同一事务 → 主事务回滚时日志也被回滚,日志没记上。
解决: 让insertLog独立开启新事务。
1 2 3 4 5 6 7 8 @Service public class EmpLogServiceImpl implements EmpLogService { @Transactional(propagation = Propagation.REQUIRES_NEW) @Override public void insertLog (EmpLog empLog) { empLogMapper.insert(empLog); } }
两个必须掌握的传播行为:
REQUIRED: 大部分情况下使用,加入已有事务
REQUIRES_NEW: 不希望事务互相影响时使用,如记录日志(无论业务成功与否日志都要记上)
事务四大特性(ACID)
特性
英文
说明
原子性
Atomicity
事务是不可分割的最小单元,要么全部成功,要么全部失败
一致性
Consistency
事务完成时,数据都必须处于一致状态
隔离性
Isolation
多个并发事务之间相互隔离,互不干扰
持久性
Durability
事务提交或回滚后,对数据的改变是永久的
3.文件上传 简介 将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载。
前端上传页面三要素
要素
要求
原因
file域
表单必须有 <input type="file">
用于选择文件
POST方式
method="post"
上传文件通常较大,POST才能传输
enctype
enctype="multipart/form-data"
普通编码格式不适合传输二进制大文件
1 2 3 4 5 <form action ="/upload" method ="post" enctype ="multipart/form-data" > 姓名: <input type ="text" name ="username" > <br > 头像: <input type ="file" name ="file" > <br > <input type ="submit" value ="提交" > </form >
后端接收(MultipartFile) 1 2 3 4 5 6 7 8 9 10 11 12 @Slf4j @RestController public class UploadController { @PostMapping("/upload") public Result upload (String username, Integer age, MultipartFile file) throws Exception { log.info("上传文件:{}, {}, {}" , username, age, file); if (!file.isEmpty()) { file.transferTo(new File ("D:\\images\\" + file.getOriginalFilename())); } return Result.success(); } }
注意: 方法形参名必须与请求参数名一致。不一致时用 @RequestParam("file") MultipartFile image 绑定。
MultipartFile常用方法:
方法
作用
getOriginalFilename()
获取原始文件名
transferTo(File dest)
将接收的文件转存到磁盘
getSize()
获取文件大小(字节)
getBytes()
获取文件内容的字节数组
getInputStream()
获取文件内容的输入流
本地存储 问题1:文件名重复会覆盖。 解决:用UUID生成唯一文件名。
1 2 3 4 String originalFilename = file.getOriginalFilename();String extName = originalFilename.substring(originalFilename.lastIndexOf("." ));String uniqueFileName = UUID.randomUUID().toString().replace("-" , "" ) + extName;
问题2:大文件上传报错。 SpringBoot默认单个文件最大1M,需配置:
1 2 3 4 5 spring: servlet: multipart: max-file-size: 10MB max-request-size: 100MB
本地存储的缺点: 不安全(磁盘损坏文件丢失)、容量有限、无法直接访问。
阿里云OSS(云存储) OSS是什么: 阿里云对象存储服务(Object Storage Service),海量、安全、低成本、高可靠的云存储服务,通过网络随时存储和调用文件。
SDK: Software Development Kit(软件开发工具包),包含依赖jar包和代码示例。
使用第三方服务的通用思路:
准备工作:注册账号、开通服务、创建Bucket、创建AccessKey
参照官方SDK示例编写入门程序
集成到项目中
关键概念:
概念
说明
Bucket
存储空间,存储文件的容器,所有文件必须隶属于某个Bucket
Object
存储的对象(就是文件)
endpoint
Bucket对应的访问域名
region
Bucket所属地域(如cn-beijing)
AccessKey
访问凭证(ID + Secret),通过环境变量配置
1. 引入依赖:
1 2 3 4 5 <dependency > <groupId > com.aliyun.oss</groupId > <artifactId > aliyun-sdk-oss</artifactId > <version > 3.17.4</version > </dependency >
2. 环境变量配置AccessKey:
1 2 3 4 set OSS_ACCESS_KEY_ID=你的ID set OSS_ACCESS_KEY_SECRET=你的Secret setx OSS_ACCESS_KEY_ID "%OSS_ACCESS_KEY_ID%" setx OSS_ACCESS_KEY_SECRET "%OSS_ACCESS_KEY_SECRET%"
3. 集成工具类AliyunOSSOperator:
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 @Component public class AliyunOSSOperator { private String endpoint = "https://oss-cn-beijing.aliyuncs.com" ; private String bucketName = "java-ai" ; private String region = "cn-beijing" ; public String upload (byte [] content, String originalFilename) throws Exception { EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM" )); String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("." )); String objectName = dir + "/" + newFileName; OSS ossClient = OSSClientBuilder.create() .endpoint(endpoint) .credentialsProvider(credentialsProvider) .region(region) .build(); try { ossClient.putObject(bucketName, objectName, new ByteArrayInputStream (content)); } finally { ossClient.shutdown(); } return endpoint.split("//" )[0 ] + "//" + bucketName + "." + endpoint.split("//" )[1 ] + "/" + objectName; } }
4. UploadController改造:
1 2 3 4 5 6 7 8 @PostMapping("/upload") public Result upload (MultipartFile file) throws Exception { if (!file.isEmpty()) { String url = aliyunOSSOperator.upload(file.getBytes(), file.getOriginalFilename()); return Result.success(url); } return Result.error("上传失败" ); }
4.配置参数优化(@Value vs @ConfigurationProperties) 问题: endpoint、bucketName等参数写死在Java代码中,测试/生产环境切换不便维护。
方式一:@Value注解逐个注入 1 2 3 4 @Value("${aliyun.oss.endpoint}") private String endpoint;@Value("${aliyun.oss.bucketName}") private String bucketName;
1 2 3 4 5 aliyun: oss: endpoint: https://oss-cn-beijing.aliyuncs.com bucketName: java-ai region: cn-beijing
适用: 一两个属性、不需要复用。
方式二:@ConfigurationProperties批量绑定(推荐) 三个步骤:
创建实体类,属性名与配置key一致
将实体类交给IOC容器管理(@Component)
实体类上添加 @ConfigurationProperties(prefix="...") 指定前缀
1 2 3 4 5 6 7 8 @Data @Component @ConfigurationProperties(prefix = "aliyun.oss") public class AliyunOSSProperties { private String endpoint; private String bucketName; private String region; }
1 2 3 4 5 6 7 8 9 10 @Component public class AliyunOSSOperator { @Autowired private AliyunOSSProperties aliyunOSSProperties; public String upload (...) { String endpoint = aliyunOSSProperties.getEndpoint(); ... } }
对比:
方式
优点
缺点
@Value
简单直接
配置项多时繁琐,不便维护复用
@ConfigurationProperties
批量注入,便于维护复用
需要额外建实体类
5.核心知识点总结
知识点
说明
@PostMapping + @RequestBody
接收JSON格式的新增请求
@Options(useGeneratedKeys, keyProperty)
主键返回,自增主键回填到对象id属性
<foreach>
批量插入,collection/item/separator/open/close
CollectionUtils.isEmpty
判断集合是否为空
事务
一组操作的集合,要么全部成功要么全部失败
@Transactional
开启事务管理,异常自动回滚
事务注解位置
业务层方法/类/接口上
rollbackFor
默认只回滚RuntimeException,rollbackFor=Exception.class回滚所有异常
REQUIRED
默认传播行为,有事务则加入
REQUIRES_NEW
总是创建新事务,互不影响(如记日志)
ACID
原子性、一致性、隔离性、持久性
上传三要素
file域 + POST + enctype=”multipart/form-data”
MultipartFile
接收上传文件,transferTo转存磁盘
UUID文件名
解决文件名重复覆盖问题
max-file-size
SpringBoot默认单文件最大1M,需配置调大
阿里云OSS
对象存储服务,解决本地存储的缺点
SDK
软件开发工具包,含依赖和示例代码
@Value
单个属性注入配置
@ConfigurationProperties
批量绑定配置到实体类,便于维护复用