前段時間設計了系統的評論模塊,並寫了篇文章 評論模塊 - 後端數據庫設計及功能實現 講解。前端
大佬們在評論區提出了些優化建議,總結一下:java
的確數據庫設計的有問題,感謝 wangbjun 和 JWang。git
下面就對評論模塊進行優化改造,首先更改表結構,合成一張表。評論表不存用戶頭像的話,須要從用戶服務獲取。用戶服務提供獲取頭像的接口,兩個服務間經過 Feign 通訊。程序員
這樣有個問題,若是一個資源的評論比較多,每一個評論都調用用戶服務查詢頭像仍是有點慢,因此對評論查詢加個 Redis 緩存。要是有新的評論,就把這個資源緩存的評論刪除,下次請求時從新讀數據庫並將最新的數據緩存到 Redis 中。github
代碼出自開源項目coderiver
,致力於打造全平臺型全棧精品開源項目。
項目地址: https://github.com/cachecats/...
本文將分四部分介紹web
數據庫表從新設計以下spring
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 查找該資源下的全部評論。sql
與數據表對應的實體類 CommentsInfo
數據庫
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
json
在 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後端的全平臺型全棧項目,歡迎關注。
項目地址:https://github.com/cachecats/...
您的鼓勵是我前行最大的動力,歡迎點贊,歡迎送小星星✨ ~