目录
一、标签列表---前端页面【续】
目前,启动所有项目,通过 http://localhost/api-question/v1/tags 可以获取标签列表数据,通过 http://localhost 可以打开主页,且,在主页的偏顶部位置会显示模拟数据的“标签列表”,接下来,应该通过Vue向服务器端发送请求,获取真实的标签列表数据,然后将数据绑定到Vue属性中,使得网页中显示这些数据 :
let tagsApp = new Vue({
el: '#tagsApp',
data: {
tags: []
},
methods: {
loadTags: function () {
// alert("准备加载标签列表……");
$.ajax({
url: '/api-question/v1/tags',
success: function(json) {
tagsApp.tags = json.data;
}
});
}
},
created: function () {
this.loadTags();
}
});
二、通过Thymeleaf复用页面中的“碎片”
目前,已经在“主页”显示了“标签列表”,在“我要提问”页面中,也会显示相同的区域!
由于2个页面都需要显示完全相同的区域,使用复制粘贴代码的方式即可实现,但是,复制粘贴代码是不易于整体维护的,当使用Thymeleaf来处理模版页面时,可以将页面中的某个区域设置为“碎片”,这个“碎片”是可以被任何其它由Thymeleaf处理的模版页面来引用的!
首先,需要在项目中添加Thymeleaf的依赖,参考代码为:
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
如果需要使用Thymeleaf实现复用,首先,页面的显示就应该通过Thymeleaf来实现,目前,页面文件都是直接放在static
文件夹下的,客户端发出请求时,也是直接访问static
下的资源的,不会通过Thymeleaf,所以,先在resources
下创建templates
文件夹,该文件夹是SpringBoot项目默认的“模版页面文件夹”,是会被SpringBoot自动配置的文件夹(如果在创建项目时就勾选了spring-boot-starter-web
依赖,在创建项目时就会自动把这个templates
文件夹创建出来),然后,把index.html
和question
文件夹移动到templates
文件夹下:
在templates
下的页面都是“模版页面”,是不可以被直接访问的,则需要通过控制器进行转发!在straw-gateway
的cn.tedu.straw.gateway.controller
包中创建SystemController
控制器类,实现对“主页”、“我要提问”页面的请求的转发:
package cn.tedu.straw.gateway.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class SystemController {
// http://localhost/index.html
@GetMapping("/index.html")
public String index() {
// 当处理请求的方法的返回值是String时
// -- 且没有使用@RestController注解,也没有使用@ResponseBody注解时
// -- 返回的String就是视图名,或者,使用 redirect: 作为前缀表示重定向
// 在SpringBoot中
// -- 执行转发时,整合了Thymeleaf框架后
// -- 默认的前缀就是 /templates/,默认的后缀是 .html
// -- 与当前方法的返回值组合起来,将得到:
// -- /templates/index.html
return "index";
}
// http://localhost/question/create.html
@GetMapping("/question/create.html")
public String createQuestion() {
return "question/create";
}
}
然后,重启项目,此前的页面是可以正常显示的,对于用户的感受是没有变化的,但是,此前是直接访问网页文件的,现在已经改成了通过控制器转发到页面,是由Thymeleaf框架处理的!
接下来,先在“主页”中“显示标签列表”的区域设置为“碎片”:
然后,在目标位置,也就是在question/create.html
页面中,在原本显示标签列表的位置,随意使用一个标签,将其“替换”为“碎片”即可:
三、发布问题---持久层
在发布问题时,需要同时向question
、question_tag
、user_question
这3张表中插入数据记录!
可以通过代码生成器自动生成与这3张表相关的类,方便后续使用。
首先,代码生成器生成的代码是基于Mybatis Plus框架的,所以,需要在straw-api-question
项目中补充添加MyBatis Plus的依赖:
然后,在代码生成器项目中,先修改生成的代码的相关参数:
运行3次代码生成器,分别输入各数据表的名称,例如:
然后,将model
包放到straw-commons
项目中,将mapper
包下的xml
文件夹删除,将mapper
、service
、controller
和resources/mapper
下的XML文件都移动到straw-api-question
项目中。
复制后,需要调整mapper
、service
包中类和接口的import
语句,调整XML文件中实体类的包名。
四、发布问题---业务层
先在cn.tedu.straw.api.question.dto
包中创建PostQuestionDTO
类,在类中声明客户端”发布问题“时会提交的参数:
package cn.tedu.straw.api.question.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class PostQuestionDTO implements Serializable {
private String title;
private String content;
private Integer[] tagIds;
private Integer[] teacherIds;
}
将straw-api-user
中的ex
包剪切到straw-commons
项目中去!
在IQuestionService
接口中声明“发布问题”的抽象方法:
public interface IQuestionService extends IService<Question> {
/**
* 发布问题
*
* @param postQuestionDTO 客户端提交的请求参数
* @param userId 当前登录的用户id
* @param userNickname 当前登录的用户昵称
*/
void postQuestion(PostQuestionDTO postQuestionDTO, Integer userId, String userNickname);
}
在QuestionServiceImpl
中设计业务方法的实现步骤:
package cn.tedu.straw.api.question.service.impl;
import cn.tedu.straw.api.question.dto.PostQuestionDTO;
import cn.tedu.straw.api.question.mapper.QuestionMapper;
import cn.tedu.straw.api.question.mapper.QuestionTagMapper;
import cn.tedu.straw.api.question.mapper.UserQuestionMapper;
import cn.tedu.straw.commons.ex.InsertException;
import cn.tedu.straw.commons.model.Question;
import cn.tedu.straw.api.question.service.IQuestionService;
import cn.tedu.straw.commons.model.QuestionTag;
import cn.tedu.straw.commons.model.UserQuestion;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Arrays;
/**
* <p>
* 服务实现类
* </p>
*
* @author tedu.cn
* @since 2020-09-16
*/
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {
@Autowired
QuestionMapper questionMapper;
@Autowired
QuestionTagMapper questionTagMapper;
@Autowired
UserQuestionMapper userQuestionMapper;
@Transactional
@Override
public void postQuestion(PostQuestionDTO postQuestionDTO, Integer userId, String userNickname) {
// 创建当前时间对象now
LocalDateTime now = LocalDateTime.now();
// 处理客户端提交的tagIds
String tagIdsString = Arrays.toString(postQuestionDTO.getTagIds());
tagIdsString = tagIdsString.substring(1, tagIdsString.length() - 1);
// 创建Question对象
Question question = new Question()
// 补全Question对象中的属性值:title < 参数
.setTitle(postQuestionDTO.getTitle())
// 补全Question对象中的属性值:content < 参数
.setContent(postQuestionDTO.getContent())
// 补全Question对象中的属性值:userId < 参数
.setUserId(userId)
// 补全Question对象中的属性值:userNickName < 参数
.setUserNickName(userNickname)
// 补全Question对象中的属性值:status < 0
.setStatus(0)
// 补全Question对象中的属性值:hits < 0
.setHits(0)
// 补全Question对象中的属性值:isDelete < 0
.setIsDelete(0)
// 补全Question对象中的属性值:tagIds < 参数,Arrays.toString()
.setTagIds(tagIdsString)
// 补全Question对象中的属性值:gmtCreate < 当前时间now
.setGmtCreate(now)
// 补全Question对象中的属性值:gmtModified < 当前时间now
.setGmtModified(now);
// 调用questionMapper.insert(question)插入数据,并获取返回值
int rows = questionMapper.insert(question);
// 判断返回值是否不为1
if (rows != 1) {
// 是:抛出InsertException
throw new InsertException("发布问题失败!服务器忙,请稍后再次尝试!");
}
// 遍历参数tagIds
for (int i = 0; i < postQuestionDTO.getTagIds().length; i++) {
// -- 创建QuestionTag对象
QuestionTag questionTag = new QuestionTag()
// -- 补全QuestionTag对象中的属性值:questionId < question.getId()
.setQuestionId(question.getId())
// -- 补全QuestionTag对象中的属性值:tagId < 在tagIds中被遍历到的元素
.setTagId(postQuestionDTO.getTagIds()[i])
// -- 补全QuestionTag对象中的属性值:gmtCreate < 当前时间now
.setGmtCreate(now)
// -- 补全QuestionTag对象中的属性值:gmtModified < 当前时间now
.setGmtModified(now);
// -- 调用questionTagMapper.insert(questionTag)插入数据,并获取返回值
rows = questionTagMapper.insert(questionTag);
// -- 判断返回值是否不为1
if (rows != 1) {
// -- 是:抛出InsertException
throw new InsertException("发布问题失败!处理问题的标签数据时出现未知错误,请联系系统管理员!");
}
}
// 遍历参数teacherIds
for (int i = 0; i < postQuestionDTO.getTeacherIds().length; i++) {
// -- 创建UserQuestion对象
UserQuestion userQuestion = new UserQuestion()
// -- 补全UserQuestion对象中的属性值:userId < 在teacherIds中被遍历到的元素
.setUserId(postQuestionDTO.getTeacherIds()[i])
// -- 补全UserQuestion对象中的属性值:questionId < question.getId()
.setQuestionId(question.getId())
// -- 补全UserQuestion对象中的属性值:gmtCreate < 当前时间now
.setGmtCreate(now)
// -- 补全UserQuestion对象中的属性值:gmtModified < 当前时间now
.setGmtModified(now);
// -- 调用userQuestionMapper.insert(userQuestion)插入数据,并获取返回值
rows = userQuestionMapper.insert(userQuestion);
// -- 判断返回值是否不为1
if (rows != 1) {
// -- 是:抛出InsertException
throw new InsertException("发布问题失败!处理回答问题的老师的数据时出现未知错误,请联系系统管理员!");
}
}
}
}
完成后,在test
的cn.tedu.straw.api.question.service
包中创建QuestionServiceTests
测试类,编写并执行单元测试:
package cn.tedu.straw.api.question.service;
import cn.tedu.straw.api.question.dto.PostQuestionDTO;
import cn.tedu.straw.commons.ex.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@Slf4j
public class QuestionServiceTests {
@Autowired
IQuestionService service;
@Test
void postQuestion() {
try {
PostQuestionDTO postQuestionDTO = new PostQuestionDTO()
.setTitle("数据库查询效率低下怎么解决")
.setContent("今天被面试官问倒了,好难受呀!")
.setTagIds(new Integer[]{2, 7, 9})
.setTeacherIds(new Integer[]{1, 3, 5});
Integer userId = 20;
String userNickName = "变形金刚";
service.postQuestion(postQuestionDTO, userId, userNickName);
log.debug("发布问题成功!");
} catch (ServiceException e) {
log.debug("发布问题失败!问题类型:{},问题原因:{}", e.getClass().getName(), e.getMessage());
}
}
}
附(一):关于链式写法
public class User {
String username;
String password;
String from;
public User(String username, String password, String from) {
this.username = username;
this.password = password;
this.from = from;
}
public User setUsername(String username) {
this.username = username;
return this;
}
public User setPassword(String password) {
this.password = password;
return this;
}
public User setFrom(String from) {
this.from = from;
return this;
}
}
User user = new User("Jack", "1234", "Chengdu");
User user = new User()
.setUsername("Jack")
.setPassword("1234")
.setFrom("Chengdu");
User user = new User();
user.setUsername("Jack");
user.setPassword("1234");
user.setFrom("Chengdu");
附(二):基于JDBC的事务处理
总则:如果某个业务方法中执行了超过1次的增、删、改操作(例如执行了2次INSERT操作,或1次INSERT加1次DELETE操作等),必须在业务方法的声明之前添加@Transactional
注解。
事务(Transaction):事务是数据库中能够保证多次连续执行的多个增、删、改操作能够全部执行成功,或全部执行失败的一种机制。
假设存在银行账户信息如下:
账户 | 余额 |
路人甲 | 1000 |
路人乙 | 2000 |
假设需要实现转账“路人乙向路人甲转账500元”,需要执行的SQL语句大致是:
update 账户表 set 余额=余额-6000 where 账户='路人乙';
update 账户表 set 余额=余额+6000 where 账户='路人甲';
如果出现某种意外,导致以上第1条SQL语句执行成功,而第2条却无法执行或执行失败,则会出现数据安全问题!
使用事务机制就可以保证以上2条SQL语句要么全部执行成功,要么全部执行失败,无论是哪一种情况,都是可能接受的,数据安全并不会受到影响!
基于Spring JDBC的事务处理大致是:
try {
开启事务:begin
执行一系列的数据操作
提交事务:commit
} catch (RuntimeException e) {
回滚事务:rollback
}
在基于Spring JDBC的事务,事务的回滚标准默认是以“捕获到RuntimeException
或其子孙类异常对象”为标准的!所以,在开发持久层的增、删、改功能时,都应该提供返回值,以表示“受影响的行数”,并且,在业务层中调用持久层的增、删、改方法时,需要及时获取返回的受影响行数,判断返回值是否与预期值不相符,如果不相符,则抛出RuntimeException
或其子孙类异常的对象,为事务的回滚提供依据!
另外,基于Spring JDBC的事务默认是捕获RuntimeException
来回滚的,也可以通过@Transactional
注解的参数来配置如何回滚,例如:
@Transactional(rollbackFor = SQLException.class)
以上配置就表示出现SQLException
时回滚!
另外,还可以将@Transactional
注解添加在业务类的声明之前,则该类中所有的方法都将以事务的机制来执行,但是,没有这个必要性,也不推荐这样处理!