事务管理
事务的定义
什么是事务
- 事务是由N步数据库操作序列组成的逻辑执行单元,这系列操作要么全执行,要么全放弃执行。
事务的特性(ACID)
- 原子性(Atomicity):事务是应用中不可再分的最小执行体(事务中部分执行失败就会回滚 。
- 一致性(Consistency):事务执行的结果,须使数据从一个一致性状态,变为另一个一致性状态。
- **隔离性(Isolation)😗*各个事务的执行互不干扰,任何事务的内部操作对其他的事务都是隔离的。
- 持久性(Durability):事务一旦提交,对数据所做的任何改变都要记录到永久存储器中。
事务的隔离性
常见的并发异常
- 第一类丢失更新、第二类丢失更新。
- 脏读、不可重复读、幻读。
常见的隔离级别 (从低到高)
- Read Uncommitted:读取未提交的数据。
- Read Committed:读取已提交的数据。
- Repeatable Read:可重复读。
- Serializable:串行化。(可以解决所有的问题,但需要加锁降低数据库性能)
第一类丢失更新
(事务1的回滚导致事务2的数据更新失败)
第二类丢失更新
(事务1和事务2最终结果都是11,事务2不能接受)
脏读
(实际上事务2读到的11,实际上N已经是10了)
不可重复读
(事务2并没有对N变动,但先后结果不一样,查询单行数据导致不一致)
幻读
(查询多行数据导致不一致)
不用的处理方式对数据安全的影响
(一般中间两种比较适合)
数据库保证事务的实现机制
- 悲观锁(数据库)
- 共享锁(S锁):事务A对某数据加了共享锁后,其他事务只能对该数据加共享锁,但不能加排他锁。
- 排他锁(X锁):事务A对某数据加了排他锁后,其他事务对该数据既不能加共享锁,也不能加排他锁。
- 乐观锁(自定义)
- 版本号、时间戳等
- 在更新数据前,检查版本号是否发生变化。若变化则取消本次更新,否则就更新数据(版本号+1)。
Spring事务管理
- 声明式事务(简单,常用的项目设置)
- 通过XML配置,声明某方法的事务特征。
- 通过注解,声明某方法的事务特征。
- 编程式事务(适合数据库中很多操作,只需要控制部分操作)
- 通过 TransactionTemplate 管理事务,并通过它执行数据库的操作。
示例
- 需求:一个用户自动注册完自动发送帖子
- 如果存在事务,整个会原子执行,报错后会回滚,也就是用户和帖子不会被创建在数据库中
声明式事务(常用,简单)
cpp
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public Object save1(){
User user = new User();
user.setUsername("test");
user.setSalt("abc");
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("742uu12@qq.com");
user.setHeaderUrl("http://www.nowcoder.com/101.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
//发布帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("hello");
post.setContent("新人报道");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
Integer.valueOf("abc");
return "ok";
}
//A调B,两者都有事务
//(REQUIRED):B支持当前事务(外部事务A),如果不存在则创建新事务
//(REQUIRES_NEW):B创建一个新事务,并且暂停当前事务(外部事务A)
//(NESTED):B如果当前存在事务(外部事务A),则嵌套在该事务中执行(有独立的提交和回滚),否则和REQUIRED一样
- 使用Transactional注解,isolation规定策略,propagation规定传播方式;
- 故意写一个报错的句子 Integer.valueOf("abc");
使用TransactionTemplate
cpp
public String save2(){
transactionTemplate.setIsolationLevel(TransactionTemplate.ISOLATION_READ_COMMITTED);
transactionTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED);
String result = transactionTemplate.execute(new TransactionCallback<String>() {
@Override
public String doInTransaction(org.springframework.transaction.TransactionStatus transactionStatus) {
User user = new User();
user.setUsername("test");
user.setSalt("abc");
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("742uu12@qq.com");
user.setHeaderUrl("http://www.nowcoder.com/101.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
//发布帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("hello");
post.setContent("新人报道");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
return "ok";
}
});
return result;
}
显示评论
- 数据层
- 根据实体查询一页评论数据。
- 根据实体查询评论的数量。
- 业务层
- 处理查询评论的业务。
- 处理查询评论数量的业务。
- 表现层
- 显示帖子详情数据时,
- 同时显示该帖子所有的评论数据。
数据层DAO
- 编写Comment实体类:
cpp
package com.newcoder.community.entity;
public class Comment {
int id;
int userId;
int entityType;
int entityId;
int targetId;
String content;
String status;
String createTime;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public int getEntityType() {
return entityType;
}
public void setEntityType(int entityType) {
this.entityType = entityType;
}
public int getEntityId() {
return entityId;
}
public void setEntityId(int entityId) {
this.entityId = entityId;
}
public int getTargetId() {
return targetId;
}
public void setTargetId(int targetId) {
this.targetId = targetId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getCreateTime() {
return createTime;
}
public void setCreateTime(String createTime) {
this.createTime = createTime;
}
@Override
public String toString() {
return "Comment{" +
"id=" + id +
", userId=" + userId +
", entityType=" + entityType +
", entityId=" + entityId +
", targetId=" + targetId +
", content='" + content + '\'' +
", status='" + status + '\'' +
", createTime='" + createTime + '\'' +
'}';
}
}
- 定义CommentMapper接口
cpp
@Mapper
public interface CommentMapper {
List<Comment> selectCommentsByEntity(int entityType, int entityId, int offset,int limit);
int selectCountByEntity(int entityType, int entityId);
}
- 编写comment-mapper.xml文件
cpp
<?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.newcoder.community.dao.CommentMapper">
<sql id="selectFields">
id
, user_id, entity_type, entity_id, target_id, content, status, create_time
</sql>
<select id="selectCommentByEntity" resultType="Comment">
select
<include refid="selectFields"/>
from comment
where entity_type = #{entityType} and entity_id = #{entityId}
order by create_time desc
</select>
<select id="selectCommentCount" resultType="int">
select count(id)
from comment
where entity_type = #{entityType}
and entity_id = #{entityId}mapper >
</select>
</mapper>
业务层
- 编写CommentService类
cpp
@Service
public class CommentService {
@Autowired
private CommentMapper commentMapper;
public List<Comment> findCommentsByEntity(int entityType, int entityId, int offset, int limit){
return commentMapper.selectCommentsByEntity(entityType,entityId,offset,limit);
}
public int findCommentCount(int entityType, int entityId){
return commentMapper.selectCountByEntity(entityType,entityId);
}
}
Controller层
修改之前的getDiscussPost函数:
cpp
@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable(name = "discussPostId") int discussPostId, Model model, Page page) {
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post", post);
//帖子的作者
User user = userService.findUserById(post.getUserId());
model.addAttribute("user", user);
//评论分页信息
page.setLimit(5);
page.setPath("/discuss/detail/" + discussPostId);
page.setRows(post.getCommentCount());//直接从帖子中取
List<Comment> commentList = commentService.findCommentsByEntity(ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
//遍历集合,将每个评论的其他信息查出来(这里嵌套是难点,之后可以在面试上说)
List<Map<String, Object>> commentVoList = new ArrayList<>();
if(commentList != null){
for(Comment comment : commentList){
//评论Vo
Map<String, Object> commentVo = new java.util.HashMap<>();
//评论
commentVo.put("comment",comment);
//作者
commentVo.put("user",userService.findUserById(comment.getUserId()));
//回复列表(评论的评论)
List<Comment> replyList = commentService.findCommentsByEntity(ENTITY_TYPE_COMMENT,comment.getId(),0,Integer.MAX_VALUE);
List<Map<String,Object>> replyVoList = new ArrayList<>();
if(replyList != null){
for(Comment reply : replyList){
Map<String,Object> replyVo = new java.util.HashMap<>();
//回复
replyVo.put("reply",reply);
//作者
replyVo.put("user",userService.findUserById(reply.getUserId()));
//回复目标
User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());
replyVo.put("target",target);
replyVoList.add(replyVo);
}
}
commentVo.put("replys",replyVoList);
//回复数量
int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT,comment.getId());
commentVo.put("replyCount",replyCount);
commentVoList.add(commentVo);
}
}
//将评论Vo列表传给前端
model.addAttribute("comments",commentVoList);
return "/site/discuss-detail";
}
方法有点长,从14行开始,首先设置分页信息(只有评论分页,评论的评论不分页)
然后查询所有评论,接着查询评论的评论,都加入hashmap中。
修改index.html
修改discuss-detail.html
这里太复杂了,直接把html附上:
注意这里的分页可以复用首页的分页逻辑。
- 评论显示分页:复用index.html中的th:fragment="pagination"
cpp
<nav class="mt-5" th:replace="index::pagination">
<ul class="pagination justify-content-center">
<li class="page-item"><a class="page-link" href="#">首页</a></li>
<li class="page-item disabled"><a class="page-link" href="#">上一页</a></li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item"><a class="page-link" href="#">4</a></li>
<li class="page-item"><a class="page-link" href="#">5</a></li>
<li class="page-item"><a class="page-link" href="#">下一页</a></li>
<li class="page-item"><a class="page-link" href="#">末页</a></li>
</ul>
</nav>
添加评论
数据层DAO
- 在CommentMapper中添加insert帖子接口:
cpp
int insertComment(Comment comment);
- 返回值为什么是int:
在MyBatis中,insert方法通常返回一个int类型的值,这个值表示的是插入操作影响的行数。如果插入成功,这个值应该是1(因为插入一条数据影响一行);如果插入失败,这个值可能是0(没有行被影响)。这样,开发者可以通过检查这个返回值来判断插入操作是否成功。
- 修改comment-Mapper,修改sql语句:
cpp
<sql id="insertFields">
user_id, entity_type, entity_id, target_id, content, status, create_time
</sql>
<insert id="insertComment" parameterType="Comment">
insert into comment (<include refid="insertFields"></include>)
values (#{userId}, #{entityType}, #{entityId}, #{targetId}, #{content}, #{status}, #{createTime})
</insert>
- 修改postmapper更新评论数量
cpp
int updateCommentCount(int id, int commentCount);
- 修改postmapper填写sql
cpp
<update id="updateCommentCount">
update discuss_post
set comment_count = #{commentCount}
where id = #{id}
</update>
业务层
- DiscussPostService:
cpp
public int updateCommentCount(int id, int commentCount) {
return discussPostMapper.updateCommentCount(id, commentCount);
}
- CommentService(引入事务管理,重点!!!)
cpp
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public int addComment(Comment comment){
if(comment == null){
throw new IllegalArgumentException("参数不能为空");
}
//转义HTML标记和过滤敏感词
comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));
comment.setContent(sensitiveFilter.filter(comment.getContent()));
int rows = commentMapper.insertComment(comment);
//更新帖子评论数量(过滤楼中楼)
if(comment.getEntityType() == ENTITY_TYPE_POST){
int count = commentMapper.selectCountByEntity(comment.getEntityType(),comment.getEntityId());
discussPostService.updateCommentCount(comment.getEntityId(), count);
}
return rows;
}
过滤敏感词、识别是帖子的评论而不是楼中楼,更新评论;
Controller层
添加一个新的CommentController:
cpp
@Controller
@RequestMapping("/comment")
public class CommentController {
@Autowired
private CommentService commentService;
@Autowired
private HostHolder hostHolder;
@RequestMapping(path="add/{discussPostId}",method = RequestMethod.POST)
public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment, Model model) {
comment.setUserId(hostHolder.getUser().getId());
comment.setStatus(0);
comment.setCreateTime(new Date());
commentService.addComment(comment);
return "redirect:/discuss/detail/" + discussPostId;
}
}
- 想要重定向回原页面,故用@PathVariable取id好拼接url。
修改模板
修改的是site/discuss-post.html
- 修改评论输入框
cpp
<div class="container mt-3">
<form class="replyform" method="post" th:action="@{|/comment/add/${post.id}|}">
<p class="mt-3">
<a name="replyform"></a>
<textarea placeholder="在这里畅所欲言你的看法吧!" name="content"></textarea>
<input type="hidden" name="entityType" value="1"/>
<input type="hidden" name="entityId" th:value="${post.id}"/>
</p>
<p class="text-right">
<button type="submit" class="btn btn-primary btn-sm"> 回 帖 </button>
</p>
</form>
</div>
在您的CommentController中,您使用了Comment对象来接收表单提交的数据。Spring MVC会自动将请求参数绑定到Comment对象的属性上,这是通过参数名和Comment对象属性名的匹配来实现的。因此,content表单元素的值会被自动绑定到Comment对象的content属性上。
- 修改楼中楼输入框:(就是回复评论的框)
cpp
<li class="pb-3 pt-3">
<form method="post" th:action="@{|/comment/add/${post.id}|}">
<div>
<input type="text" class="input-size" name="content" placeholder="请输入你的观点"/>
<input type="hidden" name="entityType" value="2"/>
<input type="hidden" name="entityId" th:value="${cvo.comment.id}"/>
</div>
<div class="text-right mt-2">
<button type="button" class="btn btn-primary btn-sm" onclick="#"> 回 复 </button>
</div>
</form>
</li>
- 修改楼中楼中楼的框(就是回复评论的评论的框)
cpp
<div th:id="|huifu-${rvoStat.count}|" class="mt-4 collapse">
<form method="post" th:action="@{|/comment/add/${post.id}|}">
<div>
<input type="text" class="input-size" name = "content" th:placeholder="|回复${rvo.user.username}|"/>
<input type="hidden" name="entityType" value="2"/>
<input type="hidden" name="entityId" th:value="${cvo.comment.id}"/>
<input type="hidden" name="targetId" th:value="${rvo.user.id}"/>
</div>
<div class="text-right mt-2">
<button type="submit" class="btn btn-primary btn-sm" onclick="#"> 回 复 </button>
</div>
</form>
</div>