实战GunDB:设计一个高性能的图书馆数据库系统

最近正在开发一个去中心化的即时聊天应用,因为考虑到即时聊天这种社交类应用的数据大量,高并发,而且数据之间的关系复杂,所以决定使用图数据库(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
  • email
  • 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_bookbookreader的关系。

注意:引用有两种,一种是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 in review_book references the book being reviewed.
  • The reader property in reivew_book references the reader creating the review.
  • The reviews/r set holds references to review_book nodes, grouped by rating.
  • The book_reviews property in the reader node is a set that holds references to review_book nodes.
  • The reviews property in the book node is a set that holds references to review_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来安装Gunthen扩展被包含在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可以是readersauthorspublishers
  • 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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容