最近正在开发一个去中心化的即时聊天应用,因为考虑到即时聊天这种社交类应用的数据大量,高并发,而且数据之间的关系复杂,所以决定使用图数据库(Graph Database)。同时,考虑到这个聊天应用的去中心化需求,查了很多图数据库,最终找到一个相当成熟的去中心化图数据库
GunDB
。网上关于
GunDB
的资料很少,这篇写的很详实,而且有可执行的源码,故翻译成中文,供有这方面需求的朋友学习。
原文链接:Data Modeling with GunDB
在这篇文章中,将使用GunDB
创建一个图书馆藏书的数据模型,这是一个为图书爱好者提供的社交应用。这个应用程序实现读者从他们喜欢的书中创建最喜欢的列表,留下评论并关注其他读者和作者。
我将从介绍什么是GunDB
数据建模开始,然后将引导你为这个应用创建一个图数据模型(Graph DB model)。然后,我将用GunDB
实现实体(Entities)和关系(Relationship),最后我将创建一些模拟数据,并展示如何对查询数据。如果你只想阅读与GunDB
有关的部分,你可以跳到"为GunDB设计"部分。如果你是GunDB
的新手,你可能想看看我关于GunDB基础知识的另一篇文章。
本文的源码可在Gitlab下载。
声明:
本文介绍的代码示例和数据模型仅用于演示,对于所讨论的业务领域可能并不准确。此外,所介绍的模型并没有涵盖文章中提到的所有用例。然而,有足够的例子介绍,你可以设计你自己的模型或扩展和改进讨论的模型。
一 GunDB简介
一般来说,数据建模(Data Modeling)是一个设计过程,在这个过程中,你要确定系统中的实体(Entities),描述它们的属性(attributes)和实体之间的关系(Relationship)。最终的结果最好是一个与数据库无关的文件,并附有图表,以记录结果。同样重要的是,该文件在项目开发过程中是可维护的,可以使用版本控制的代码,或使用图表工具,如 draw.io。
如何开始建模过程呢?你可以从收集尽可能多的信息开始。你与最了解业务需求的人交谈,并确定用例。用例可以揭示一个系统的很多情况,可以非常有效地指导设计过程。此外,用例直接对应于软件团队可以转化为可交付的功能。
在用例被记录下来后,下一步是提取实体、实体的属性和关系。这通常涉及到列出实体、它们的属性和画一些描述关系的图。最后的结果是一个可维护的文件,每个人都可以在项目的整个开发生命周期中参考和更新。
在文件的第一个版本定稿后,你就可以开始添加你要使用的数据库系统的具体设计要求。以关系型数据库为例,它将涉及到识别表、外键、连接表、模式等等。对于基于文档的数据库,例如,它将涉及到识别文档、参考文献,也许还有一些模式。
现在,对于图形数据库,不管是什么数据库,你几乎总是要决定以下三个部分。
- 节点(Nodes):一个系统的实体
- 节点属性(Node properties):每个实体的属性
- 边缘(Edges):实体之间的关系(Relationship)
图书馆应用程序的建模
在下面的章节中,我们将经历图书馆应用程序的设计过程。我们将确定一些实体、它们的属性和它们的关系。我们将为GunDB
扩展设计,最后创建一些模拟数据并探索运行查询。请注意,应用程序的一些功能可能会被遗漏在数据模型中。但我们将涵盖足够多的实体和关系,这样,如果你对设计整个应用程序的模型感兴趣,你可以有一个很好的起点。
二 实例
下面是我们在收集了关于这个应用程序的信息后确定的用例。
用户(Users)
- 用户可以是读者、作者、出版商,或三者的组合。
- 该应用程序将有读者和作者可以创建账户。
- 读者和作者可以有个人资料,他们可以用来分享关于他们自己的一些情况。
- 管理员可以执行管理任务,如在系统中创建书籍或管理出版商。
- 出版商可以管理他们已经出版的书籍。
读者(Readers)
-
读者可以收藏书籍
他们可以创建收藏书单并将书添加到他们的书单中。根据读者的选择,一本书可以出现在多个书单中。他们还可以决定哪些书单保持隐私,哪些书单可以公开。
-
读者可以评论书籍
他们可以对一本书从1星到5星进行评分,并留下评论。他们可以提交评论,并在访问一本书的详细信息页面时显示评论。
-
读者可以关注其他读者
读者可以进入另一个读者的档案,看到关注他们的粉丝和他们关注的其他读者。
-
读者也可以关注作者
与上述用例类似,你可以看到他们关注的作者,以及关注他们的作者。
种子(Feed)
这个想法来源于,任何使用该应用程序的人,包括匿名用户,都可以看到最新的更新。例如,他们可以看到已添加的新书或本月最受欢迎的书籍,等等。
书籍(Books)
- 管理员可以添加书籍和管理现有书籍。
- 书籍可以很容易通过类别或关键词进行搜索。
- 书籍的添加方式可以根据读者喜欢或阅读过的书籍,并向他们推荐。
三 实体、关系和查询
现在来定义实体、实体的属性和关系。从上面的用例来看,我们将关注以下内容:
实体(Entities)
下面是我们要关注的系统的一些实体。
- 用户(User)
- 读者(Reader)
- 作者(Author)
- 出版商(Publisher)
- 书籍(Book)
- 书籍类别(Category)
属性 Attributes
User属性
- name
- username
- roles
Reader属性
- name
- favorite books
- reviews
- following
- followers
Author属性
- name
- books
- followers
- following
Book属性
- title
- subtitle?
- isbn
- authors
- publisher
- categories
- reviews?
Category属性
- name
关系 Relationship
下面是各Entities
之间的一些关系。
- 一个用户可以是读者(Reader)、作者(Author)、出版商(Publisher),或三种角色的组合
- 用户可以关注0至多个用户
- 读者可以评论0至多本书籍
- 读者可以收藏0至多本书籍
- 书籍可以属于1个或多个类别
- 作者可以撰写0至多本书籍
- 出版商可以出版0至多本图书
下图总结了上述关系
[图片上传失败...(image-f10aab-1651928957290)]
查询
在本节中,我们将考虑如何使用这些数据。换句话说,是如何对数据进行查询。定义查询有助于设计一个好的模型,帮助我们回答关于数据的问题。下面是一个关于我们如何使用数据和我们想要运行的查询的总结。请注意,这里我们主要关注的是实体之间的关系和一些关于数据的整体见解。
对于读者
- 获得他们的评论
- 获得他们的追随者(作者或其他读者)
- 获得他们的追随者
- 获得他们的私人和公共最喜欢的书单
对于作者
- 获得他们写的书
- 获得他们的追随者(读者或其他作者)。
- 获得他们的追随者
对于书籍
- 得到它所属的类别
- 获得其评论
- 获得其作者
- 得到它的出版商
- 获得收藏该书的读者
- 获得撰写或合作撰写该书的作者
对于出版商
- 获得他们已出版的书籍
- 获得与出版商合作过的作者
对于图书类别
- 获得属于该类别的书籍
- 给定一本书,得到它所属的所有类别
除了上述询问外,我们还希望能够回答以下问题:
- 什么是最受欢迎的书?一本受欢迎的书可以定义为拥有最多五星评论的书,并且有最多的读者将其添加到他们的书单中。
- 哪些标题、关键词或类别被搜索得最多?
- 指定一个评价星级,有哪些书有这个评级?例如,我们希望能够看到所有评级为2星的书籍。
- 两个或更多读者共同喜欢的书是什么?假设只依据读者共享的书单。
- 给出两个或更多的读者/作者,他们有哪些共同的被关注者/关注者。
- 给出一个关键词,返回所有具有该搜索关键词的书籍
四 设计GunDB数据结构
在这一节中,我们将使用前几节的信息,创建一个数据模型,通过GunDB
实现。
节点和属性
首先,我们先来看在GunDB
中代表实体的节点(Node),并定义它们的属性。
Book
book
- uuid: string (internal)
- type: string (internal)
- title: string
- subtitle: string
- isbn: string
--
* reviews: Set
* categories: Set
* authors: Set
* publisher: Link
可以使用draw.io,创建数据模型。新建空白图后,选择: Arrange->Insert->Advanced->From Text...
,然后将上述内容粘贴进去,在下拉框中选择List
,点击确认,就可以生成一个数据实体图,如下图:
[图片上传失败...(image-527004-1651928957290)]
Book Category
book_category
- uuid: string (internal)
- type: string (internal)
- name: string
--
* books: Set
User
user
- uuid: string (internal)
- type: string (internal)
- name: string
- username: string
- email: string
--
* roles: Set
Reader
reader
- uuid: string (internal)
- type: string (internal)
- name: string
--
* book_reviews: Set
* following: Set
* followers: Set
* favorite_books: Set
Author
author
- uuid: string (internal)
- type: string (internal)
- name: string
--
* books: Set
* following: Set
* followers: Set
Publisher
publisher
- uuid: string (internal)
- type: string (internal)
- name: string
- address: string
--
* books: Set
* authors: Set
上述节点建立完成后,如下图:
[图片上传失败...(image-257a7b-1651928957290)]
点击这里,获得完整的数据模型
关系
在本节中,我们将用GunDB
构建节点之间的关系。
GunDB
默认创建了单向的关系,并且不强迫你为边(Edges)定义属性。它给你自由来决定哪些边需要属性,哪些实体需要双向关系。你可以完全控制设计数据模型图。使用链接节点(Link Node)来描述关系是很有用的。
评论(Review Book)
评论一本书可以用读者和书之间的一个链接节点来表示,其属性如下。
review_book (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- rating: number (integer, 1 <= n < 6)
- content: string (max 375 characters)
--
* book: Node
* reader: Node
这个链接节点的属性,包括评级值(rating)和评论的内容(content)。它还包括两个引用,一个是book
,另一个是reader
,描述了review_book
与book
和reader
的关系。
注意:引用有两种,一种是Set
,另一种是Node
。如果引用是一对多的话,用Set
,如书单的books
引用,因为对应多本书,所以用Set
;而review_book
中的book
,因为与书籍是一对一,所以用Node
。相应的,如果是Set
,使用set()
添加数据;如果是Node
,则使用put()
。除此之外,建议属性的命名上,所有Set
为复数词,Node
为单数词。
[图片上传失败...(image-2952ce-1651928957290)]
上图表示以下动作:
- 我们故意让所有的链接都是双向的,这样我们就可以从任何给定的节点穿越链接了
- 从一个
review_book
的链接节点,我们可以去找读者,也可以去找书。 - 从一个
Reader
那里,我们可以从book_reviews
集里找到他们的book_reviews
。 - 从一本
Book
中,我们可以从reviews
中获得评论。
我们也可以用纯文本来描述上面的关系。
;Review Book:
reader->book_reviews->book_reviews(set)
book_reviews(set)->review_book
review_book->reader->reader
review_book->book->book
book->reviews->reviews(set)
reviews(set)->review_book
我们要探讨的其余关系,就结构而言,与上面讨论的关系非常相似。也就是说,起点或终点的节点将有一组指向关系链接。而节点链接本身将有一个指向"源"的引用,另一个指向"目的"节点。
注:上述描述关系的描述可以在draw.io
中添加图,方式与上述添加节点属性类似,只是在下拉框中,请选择Diagram
。
Author Book
创作一本书可以用作者和书之间的链接节点来表示,其属性如下。
author_book (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- date: string (ISO date)
--
* book: Node
* author: Node
[图片上传失败...(image-3c388e-1651928957290)]
;Author Book:
author->books->books(set)
books(set)->author_book
author_book->author->author
author_book->book->book
book->authors->authors(set)
authors(set)->author_book
Favorite Books
Attributes:
favorite_list (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- list_name: string
- is_public: string ("true" or "false")
--
* books: Set
* belongs_to: Node
[图片上传失败...(image-f9a2f5-1651928957290)]
Diagram:
;Reader's favorite books:
reader->favorite_books->favorite_books(set)
favorite_books(set)->book_list
book_list->books->books(set)
book_list->belongs_to->reader
books(set)->book
Book Category
Attributes:
book_category (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- category_name: string
--
* book: Node
* belongs_to: Node
[图片上传失败...(image-5f1117-1651928957290)]
Diagram:
;Books in a category:
category->books->books(set)
books(set)->book_category
book_category->belongs_to->category
book_category->book->book
book->categories->categories(set)
categories(set)->book_category
Publish Book
Attributes:
publish_book (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- date: string (ISO date)
--
* book: Node
* publisher: Node
[图片上传失败...(image-b7a5ca-1651928957290)]
Diagram:
;Publish Book:
publisher->books->books(set)
books(set)->publish_book
publish_book->publisher->publisher
publish_book->book->book
book->publication_details->publish_book
User Roles
Attributes:
role (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- role_name: string
--
* role_type: Node
* user: Node
* permissions: Set
[图片上传失败...(image-158afd-1651928957290)]
Diagram:
;User Roles:
user->roles->roles(set)
roles(set)->role
role->assigned_to->user_type
role->user->user
user_type->role->role
roles/name->role
Follow Readers/Authors
Attributes:
follow (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- date: string (ISO date)
--
* reader: Node
* by_reader: Node
[图片上传失败...(image-c5dea4-1651928957290)]
请注意,可以为读者关注作者或作者关注读者或其他作者定义类似的关系。你仍然可以把关系名称保留为"follow",但这样你可能想把链接属性重命名为更合适的名称。例如,一个作者跟随一个读者的关系可以定义为:
follow (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- date: string (ISO date)
--
* reader: Node
* by_author: Node
另一种可能性是将"关注"关系从一个用户概括到另一个用户,而不必担心用户的类型。在这种情况下,这种关系可以定义为:
follow (Link Node)
- uuid: string (internal)
- type: string (internal)
- name: string (internal)
- date: string (ISO date)
--
* user: Node
* by_user: Node
在这种情况下,只要用户和用户类型链接得当,你就可以从任何节点上遍历图,找到他们的追随者,以及他们在追随谁。在下一节用GunDB
创建图时,我们将对此进行更多的探讨。
五 创建数据图(Graph)
现在我们已经定义了节点、节点属性和节点间的关系,现在添加一些模拟数据并测试这个模型。这里的目标是用GunDB
创建一个图(Graph),使用上面的定义,并对数据运行一些查询,以验证我们得到正确的数据。
添加一些模拟数据
function fakeBooks(opts = {n: 5}) {
const books = [];
let howMany = opts.n;
while(howMany-- > 0) {
let id = uuid();
let count = (opts.n - howMany);
const book = {
uuid: id,
type: "Book",
title: `The Book Title ${count}`,
subtitle: `Lorem ipsum dolor ${count}`,
isbn: count,
};
books.push(book);
}
return books;
}
上面的函数,默认情况下,返回一个由5个代表书籍的普通JavaScript对象组成的数组。接下来,我们要调用这个函数,从书籍对象中创建Gun
节点
const db = Gun();
const books = fakeBooks();
for (let b of books) {
db.get(b.uuid).put(b);
}
为了方便起见,我们还将创建一个JavaScript
数组,用来保存对我们刚刚创建的Gun节点的引用。
const bookNodes = books.map(b => db.get(b.uuid));
创建关系
建立评论(Review book)-书
关系
现在,我们有了一些假数据,让我们去创建关系。我们要看的第一个关系是书评。下图说明了我们将需要创建的节点和链接。
[图片上传失败...(image-9a1cb8-1651928957290)]
- The
review_book
link node represents a reader reviewing a book. - The
book
property inreview_book
references the book being reviewed. - The
reader
property inreivew_book
references the reader creating the review. - The
reviews/r
set holds references toreview_book
nodes, grouped byrating
. - The
book_reviews
property in thereader
node is a set that holds references toreview_book
nodes. - The
reviews
property in thebook
node is a set that holds references toreview_book
nodes.
以上六个步骤用代码实现如下:
function reviewBook(opt) {
const {db, reader, book, rating, content} = opt;
const linkId = uuid();
const review_book = db.get(linkId).put({ // A
uuid: linkId,
type: "Link",
name: "review_book",
rating: rating,
content: content,
});
review_book.get("book").put(book); // B
review_book.get("reader").put(reader); // C
db.get(`reviews/${rating}`).set(review_book); // D
book.get("reviews").set(review_book); // E
reader.get("book_reviews").set(review_book); // F
return review_book;
}
在上面的片段中,reviewBook
函数接受了一个对象,该对象持有以下引用。
- 一个数据库实例
- 一个
Reader
节点 - 一个
Book
节点 - 评论的评级值
- 评论的内容
然后,我们在A行
创建一个review_book
的评论节点,使用传入的值设置评级和内容。在B行
,我们在评论上创建一个名为book
的属性,指向给定的book
节点。在C行
,我们定义了一个名为读者的属性,指向给定的读者节点。在D行
,我们将评论节点添加到review/rating
集。这个集合将帮助我们通过评级值来引用评论。在E行
,我们在给定的书籍节点上创建了一个叫做reviews
的集合,并将评论节点添加到其中。在F行
,我们在读者节点上创建一个名为book_reviews
的集合,并将评论节点加入其中。最后,我们从函数中返回review_book
链接节点。
现在我们有了这个函数,在主文件中我们可以让读者创建评论。例如,我们可以由读者1
创建两个评论。
reviewBook({
db,
reader: readerNodes[0],
book: bookNodes[0],
rating: 5,
content: "Great book!",
});
reviewBook({
db,
reader: readerNodes[0],
book: bookNodes[1],
rating: 1,
content: "It was ok.",
});
为了快速测试书评关系,我们可以运行以下查询并手动验证结果。
- 显示读者1的所有评论
readerNodes[0].get("book_reviews").map().once(console.log);
- 显示所有读者1评论过的书名
readerNodes[0].get("book_reviews").map().get("book").get("title").once(console.log);
- 显示指定书的所有评论
bookNodes[0].get("reviews").map().once(console.log);
- 显示指定书籍的五星评论
bookNodes[0].get("reviews").map().once(review => {
if(review.rating === 5) {
db.get(review.book).once(b => {
console.log(review);
});
}
});
- 显示指定书籍已经评论过的用户名单
bookNodes[0].get("reviews").map().get("reader").get("name").once(console.log);
- 显示所有1星评论
db.get("reviews/1").map().once(console.log);
- 显示所有1星评论的书的书名
db.get("reviews/1").map().get("book").get("title").once(console.log);
- 显示所有留下1星评论的读者
db.get("reviews/1").map().get("reader").get("name").once(console.log);
使用Promise方式查询
GunDB
使用一个流式API,这对实时应用程序来说是完美的。但是在某些情况下,你可能想使用Promise
来获得一组结果,而不是侦听更新。GunDB
包含一个扩展,可以让你把查询变成一个Promise
。在本节中,我们将使用then
把上面的一些查询变成Promise
。
首先,我们需要包含then
扩展。如果你使用npm
来安装Gun
,then
扩展被包含在node_modules/gun/lib/then
中。你可以在包含Gun
后加载它。
const gun = require("gun");
require("gun/lib/then")。
在包含then
扩展后,既可以使用Promise
方式查询记录,下面的代码返回一个Promise
,该Promise
可解析为一个包含键和每个键的元数据的图:
const result = readerNodes[0].get("book_reviews").then();
现在,为了提取数据,你可以这样做:
const removeMetaData = (o) => { // A
const copy = {...o};
delete copy._;
return copy;
};
const bookReviews = readerNodes[0].get("book_reviews").then() // B
.then(o => removeMetaData(o)) // C
.then(refs => Promise.all(Object.keys(refs).map(k => db.get(k).then()))) // D
.then(r => console.log(r)); // E
- A行,我们定义了一个辅助函数来创建一个对象的副本并删除"_"元数据域
- B行,我们运行一个由读者创建的书评的获取查询,并启用
Promise
。 - C行,我们使用我们的辅助函数来删除元数据,并返回一个只包括结果键的副本。
- D行,我们获取引用并使用
db.get
将其解析为数据节点。
下面是另一个使用then的例子,用来显示所有一星评价的书籍:
db.get("reviews/1").then()
.then(filterMetadata)
.then(r => Promise.all(Object.keys(r).map(k => db.get(k).then())))
.then(r => Promise.all(Object.keys(r).map(k => db.get(r[k].book["#"]).then())))
.then(r => log(r));
以上测试代码在github上可以找到,点击
建立作者Author-书Book
的关系
我们要写的下一个是作者与书籍的关系。下图说明了我们将要创建的链接和节点。
[图片上传失败...(image-5af248-1651928957290)]
-
author_book
链接节点表示由一个或多个作者撰写书籍。 -
author_book
节点中的book
属性引用了所写的书。 -
author_book
节点中的author
属性引用了该书的作者。 -
author
节点中的book
属性引用了一个持有对auther_book
节点的引用的集合。 -
book
节点中的authors
属性引用了一个持有对author_book
节点的引用的集合。
定义节点和关系如下:
function authorBook(opt) {
const {db, author, book, date} = opt;
const linkId = uuid();
const authorBookNode = db.get(linkId).put({
uuid: linkId,
type: "Link",
name: "author_book",
date: date,
});
authorBookNode.get("book").put(book);
authorBookNode.get("author").put(author);
book.get("authors").set(authorBookNode);
author.get("books").set(authorBookNode);
return authorBookNode;
}
下面是一些查询脚本,也可以从这里获得。
- 查询作者1写的书名
authorNodes[0].get("books").map().get("book").get("title").once(log);
- 查询图书1的作者名字
bookNodes[0].get("authors").map().get("author").get("name").once(log);
还可以利用book_review
做组合查询,例如:
- 给定作者,列出该作者写的书名以及与这些书籍相关的评论
authorNodes[0].get("books").map().get("book").get("title").once(log);
authorNodes[0].get("books").map().get("book").get("reviews").map().get("rating").once(log);
建立收藏书单(Favorite Books)
读者可以创建收藏书单,并将他们喜欢的书添加到书单中。下图显示了将要创建的书单节点和链接节点。
[图片上传失败...(image-c91dd4-1651928957290)]
-
book_list
(上文中也用favorite_list
表示)节点表示一个包含由读者添加的一组书籍的列表。 -
book_list
中的books
属性持有对这组书籍(book)的引用。 -
belongs_to
属性引用了创建该列表的读者(reader)。 -
reader
节点中的favorite_books
属性引用了一个集合,该集合持有所有book_list
节点的引用。
定义节点和关系如下:
function favoriteBooks(opt) {
const {db, reader, books, listName} = opt;
const listId = uuid();
const list = db.get(listId).put({
uuid: listId,
type: "Link",
name: "favorite_list",
list_name: listName,
is_public: "true",
});
const faveBooks = db.get(uuid());
for (book of books) {
faveBooks.set(book);
}
list.get("books").put(faveBooks);
list.get("belongs_to").put(reader);
reader.get("favorite_books").set(list);
return list;
}
下面是一些查询脚本,也可以从这里获得。
- 获取读者1的所有收藏列表的书籍
readerNodes[0].get("favorite_books").map().get("books").map().get("title").once(log);
- 获取读者1的收藏列表名为
List 1
的书籍的书名
readerNodes[0].get("favorite_books").map().once(list => {
if(list.list_name === "List 1") {
db.get(list.books).map().get("title").once(log);
}
});
- 获取读者1的个人收藏列表的名称
readerNodes[0].get("favorite_books").map().once(list => {
if(list.is_public === "false") {
log(list.list_name);
}
});
出版书籍
出版图书可以用出版商(publisher)和图书(book)之间的链接节点来表示。下图显示了这些节点和关系。
[图片上传失败...(image-48f67-1651928957290)]
-
publish_book
节点是一个链接,它持有对已出版图书和出版商的引用。 -
publish_book
节点中的book
属性引用了已出版的书。 -
publish_book
节点中的publisher
属性引用了出版商。 -
book
节点包含一个名为publisher
的属性,引用publish_book
节点。 -
publisher
节点有一个名为books
的属性,引用publish_book
节点。
定义节点和关系如下:
function publishBook(opt) {
const {db, publisher, book, date} = opt;
const linkId = uuid();
const publishLink = db.get(linkId).put({
uuid: linkId,
type: "Link",
name: "publish_book",
date: date,
});
publishLink.get("book").put(book);
publishLink.get("publisher").put(publisher);
book.get("publisher").put(publishLink);
publisher.get("books").set(publishLink);
return publishLink;
}
下面是一些查询脚本,也可以从这里获得
- 获取出版商1发行的所有图书
publisherNodes[0].get("books").map().get("book").get("title").once(log);
- 给到书名,获取出版该书的出版商
bookNodes[0].get("publication_details").get("publisher").get("name").once(log);
图书分类
对书籍的分类可以用类别节点和书籍节点之间的链接节点来表示。下图显示了我们需要创建的节点和链接。
[图片上传失败...(image-e714f2-1651928957290)]
-
book_category
链接节点将一个类别与一本书联系起来。 -
book_category
节点中的belongs_to
属性引用ategory
节点。 -
book_category
节点中的book
属性引用了book
节点。 -
category/name
集持有对book_category
节点的引用。 -
category
节点中的books
属性是一个集合,持有对book_category
链接节点的引用。 -
book
节点中的categories
属性是一个集合,它也持有对book_category
链接节点的引用。
定义节点和关系如下:
function bookCategory(opt) {
const {db, category, book, categoryName} = opt;
const linkId = uuid();
const categoryLink = db.get(linkId).put({
uuid: linkId,
type: "Link",
name: "book_category",
category_name: categoryName,
});
categoryLink.get("book").put(book);
categoryLink.get("belongs_to").put(category);
db.get(`category/${categoryName}`).set(categoryLink);
book.get("categories").set(categoryLink);
category.get("books").set(categoryLink);
return categoryLink;
}
下面是一些查询脚本,也可以从这里获得
- 列出类别1的所有图书书名
categoryNodes[0].get("books").map().get("book").get("title").once(log);
- 给定图书,列出类别
bookNodes[0].get("categories").map().get("belongs_to").get("name").once(log);
- 给定类别名称
Category 1
,列出该类别的所有图书
db.get("category/Category 1").map().get("book").get("title").once(log);
用户角色
为了给用户分配角色,我们可以创建一个集合,并向该集合添加角色链接。下图显示了我们需要创建的节点和链接。
[图片上传失败...(image-d7380c-1651928957290)]
The role node link contains the role's information. Its user property references the user node and its assigned_to property references a user type node. A user type node can be a reader, author, or publisher.
The roles property in the user node is a set that holds references to role nodes.
The user type node has a role property that points to the role node.
-
role
节点链接包含了该角色的信息。它的用户属性引用了user
节点,它的role_type
属性引用了一个user_type
节点。一个用户user_type
可以是readers
、authors
或publishers
。 -
user
节点中的role
属性是一个集合,持有对role
节点的引用。 -
user_type
节点有一个指向role
节点的角色属性。
定义节点和关系如下:
function userRole(opt) {
const {db, name, userTypeNode, user} = opt;
const linkId = uuid();
const roleLink = db.get(linkId).put({
uuid: linkId,
type: "Link",
name: "role",
role_name: name,
});
roleLink.get("user").put(user);
roleLink.get("role_type").put(userTypeNode);
db.get(`roles/${name}`).set(roleLink);
const userUuid = user._.put.uuid; // HACK, DONT DO THIS IN ACTUAL APP
const userType = userTypeNode._.put.type.toLowerCase() + "s"; // HACK, DONT DO THIS IN ACTUAL APP
db.get(`users/${userUuid}`).set(user);
db.get('users').set(user);
db.get(userType).set((userTypeNode));
user.get("roles").set(roleLink);
userTypeNode.get("role").put(roleLink);
return roleLink;
}
下面是一些查询脚本,也可以从这里获得
- 列出所有读者名字
db.get("readers").map().get("name").once(log);
- 列出所有用户的email
db.get("users").map().get("email").once(log);
- 获取用户1的角色
userNodes[0].get("roles").map().get("role_name").once(log);
关注与被关注
"关注"关系与我们迄今为止所涉及的关系有一点不同。这是因为上面的大多数关系,如写书或评书,都意味着是双向的关系。但是关注不一定是双向的。例如,用户A可以关注用户B,但这并不意味着用户B也在立即关注用户A。正因为如此,我们需要为每个用户定义两个集合,以记录"关注"和"追随者 "的情况。下图展示了用户A关注用户B时我们需要创建的节点和链接。
[图片上传失败...(image-a4fe2f-1651928957290)]
-
follow
链接节点表示用户A
和用户B
之间的"关注"关系。 -
follow
链接节点中的who
属性表示"关注"关系的"目的地"。 -
follow
链接节点中的by
属性代表"被关注"关系的"来源"。 - 用户A中的
following
属性是对follow
节点的链接引用的集合。 - 用户B中的
followers
属性是对follow
节点的链接引用的集合。
如果用户B同时也关注了用户A,则关系变为下图:
[图片上传失败...(image-7652dc-1651928957290)]
实现代码如下:
function follow(opt) {
const {db, sourceNode, destinationNode} = opt;
const linkId = uuid();
const followLink = db.get(linkId).put({
uuid: linkId,
type: "Link",
name: "follow",
date: new Date().toISOString(),
});
followLink.get("who").put(destinationNode);
followLink.get("by").put(sourceNode);
sourceNode.get("following").set(followLink);
destinationNode.get("followers").set(followLink);
return followLink;
}
下面是一些查询脚本,也可以从这里获得
- 列出读者2的所有关注
readerNodes[1].get("following").map().get("who").get("name").once(log);
- 列出所有关注读者2的用户
readerNodes[1].get("followers").map().get("by").get("name").once(log);
整体性的查询
现在我们已经创建了一些数据并添加了一些关系,让我们来问一下下面这个整体性的问题。
- 什么是最受欢迎的书?
为了简单起见,我们将流行的书定义为已经被读者列入许多最喜欢的书单,并且有五颗星的评价。使用promise helpers(包含在文章的repo中),我们可以运行两个查询,一个是最喜欢的列表,另一个是五星评论。
// uuids of the books included in all favorite lists
q(db).get("readers").getSet()
.get("favorite_books").getSet()
.get("books").getSet().get("uuid")
.data();
// uuids of the 5-star books
q(db).get("reviews/5").getSet()
.get("book")
.get("uuid")
.data();
然后,我们可以计算被列入最喜爱名单的书的数量,并按降序排列。第一本书将是收录数量最多的书。然后,我们可以简单地检查该书是否被列入五星级名单。你可以在main.js文件中看到完整的代码片段。
你可以使用上面的例子来运行各种查询,以获得对你的数据的有趣的洞察力。我们的想法是,首先运行一些查询来收集感兴趣的尿素。一旦你有了这些数据,只需做一些基本的比较,就可以得到你要找的答案了。
进一步的改进
有几件事情你应该考虑改进实现。
- 在保存数据之前,使用模式验证来强制执行一个结构(schema)。有一个叫gun-schema的扩展,它在幕后使用
is-my-json-valid
包来帮助你验证你的对象形状。 - 为你的数据创建作用域(scope)。有一个叫Reticle的扩展,可以帮助你创建范围,以避免键之间的碰撞。
- 如果你想确保不添加意外的数据,在添加到数据库之前验证约束是很重要的。此外,如果一个关系需要的话,你可能还想检查单一性。
- 使用
GraphQL
是在客户和系统的后端之间建立数据协议的一种方式。使用GraphQL
,客户可以问他们需要什么,而不用担心后台的细节。你可能想看看Graphql-gun包,它是GunDB
的一个Graphql API
。
如果你有任何问题或不确定的地方,请加入GunDB的聊天室,每个人都非常友好和有帮助。
总结
希望这篇文章能给你一些关于如何使用GunDB
进行图数据建模的启发。在这篇文章中,我只是触及了GunDB
可能的内容,这并不是整个Gun系统的全部模型。数据建模,就像任何设计过程一样,需要大量的计划和实验。然而,一个好的、经过深思熟虑的设计,可以为你在以后的工作中节省大量的时间和精力。如果你对如何改进本文介绍的模型有任何想法,请留言,我们非常感谢任何意见。
另外,我已在draw.io上共享了本文中的图数据库图,见:https://app.diagrams.net/#G1TbiTdr3IyE8YWIoNhC60jxj2VL8fMkV4