前段時間設計了系統的評論模塊,並寫了篇文章 評論模塊 - 後端數據庫設計及功能實現 講解。前端
大佬們在評論區提出了些優化建議,總結一下:java
的確數據庫設計的有問題,感謝 wangbjun 和 JWang。git
下面就對評論模塊進行優化改造,首先更改表結構,合成一張表。評論表不存用戶頭像的話,須要從用戶服務獲取。用戶服務提供獲取頭像的接口,兩個服務間經過 Feign 通訊。程序員
這樣有個問題,若是一個資源的評論比較多,每一個評論都調用用戶服務查詢頭像仍是有點慢,因此對評論查詢加個 Redis 緩存。要是有新的評論,就把這個資源緩存的評論刪除,下次請求時從新讀數據庫並將最新的數據緩存到 Redis 中。github
代碼出自開源項目
coderiver
,致力於打造全平臺型全棧精品開源項目。
項目地址:github.com/cachecats/c…web
本文將分四部分介紹spring
數據庫表從新設計以下sql
CREATE TABLE `comments_info` (
`id` varchar(32) NOT NULL COMMENT '評論主鍵id',
`pid` varchar(32) DEFAULT '' COMMENT '父評論id',
`owner_id` varchar(32) NOT NULL COMMENT '被評論的資源id,能夠是人、項目、資源',
`type` tinyint(1) NOT NULL COMMENT '評論類型:對人評論,對項目評論,對資源評論',
`from_id` varchar(32) NOT NULL COMMENT '評論者id',
`from_name` varchar(32) NOT NULL COMMENT '評論者名字',
`to_id` varchar(32) DEFAULT '' COMMENT '被評論者id',
`to_name` varchar(32) DEFAULT '' COMMENT '被評論者名字',
`like_num` int(11) DEFAULT '0' COMMENT '點讚的數量',
`content` varchar(512) DEFAULT NULL COMMENT '評論內容',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',
PRIMARY KEY (`id`),
KEY `owner_id` (`owner_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='評論表';
複製代碼
相比以前添加了父評論id pid
,去掉了用戶頭像。owner_id
是被評論的資源id,好比一個項目下的全部評論的 owner_id
都是同樣的,便於根據資源 id 查找該資源下的全部評論。數據庫
與數據表對應的實體類 CommentsInfo
json
package com.solo.coderiver.comments.dataobject;
import lombok.Data;
import org.hibernate.annotations.DynamicUpdate;
import javax.persistence.Entity;
import javax.persistence.Id;
import java.io.Serializable;
import java.util.Date;
/** * 評論表主表 */
@Entity
@Data
@DynamicUpdate
public class CommentsInfo implements Serializable{
private static final long serialVersionUID = -4568928073579442976L;
//評論主鍵id
@Id
private String id;
//該條評論的父評論id
private String pid;
//評論的資源id。標記這條評論是屬於哪一個資源的。資源能夠是人、項目、設計資源
private String ownerId;
//評論類型。1用戶評論,2項目評論,3資源評論
private Integer type;
//評論者id
private String fromId;
//評論者名字
private String fromName;
//被評論者id
private String toId;
//被評論者名字
private String toName;
//得到點讚的數量
private Integer likeNum;
//評論內容
private String content;
//建立時間
private Date createTime;
//更新時間
private Date updateTime;
}
複製代碼
數據傳輸對象 CommentsInfoDTO
在 DTO 對象中添加了用戶頭像,和子評論列表 children
,由於返給前端要有層級嵌套。
package com.solo.coderiver.comments.dto;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
@Data
public class CommentsInfoDTO implements Serializable {
private static final long serialVersionUID = -6788130126931979110L;
//評論主鍵id
private String id;
//該條評論的父評論id
private String pid;
//評論的資源id。標記這條評論是屬於哪一個資源的。資源能夠是人、項目、設計資源
private String ownerId;
//評論類型。1用戶評論,2項目評論,3資源評論
private Integer type;
//評論者id
private String fromId;
//評論者名字
private String fromName;
//評論者頭像
private String fromAvatar;
//被評論者id
private String toId;
//被評論者名字
private String toName;
//被評論者頭像
private String toAvatar;
//得到點讚的數量
private Integer likeNum;
//評論內容
private String content;
//建立時間
private Date createTime;
//更新時間
private Date updateTime;
private List<CommentsInfoDTO> children;
}
複製代碼
爲了方便理解先看一下項目的結構,本項目中全部的服務都是這種結構
每一個服務都分爲三個 Module,分別是 client
, common
, server
。
client
:爲其餘服務提供數據,Feign 的接口就寫在這層。common
:放 client
和 server
公用的代碼,好比公用的對象、工具類。server
: 主要的邏輯代碼。在 client
的 pom.xml
中引入 Feign 的依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
複製代碼
用戶服務 user
須要對外暴露獲取用戶頭像的接口,以使評論服務經過 Feign 調用。
在 user_service
項目的 server
下新建 ClientController
, 提供獲取頭像的接口。
package com.solo.coderiver.user.controller;
import com.solo.coderiver.user.common.UserInfoForComments;
import com.solo.coderiver.user.dataobject.UserInfo;
import com.solo.coderiver.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/** * 對其餘服務提供數據的 controller */
@RestController
@Slf4j
public class ClientController {
@Autowired
UserService userService;
/** * 經過 userId 獲取用戶頭像 * * @param userId * @return */
@GetMapping("/get-avatar")
public UserInfoForComments getAvatarByUserId(@RequestParam("userId") String userId) {
UserInfo info = userService.findById(userId);
if (info == null){
return null;
}
return new UserInfoForComments(info.getId(), info.getAvatar());
}
}
複製代碼
而後在 client
定義 UserClient
接口
package com.solo.coderiver.user.client;
import com.solo.coderiver.user.common.UserInfoForComments;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "user")
public interface UserClient {
@GetMapping("/user/get-avatar")
UserInfoForComments getAvatarByUserId(@RequestParam("userId") String userId);
}
複製代碼
在評論服務的 server
層的 pom.xml
裏添加 Feign 依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
複製代碼
並在入口類添加註解 @EnableFeignClients(basePackages = "com.solo.coderiver.user.client")
注意到配置掃描包的全類名
package com.solo.coderiver.comments;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@SpringBootApplication
@EnableDiscoveryClient
@EnableSwagger2
@EnableFeignClients(basePackages = "com.solo.coderiver.user.client")
@EnableCaching
public class CommentsApplication {
public static void main(String[] args) {
SpringApplication.run(CommentsApplication.class, args);
}
}
複製代碼
封裝 CommentsInfoService
,提供保存評論和獲取評論的接口
package com.solo.coderiver.comments.service;
import com.solo.coderiver.comments.dto.CommentsInfoDTO;
import java.util.List;
public interface CommentsInfoService {
/** * 保存評論 * * @param info * @return */
CommentsInfoDTO save(CommentsInfoDTO info);
/** * 根據被評論的資源id查詢評論列表 * * @param ownerId * @return */
List<CommentsInfoDTO> findByOwnerId(String ownerId);
}
複製代碼
CommentsInfoService
的實現類
package com.solo.coderiver.comments.service.impl;
import com.solo.coderiver.comments.converter.CommentsConverter;
import com.solo.coderiver.comments.dataobject.CommentsInfo;
import com.solo.coderiver.comments.dto.CommentsInfoDTO;
import com.solo.coderiver.comments.repository.CommentsInfoRepository;
import com.solo.coderiver.comments.service.CommentsInfoService;
import com.solo.coderiver.user.client.UserClient;
import com.solo.coderiver.user.common.UserInfoForComments;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Slf4j
public class CommentsInfoServiceImpl implements CommentsInfoService {
@Autowired
CommentsInfoRepository repository;
@Autowired
UserClient userClient;
@Override
@CacheEvict(cacheNames = "comments", key = "#dto.ownerId")
public CommentsInfoDTO save(CommentsInfoDTO dto) {
CommentsInfo result = repository.save(CommentsConverter.DTO2Info(dto));
return CommentsConverter.info2DTO(result);
}
@Override
@Cacheable(cacheNames = "comments", key = "#ownerId")
public List<CommentsInfoDTO> findByOwnerId(String ownerId) {
List<CommentsInfo> infoList = repository.findByOwnerId(ownerId);
List<CommentsInfoDTO> list = CommentsConverter.infos2DTOList(infoList)
.stream()
.map(dto -> {
//從用戶服務取評論者頭像
UserInfoForComments fromUser = userClient.getAvatarByUserId(dto.getFromId());
if (fromUser != null) {
dto.setFromAvatar(fromUser.getAvatar());
}
//從用戶服務取被評論者頭像
String toId = dto.getToId();
if (!StringUtils.isEmpty(toId)) {
UserInfoForComments toUser = userClient.getAvatarByUserId(toId);
if (toUser != null) {
dto.setToAvatar(toUser.getAvatar());
}
}
return dto;
}).collect(Collectors.toList());
return sortData(list);
}
/** * 將無序的數據整理成有層級關係的數據 * * @param dtos * @return */
private List<CommentsInfoDTO> sortData(List<CommentsInfoDTO> dtos) {
List<CommentsInfoDTO> list = new ArrayList<>();
for (int i = 0; i < dtos.size(); i++) {
CommentsInfoDTO dto1 = dtos.get(i);
List<CommentsInfoDTO> children = new ArrayList<>();
for (int j = 0; j < dtos.size(); j++) {
CommentsInfoDTO dto2 = dtos.get(j);
if (dto2.getPid() == null) {
continue;
}
if (dto1.getId().equals(dto2.getPid())) {
children.add(dto2);
}
}
dto1.setChildren(children);
//最外層的數據只添加 pid 爲空的評論,其餘評論在父評論的 children 下
if (dto1.getPid() == null || StringUtils.isEmpty(dto1.getPid())) {
list.add(dto1);
}
}
return list;
}
}
複製代碼
從數據庫取出來的評論是無序的,爲了方便前端展現,須要對評論按層級排序,子評論在父評論的 children
字段中。
返回的數據:
{
"code": 0,
"msg": "success",
"data": [
{
"id": "1542338175424142145",
"pid": null,
"ownerId": "1541062468073593543",
"type": 1,
"fromId": "555555",
"fromName": "張揚",
"fromAvatar": null,
"toId": null,
"toName": null,
"toAvatar": null,
"likeNum": 0,
"content": "你好呀",
"createTime": "2018-11-16T03:16:15.000+0000",
"updateTime": "2018-11-16T03:16:15.000+0000",
"children": []
},
{
"id": "1542338522933315867",
"pid": null,
"ownerId": "1541062468073593543",
"type": 1,
"fromId": "555555",
"fromName": "張揚",
"fromAvatar": null,
"toId": null,
"toName": null,
"toAvatar": null,
"likeNum": 0,
"content": "你好呀嘿嘿",
"createTime": "2018-11-16T03:22:03.000+0000",
"updateTime": "2018-11-16T03:22:03.000+0000",
"children": []
},
{
"id": "abc123",
"pid": null,
"ownerId": "1541062468073593543",
"type": 1,
"fromId": "333333",
"fromName": "王五",
"fromAvatar": "http://avatar.png",
"toId": null,
"toName": null,
"toAvatar": null,
"likeNum": 3,
"content": "這個小夥子不錯",
"createTime": "2018-11-15T06:06:10.000+0000",
"updateTime": "2018-11-15T06:06:10.000+0000",
"children": [
{
"id": "abc456",
"pid": "abc123",
"ownerId": "1541062468073593543",
"type": 1,
"fromId": "222222",
"fromName": "李四",
"fromAvatar": "http://222.png",
"toId": "abc123",
"toName": "王五",
"toAvatar": null,
"likeNum": 2,
"content": "這個小夥子不錯啊啊啊啊啊",
"createTime": "2018-11-15T06:08:18.000+0000",
"updateTime": "2018-11-15T06:36:47.000+0000",
"children": []
}
]
}
]
}
複製代碼
其實緩存已經在上面的代碼中作過了,兩個方法上的
@Cacheable(cacheNames = "comments", key = "#ownerId")
@CacheEvict(cacheNames = "comments", key = "#dto.ownerId")
複製代碼
兩個註解就搞定了。第一次請求接口會走方法體
關於 Redis 的使用方法,我專門寫了篇文章介紹,就不在這裏多說了,須要的能夠看看這篇文章:
Redis詳解 - SpringBoot整合Redis,RedisTemplate和註解兩種方式的使用
以上就是對評論模塊的優化,歡迎大佬們提優化建議~
代碼出自開源項目 coderiver
,致力於打造全平臺型全棧精品開源項目。
coderiver 中文名 河碼,是一個爲程序員和設計師提供項目協做的平臺。不管你是前端、後端、移動端開發人員,或是設計師、產品經理,均可以在平臺上發佈項目,與志同道合的小夥伴一塊兒協做完成項目。
coderiver河碼 相似程序員客棧,但主要目的是方便各細分領域人才之間技術交流,共同成長,多人協做完成項目。暫不涉及金錢交易。
計劃作成包含 pc端(Vue、React)、移動H5(Vue、React)、ReactNative混合開發、Android原生、微信小程序、java後端的全平臺型全棧項目,歡迎關注。
您的鼓勵是我前行最大的動力,歡迎點贊,歡迎送小星星✨ ~