前言
最近在开发一个小型个人博客,在数据库设计方面遇到了一些问题,这里做一个记录。
我设计的博客包含博客的基本信息
,包括标题
、内容
等,此外还包括博客的标签
、类型
和评论
。
实际效果如下:
设计
一条博客下面对应多条一级父评论
,父评论
下面又有子评论
;每一条评论都有父评论(如果为第一级评论,则父评论为空),也有子评论(子评论可为空)。两种评论都有共同的评论博客ID
。这里暂且忽略评论用户的ID等信息。
对象关系
一篇博客可能对应多条评论,一条评论只能对应一篇博客。(一对多)
一条父评论可能包含多条子评论,一条子评论只能对应一条父评论。(一对多)
上面关系里:
博客和评论(一对多)的关系可以在评论表中使用外键关联博客表的博客id主键父评论和子评论(一对多)的关系可以在评论表中使用
注意:在企业开发中,在面对大量数据、表、并发的需求时,应当避免使用外键来关联表;关联关系应当在业务层进行处理与判断,详情可参考【IT老齐012】阿里开发规范解读:为啥禁用外键约束?,这里我为了简单明了,使用主键来关联。
关系图
这里放一个关系图,用来理解上述的对象之间的关系
难点:如何在持久层存储这种关系(评论),然后在业务层高效表示出来?
持久层最佳的实践是:评论表存储每一条评论信息,每条评论都有一个主键comment_id
,每一条评论使用外键parent_id
关联同一张表里的父评论的comment_id
,再用外键blog_id
关联博客表里的博客blog_id
主要方案:
采用树形结构(类似二叉树)采用二维结构(只有第一层和第二层的嵌套)
这里我才用“二维结构”,主要原因还是实现简单。
注意:以下的实现介绍都是在“二维结构”的实现方式上设计的,树形结构也可用这个实现,只是在处理从数据库中读取评论时操作不同(代码中有体现)。
评论表:
这里我还添加了一个属性root_parent
,主要用来存储一条评论的最顶层的父评论的id
,而parent_id
用来存储直接关联的父评论。这么做的主要原因:
我的业务层采用的是二维结构,存储一个根节点id方便处理数据,提高效率。未来需要删除评论时可以删除comment_id=x
的评论以及所有root_parent=x
的子评论。
Java实体类
Blog
这里省略了无关的字段,只专注于博客、评论
@Datapublic class Blog {/** 博客自身属性 **/private Integer blog_id; // 博客IDprivate String title;// 标题private String content; // 内容/** 实体关系 **/private List<Comment> comments = new ArrayList<>(); // 博客评论(一对多)// 构造器 ...}
Comment
这里省略了无关的字段,只专注于评论
@Datapublic class Comment {/** 自身属性 **/private Integer comment_id;// 评论idprivate String content;// 评论内容/** 实体关系 **/private Integer blog_id;// 这里考虑到实际情况下:因为blog本身可能很大,在前后端传输数据时会影响效率,所以comment没必要再包含blogprivate Integer parent_id; // 父评论的id(被回复的评论)private Integer root_parent_id; // 根评论的id(最顶级的评论)private List<Comment> child; // 本评论下的子评论 // 构造器 ...}
注意:
1.如果采用“树型结构”中,那么评论都需严格按照父子关系存储,子评论都存储在父评论的child
中,子评论通过parent_id
来找到其直接关联的父评论,通过root_parent
来找到根评论。
2. 二维结构中,只有根评论(一级评论)的child
中存储子评论,所有子评论(二级评论)的child
中都不再存储其子评论。(这就是为什么二维结构实现起来相对简单一些的原因)
MySQL建表
-- 博客SET FOREIGN_KEY_CHECKS = 0; DROP TABLE IF EXISTS `blog`;create table `blog`(`blog_id` int NOT NULL AUTO_INCREMENT,`title` varchar(64) NOT NULL,`content` mediumtext NOT NULL,PRIMARY KEY (`blog_id`))ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 评论SET FOREIGN_KEY_CHECKS = 0; DROP TABLE IF EXISTS `comment`;create table `comment`(`comment_id` int NOT NULL AUTO_INCREMENT,`content` text NOT NULL,`blog_id` int NOT NULL, `parent_id` int DEFAULT NULL,`root_parent_id` int DEFAULT NULL,PRIMARY KEY (`comment_id`),CONSTRAINT FOREIGN KEY (`blog_id`) REFERENCES `blog`(`blog_id`) ON DELETE CASCADE ON UPDATE CASCADE,CONSTRAINT FOREIGN KEY (`parent_id`) REFERENCES `comment`(`comment_id`) ON DELETE CASCADE ON UPDATE CASCADE,CONSTRAINT FOREIGN KEY (`root_parent_id`) REFERENCES `comment`(`comment_id`) ON DELETE CASCADE ON UPDATE CASCADE)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
同样的,这里省略了无关的属性,只专注于博客、评论的核心字段。
从数据库读取评论
按照现有数据库中存储的数据,无法直接(dao层数据库取出)达到如下效果:
这里我参考了评论系统数据库设计及实现的实现方案,在Service层对数据进行处理达到了理想效果,且时间复杂度控制在O(n)。
大体思路:
从数据库读取评论处理评论之间的嵌套关系返回结果
具体步骤:
查询评论表,取出所有blog_id
=给定的blog_id
的评论遍历“步骤1”中查询到的评论,把所有评论加入HashMap
方便以O(1)的时间复杂度取得,如果是父评论就直接加入最终的result
(最终返回的就是result
)再次遍历“步骤1”中查询到的评论,如果当前评论有父评论,就通过“步骤2”中的HashMap
取得父评论;再把当前评论加入到父评论的子评论列表。(如果需要构建树型结构评论,需要一直嵌套添加子评论至父评论的child
中;如果需要构建二维结构评论,则只添加一层即可)返回给调用方
CommentUtils
public static List<Comment> processComments(List<Comment> list, boolean useTree) {// 空间换时间:把comment_id和comment放入map,能够快速取到CommentMap<Integer, Comment> map = new HashMap<>();// 最终要返回的listList<Comment> result = new ArrayList<>();// 遍历一次,把所有comment加入到map中,方便快速查找映射// 如果是父评论,就直接放入result列表for (Comment comment : list) {if (comment.getParent_id() == null) {result.add(comment);}map.put(comment.getComment_id(), comment);}// 再次遍历,子评论放入到父评论的child中for (Comment comment : list) {// 为子评论设置父评论的nickname和email,以用于前端展示if (comment.getParent_id() != null) {Comment parentComment = map.get(comment.getParent_id());comment.setParent_nickname(parentComment.getNickname());comment.setParent_email(parentComment.getEmail());}Integer id;// 如果是以树形结构构建子评论,需要获取的id就是parent_id// 如果是以其它方式(二维数组),需要获取的id就是root_parent_idif (useTree) {id = comment.getParent_id();} else {id = comment.getRoot_parent_id();}// 只要存在父评论/根父评论if (id != null) {// 直接通过map快速取得评论,并把自己加入到父评论的子集Comment p = map.get(id);if (p.getChild() == null) {p.setChild(new ArrayList<>());}p.getChild().add(comment);}}return result;}
CommentServiceImpl
/*** 查询一条博客下的所有评论* @param blog_id* @return 嵌套父子关系的博客列表*/@Overridepublic List<Comment> getComments(int blog_id, boolean useTree) {// 读取评论blog_id为blog_id的数据List<Comment> list = commentMapper.getComments(blog_id);// 处理并构建评论;useTree:是否使用树形结构的评论(false则使用二维结构)return CommentUtils.processComments(list, useTree);}
最后,在查询博客时,可将查询到的评论加入到某一条博客实例中;也可将评论和博客分离。在我的个人网站中采用的方法是,博客模块和评论模块分开请求服务器,并且独立加载,互不干扰。
树形结构:类似实现效果如下(图片来自引用2):
二维结构:实现效果如下
插件
前段时间发现一个博客评论模块的插件,UI看起来很好看,功能也很全很强大,分享一下:
官网:Disqus
放在博客里是这样的:
参考:
SpringBoot开发一个小而美的个人博客评论系统数据库设计及实现