续 显示问题回答列表
开发业务逻辑层
上次课,我们已经确定了sql语句
而且看到sql语句比较简单,可以使用QueryWrapper来实现
所以直接编写业务逻辑层
IAnswerService接口添加方法
// 根据问题id查询所有回答的业务逻辑层方法
List<Answer> getAnswersByQuestionId(Integer id);
AnswerServiceImpl实现类
@Override
public List<Answer> getAnswersByQuestionId(Integer id) {
// 使用QueryWrapper实现查询指定id的所有回答
QueryWrapper<Answer> query=new QueryWrapper<>();
query.eq("quest_id",id);
// 执行查询
List<Answer> answers=answerMapper.selectList(query);
// 别忘了返回
return answers;
}
开发控制层代码
AnswerController添加对应方法
// 根据问题id查询所有回答的方法
// /v1/answers/question/149
@GetMapping("/question/{id}")
public List<Answer> questionAnswers(
@PathVariable Integer id){
List<Answer> answers=answerService
.getAnswersByQuestionId(id);
return answers;
}
重启服务
localhost:8080/v1/answers/question/149
观察是否显示149号问题的所有回答
Vue绑定和js代码
detail_teacher.html
262行附近
<!--列出所有的答案-->
<div class="row mt-5 ml-2" id="answersApp">
<!-- ↑↑↑↑↑↑↑↑↑↑ -->
<div class="col-12">
<div class="well-sm">
<h3><span v-text="answers.length">3</span>条回答</h3>
<!-- ↑↑↑↑↑↑↑↑↑↑ -->
</div>
<div class="card card-default my-5"
v-for="answer in answers">
<!-- ↑↑↑↑↑↑↑↑↑↑ -->
<!-- Default panel contents -->
<div class="card-header">
<div class="row">
<div class="col-1">
<img style="width: 50px;height: 50px;border-radius: 50%;"
src="../img/user.jpg">
</div>
<div class="col-8 ">
<div class="row">
<span class="ml-3"
v-text="answer.userNickName">张三</span>
<!-- ↑↑↑↑↑↑↑↑↑↑ -->
</div>
<div class="row">
<span class="ml-3"
v-text="answer.duration">2天前</span>
<!-- ↑↑↑↑↑↑↑↑↑↑ -->
</div>
</div>
<div class="3">
</div>
</div>
</div>
<div class="card-body ">
<span class="question-content text-monospace" v-html="answer.content">
方法的重载是overloading,方法名相同,参数的类型或个数不同,对权限没有要求
方法的重写是overrding 方法名称和参数列表,参数类型,返回值类型全部相同,但是所实现的内容可以不同,一般发生在继承中
</span>
<!-- 其他代码略 -->
</div>
我们继续在question_detail.js文件中编写代码
新建另一个Vue对象,代码如下
let answersApp=new Vue({
el:"#answersApp",
data:{
answers:[]
},
methods:{
loadAnswers:function (){
// 也是需要从浏览器地址栏?之后获得问题id的
let qid=location.search;
if(!qid){
return;
}
qid=qid.substring(1);
axios({
url:"/v1/answers/question/"+qid,
method:"get"
}).then(function(response){
answersApp.answers=response.data;
})
}
},
created:function(){
this.loadAnswers();
}
})
重启服务之后登录讲师,访问问题详情页
观察是否显示当前问题的所有回答列表
重构计算持续时间的方法
我们的回答列表中也包含显示持续时间的功能
这样我们在项目中已经多次需要计算持续时间
为了减少计算持续时间代码出现的冗余
我们决定在utils.js文件中编写一个计算持续时间的方法
然后所有需要这个方法的位置进行调用,以减少代码冗余
utils.js文件编写代码如下
// 计算持续时间的方法
function addDuration(item){
// 如果item为空,或item的createtime属性为空,不能计算持续时间
if( !item || !item.createtime){
// 不能计算持续时间,直接终止方法
return;
}
let createtime = new Date(item.createtime).getTime();
//当前时间毫秒数
let now = new Date().getTime();
let duration = now - createtime;
if (duration < 1000*60){ //一分钟以内
item.duration = "刚刚";
}else if(duration < 1000*60*60){ //一小时以内
item.duration =
(duration/1000/60).toFixed(0)+"分钟以前";
}else if (duration < 1000*60*60*24){
item.duration =
(duration/1000/60/60).toFixed(0)+"小时以前";
}else {
item.duration =
(duration/1000/60/60/24).toFixed(0)+"天以前";
}
}
一旦上面的方法编写完毕
我们之前使用计算持续时间的方法都可以修改为调用上面的方法
例如
index.js文件中updateDuration方法修改为
updateDuration:function () {
let questions = this.questions;
for(let i=0; i<questions.length; i++){
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
addDuration(questions[i]);
}
}
index_teacher.js文件中,也执行这样的修改
还有question_detail.js文件中questionApp对象中,也有这个方法
可以直接修改为
axios({
url:"/v1/questions/"+qid,
method:"get"
}).then(function(response){
questionApp.question=response.data;
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
addDuration(response.data);
})
原本为了实现持续时间编写的updateDurtaion方法可以删除了!
最后实现上面当前问题回答列表中的持续时间
在loadAnswers方法的then中修改为
loadAnswers:function(){
let qid=location.search;
if(!qid){
return;
}
qid=qid.substring(1);
axios({
url:"/v1/answers/question/"+qid,
method:"get"
}).then(function(response){
answersApp.answers=response.data;
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
answersApp.updateDuration();
})
},
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
updateDuration:function(){
let answers=this.answers;
for(let i=0;i<answers.length;i++){
addDuration(answers[i]);
}
}
将讲师新增的回答立即显示在页面
到此为止
我们完成了讲师回复和显示回复列表的功能
但是当前页面如果再添加新的讲师回复的信息,必须通过刷新页面才能显示在回复列表中,而刷新页面会重新连接数据库获得页面中所有信息,增加服务器压力,白白浪费流量和服务器性能
我们希望讲师添加回复时,能够在不刷新页面的前提下将新增的回复添加在回复列表中,来完成讲师新增回复的效果
业务逻辑层在开发时已经考虑到这个问题,返回了新增成功的Answer对象
我们只需要修改控制层,将新增成功的Answer返回即可
AnswerController的postAnswer方法修改为:
// ↓↓↓↓↓↓
public Answer postAnswer(
@Validated AnswerVO answerVO,
BindingResult result,
@AuthenticationPrincipal UserDetails user){
log.debug("表单信息:{}",answerVO);
if(result.hasErrors()){
String msg=result.getFieldError().getDefaultMessage();
// ↓↓↓↓↓↓↓↓↓↓↓ 修改为抛出异常
throw new ServiceException(msg);
}
// 这里调用业务逻辑层方法
// ↓↓↓↓↓↓↓↓↓↓↓ 接收业务逻辑层中新增成功返回的Answer对象
Answer answer=answerService.saveAnswer(answerVO,user.getUsername());
return answer;
}
有了上面的修改,我们之前调用这个控制层的axios方法的就收到的值就由之前能够接收到"ok"这个信息,变为了接收到新增成功的answer对象
我们需要将这个新增成功的answer对象添加到answers这个集合中,即可实现页面中显示的效果了
question_detail.js文件中postAnswer方法的then中进行下面的修改
then(function(response){
console.log(response.data);
// response.data就是新增成功的answer对象
let answer=response.data;
answer.duration="刚刚";
//将新增成功的回答添加到回答列表中
answersApp.answers.push(answer);
// 利用summernote给定的方法清空(重置)其中内容
$("#summernote").summernote("reset");
})
开发新增评论的功能
认识评论Comment表
comment表就是我们的评论表
这个表故意少编写了一个列,因为在实际开发过程中,经常会遇到需要在开发中修改\添加表中列的情况,现在comment列是缺少了用户昵称列user_nick_name
下面就使用sql语句添加这个列
-- comment 表新增user_nick_name 列
ALTER TABLE comment
ADD COLUMN user_nick_name VARCHAR(255)
AFTER user_id
-- 为新增的user_nick_name列赋值
UPDATE comment c
SET user_nick_name=
(SELECT nickname FROM user u
WHERE c.user_id=u.id)
model包下Comment实体类添加属性如下
/**
* 用户昵称
*/
@TableField("user_nick_name")
private String userNickName;
问题回答和评论的结构
上图所示
question和answer是一对多的关系
answer和comment也是一对多的关系
最终在页面上的效果可能为
下面我们就要针对评论开发个各种功能
依次完成评论的增,查,删,改功能
创建CommentVO类
和讲师回复业务流程相似
我们新增评论也需要一个VO类
vo包中创建CommentVO代码如下
@Data
@Accessors(chain = true)
public class CommentVO implements Serializable {
@NotNull(message = "回答id不能为空")
private Integer answerId;
@NotBlank(message ="评论内容不能为空")
private String content;
}
编写控制层接收表单信息
CommentController添加新增评论,接收表单信息的控制层方法
@RestController
@RequestMapping("/v1/comments")
@Slf4j
public class CommentController {
@Autowired
private ICommentService commentService;
// @PostMapping等价于@PostMapping("")
@PostMapping
public Comment postComment(
@Validated CommentVO commentVO,
BindingResult result,
@AuthenticationPrincipal UserDetails user
){
log.debug("接收到表单信息:{}",commentVO);
if(result.hasErrors()){
String msg=result.getFieldError().getDefaultMessage();
throw new ServiceException(msg);
}
// 这里调用业务逻辑层
// 暂时返回null,保证编译通过
return null;
}
}
页面的绑定和js代码
页面中所有添加评论的按钮点击之后都会展开页面中所有添加评论的表单,这是个bug
我们最终要实现的效果是点击添加评论的按钮,只展开或收缩当前回答对应的评论表单
下面我们来解决这个bug,实现正确效果
detail_teacher.html的360行附近
<p class="text-left text-dark">
<a class="btn btn-primary mx-2"
href="#">采纳答案</a>
<a class="btn btn-outline-primary" data-toggle="collapse"
href="#collapseExample1"
role="button" aria-expanded="false"
aria-controls="collapseExample"
:href="'#addComment'+answer.id">
<i class="fa fa-edit"></i>添加评论
</a>
</p>
<div class="collapse" id="collapseExample1"
:id="'addComment'+answer.id">
<div class="card card-body border-light">
<form action="#" method="post" class="needs-validation"
novalidate
@submit.prevent="postComment(answer.id)">
<div class="form-group">
<textarea class="form-control" name="content" rows="3" required></textarea>
<div class="invalid-feedback">
评论内容不能为空!
</div>
</div>
<button type="submit" class="btn btn-primary my-1 float-right">提交评论</button>
</form>
</div>
</div>
vue绑定了html之后开始编写能够完成新增评论的vue代码
继续在question_detail.js文件中编写
在answersApp对象中新增方法,代码如下
postComment:function(answerId){
// 新增评论功能,需要answerId和content
// 参数就是answerId,我们需要获得的只有用户输入的内容
// 用户输入的内容在一个textarea标签中
// 我们使用jQuery的后代选择器选中这个标签
// 这个空格千万别删 ↓↓
let textarea=$("#addComment"+answerId+" textarea");
let content=textarea.val();
// 创建表单
let form=new FormData();
form.append("answerId",answerId);
form.append("content",content);
axios({
url:"/v1/comments",
method:"post",
data:form
}).then(function(response){
// 控制器返回null,暂时什么都不用写
})
}
重启服务,在新增评论的表单中提交数据
检查idea控制台收到的信息是否正确
public interface ICommentService extends IService<Comment> {
// 新增评论的业务逻辑层方法
Comment saveComment(CommentVO commentVO,String username);
}
CommentServiceImpl实现
@Service
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements ICommentService {
@Autowired
private UserMapper userMapper;
@Autowired
private CommentMapper commentMapper;
@Override
@Transactional
public Comment saveComment(CommentVO commentVO, String username) {
User user=userMapper.findUserByUsername(username);
Comment comment=new Comment()
.setContent(commentVO.getContent())
.setAnswerId(commentVO.getAnswerId())
.setUserId(user.getId())
.setUserNickName(user.getNickname())
.setCreatetime(LocalDateTime.now());
int num=commentMapper.insert(comment);
if(num!=1){
throw new ServiceException("数据库忙");
}
// 千万别忘了返回comment
return comment;
}
}
完善控制层调用
CommentController类中
调用业务逻辑层方法,代码如下
// 这里调用业务逻辑层
Comment comment=commentService.saveComment(
commentVO,user.getUsername());
// 千万别忘了返回新增成功的评论对象comment
return comment;
再次重启服务
操作新增评论,就能真正将评论新增到数据库了!
显示评论列表功能
查询评论列表的思路
我们现在要显示当前问题的所有回答的所有评论
而且评论还应该正确的显示在对应的回答下
要想实现这样的效果有两种思路
上图所示思路
左侧:在现有基础上,分别查询每个回答各自对应的评论,通过多次连接数据库将每个回答的评论查询并显示出来
这么做缺点非常明显,当前问题的回答越多,连库次数越多,效率低
右侧:重构查询所有回答的方法,在查询当前问题所有回答的同时就将每个回答对应的评论也查询出来
这么做的优点是无论当前问题多少回答,都只连接一次数据库,但是我们需要学习Mybatis关联查询操作比较复杂,但是效率明显高于左侧解决方案
编写执行查询的sql语句
Mybatis框架执行任何操作都需要明确sql操作
我们来进行sql语句的编写
目标: 查询指定问题id(例如149)的所有回答以及这个回答的所有评论
SELECT * FROM answer a
LEFT JOIN comment c ON a.id=c.answer_id
WHERE quest_id=149
ORDER BY a.id
上面sql语句的查询结果包含
所有回答以及包含所有回答对应的评论
我们要实现每个回答能够包含所有评论的数据结构
为Answer实体类添加评论集合属性
Answer实体类要添加一个Comment类型的集合满足查询出的回答包含它的所有评论的结构
Answer实体类最后添加这个属性
/**
* 当前回答包含的所有评论集合
*/
@TableField(exist = false)
private List<Comment> comments=new ArrayList<>();
Answer类中声明一个List<Comment>类型的属性,用于保存当前回答的所有评论
关联查询和Answer类的映射
所谓映射就是查询结果中每个列和java类中每个属性的对应关系
正常情况下,列名和java属性的映射都是自动的
但是我们当前数据库sql语句查询结果中列名有非常严重的重名,需要起别名才能解决
SELECT
a.id,
a.content,
a.like_count,
a.user_id,
a.user_nick_name,
a.quest_id,
a.createtime,
a.accept_status,
c.id comment_id,
c.user_id comment_user_id,
c.user_nick_name comment_user_nick_name,
c.answer_id comment_answer_id,
c.content comment_content,
c.createtime comment_createtime
FROM answer a
LEFT JOIN comment c ON a.id=c.answer_id
WHERE quest_id=149
ORDER BY a.id
然后Mybatis关联查询需要使用特殊的方式进行操作,然后进行映射配置完成查询操作
编写Mapper的Xml文件
上面章节中提到的配置映射信息的内容,要编写在Mapper对应的xml文件中
我们可以直接将mapper包中xml文件夹里的AnswerMapper.xml文件复制
粘贴到resources文件下的mapper文件夹中(mapper文件夹不存在就创建它)
复制完毕推荐rebuild一下!
表连接(关联查询)回顾
常用的表连接
两大类
1.内连接
inner join :sql语句中inner可以省略
2.外连接
外连接又分左连接和右连接
left outer join
right outer join
其中outer可以省略
内连接:要求两张表必须有对应的数据才能查询出来
如果有的数据在另一张表中没有对应数据,那么它不会显示在查询结果中
外连接:要求定义一张主表
left join就是左侧表是主表 right join就是右侧表是主表
外连接主表中的所有数据一定会被查询出来
如果主表中的数据没有和被连接表的数据对应,它会对应null查询出一次