原文链接:GunDB, a Graph Database in JavaScript
最近我一直在使用GunDB
,想和你分享我目前学到的一些东西。GunDB
不仅仅是一个图形数据库(Graph DB)。它更是一个项目组,旨在简化扩展,提高数据安全性,节约成本,并赋予应用程序开发人员更多的能力。
在这篇文章中,我将完全把Gun
作为一个数据库来探讨,并在接下来的文章中随着我的进展和学习,探索其他方面。
所有文章中的源码在这里可以找到。
简介
一般来说,数据库是一个软件,安装在你的电脑或远程服务器上,用来存储数据。这些数据可以存储在磁盘上或内存中。
数据库有不同的类型:关系型(Relational DB)、面向文档型(Document DB)、键值型(Key-value DB)或图形型(Graph DB)。下面是一些例子:
- 关系型:MySql, PostgreSQL, SQL Server
- 面向文档的:MongoDB, CouchDB
- 键值型:Redis, LevelDB
- 基于图形:Neo4j、OrientDB
与其他数据库不同,Gun
没有二进制文件需要安装,Gun
是用JavaScript
编写的,这意味着你可以在任何运行JavaScript
的地方使用它。开始使用Gun
就像下载一个JavaScrip
文件或用npm
安装一个插件一样容易。
开始使用
开始使用Gun
的最简单方法是将其作为一个单一的JavaScript
文件下载。你可以从以下网址下载最新的简化版本:https://rawgit.com/amark/gun/master/gun.min.js
按下面的方式加载到html文件中:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="gun.min.js"></script> <!-- import Gun -->
<title>Gun Basics</title>
</head>
<body>
</body>
</html>
然后你可以在浏览器中打开Html文件,并使用控制台console
与Gun
互动。在大多数浏览器中,你可以在页面上点击右键,在弹出菜单中选择检查元素
来打开控制台。下面例子可以用它来创建一个汽车记录并打印它的制造商。
const db = window.Gun();
const car = db.get("123").put({
make: "Toyota",
model: "Camry",
});
car.once(v => console.log(v.make)); // --> Toyota
在上面的代码中,首先,我们创建一个Gun
的实例。然后,使用123
为键key的get
方法来引用一个空节点。接下来,我们使用put
方法和一个普通的JavaScript
对象添加一些数据。最后,我们使用相同的key来获取记录(节点),并使用once
方法读出值。注意,普通对象会自动转换为Gun
节点。我将在后面的"基础知识"部分更详细地解释每个方法。
下面是一个示意图,帮助你更好地理解正在发生的事情。
[图片上传失败...(image-96c992-1651928820273)]
现在让我们用Node
来实验Gun
。首先,创建一个目录并使用npm
安装Gun
。
cd ~ && mkdir gun-demo && cd gun-demo
npm init -y
npm i gun -S
创建一个main.js
文件如下:
const db = require('gun')();
const car = db.get("123").put({
make: "Toyota",
model: "Camry",
});
car.once(v => console.log(v.make)); // --> Toyota
然后通过node main.js
执行该文件,你应该能看到控制台中输出Toyota
。另外,你也可以在Node
的REPL
中使用Gun
,首先在终端中调用node
,然后运行以下程序。
const db = require('gun')();
然后就可以与db
对象进行交互,如果想关闭REPL
,按Ctrl+c
两次。
Gun的基础知识
在以下章节中,我将向你展示Gun
的基础知识,并探索其创建Create、读取Read、更新Update和删除Delete记录(即CRUD)的基本方法。我还将向你展示如何创建集合Sets
和关系Relationships
。我还将简要地提到订阅记录subscribing
以获得更新的不同方法。请注意,我将交替使用记录record
和节点node
这个术语,因为Gun
是一个图数据库,记录被表示为节点node
。
CRUD
创建 Create
示例代码:
const entry = db.get('8899').put({
uuid: '8899',
some_prop: 'some value',
});
在上面的代码中,我们使用get
方法来创建一个使用8899
为key的节点的引用。然后,我们使用put
方法,用一个普通的JavaScript对象将数据添加到该节点。普通对象会自动转换为Gun节点
。
注意,如果给定的key已经存在,添加的数据可能会覆盖现有的数据。我将在"更新"部分更详细地介绍更新。下图展示了数据库中的给定键是如何指向一个节点的。
[图片上传失败...(image-e33be2-1651928820273)]
关于Key
的说明:你应该总是使用唯一的键。你可能想对通用节点使用uuids
,并为索引目的使用与可读字符串相结合的Hash字符串。在使用Gun
时,命名是非常重要的,因为所有数据都存在于全局空间。你可能需要这个Reticle扩展,以帮助你对你的键进行命名。
读取 Read
我们可以使用get
方法来查询一个给定Key的节点。然后我们可以使用on
或once
来订阅它。使用on
,你可以在更新发生时获得更新,但once
只发出一次当前值。
const node = db.get("1122").once(v => console.log(v));
你可以不断地调用get
。如果引用不存在,它们会被创建。否则,将返回给定路径上的值。让我们来看看一个例子。下面我们将创建一个名为node1
的节点,它有一些属性。
const node1 = db.get("3344").put({
name: "node1"
});
node1.get("doc1").put({
name: "doc1",
});
node1.get("doc1").get("sub_doc").put({
name: 'sub_doc',
});
执行以上代码,会形成如下的数据链:
[图片上传失败...(image-140be0-1651928820273)]
为了获取node1.doc1.sub_doc
,可以使用一连串的get
获得:
node1.get('doc1').get('sub_doc').once(v => console.log(v));
注意:当你使用put
时,如果没有明确指定键,就会自动生成一个键。此外,db
对象会保存一个对该键的引用。例如,当我们做node1.get("doc1").put
时,一个具有唯一键的新节点在幕后被生成。我们可以看到,如果我们记录node1.doc1
的值并查看内部属性_
,如下图:
[图片上传失败...(image-37df8d-1651928820273)]
现在,如果你知道这个节点的唯一键,你可以直接从db
对象中访问它所指向的节点:
db.get('unique_key')...
为了更好的理解上述节点的关系,见下图:
[图片上传失败...(image-7b95fa-1651928820273)]
注意其他两个自动生成的唯一键是如何从db
中直接指向新创建的节点的。
更新 Update
示例代码:
db.get('9871').put({
name: 'Tom',
});
请注意,所有的更新都是部分更新。在上面的代码中,只有name
字段被更新。只要你有一个对节点的引用,你就可以简单地使用put
来更新值。让我们看一下另一个例子:
const n1 = db.get('5416')
.put({
name: 'n1',
prop: '...',
doc1: {
prop: '...',
},
});
const n2 = db.get('8899')
.put({
name: 'n2',
doc2: {
prop: '...',
}
});
n1.get('related_to').put(n2);
在上面的代码中,我们创建了两个节点:n1
和n2
。n1
节点有属性name
,prop
和doc1
。doc1
属性定义了一个子对象,它被自动转化为一个节点,并被一个自动生成的键所引用。
然后我们创建n2
节点,该节点有两个属性name
和doc2
,与n1
相似。最后我们在n1
上创建一个名为related_to
的属性,指向n2
。下图展示了这些关系:
[图片上传失败...(image-f57de-1651928820273)]
现在我们开始更新数据:
- 更新
n1.doc1.prop
的数据
n1.get('doc1').put({
prop: 'other value'
});
- 更新
n1.related_to
n1.get('related_to').put(
db.get('9185').put({
new_prop: 'some value',
}
)
);
在上面的代码中,我们通过创建一个新的节点完全改变了n1
所指向的对象。注意,n2
并没有改变,我们只是更新了related_to
指针。
- 更新
n2
,该节点被n1
引用
n1.get('related_to').put({
new_stuff: 'some value',
other_stuff: 'some value',
})
在上面的代码中,new_stuff
和other_stuff
将被添加到n2
上已有的内容。如果一个属性已经存在,它将被覆盖,否则新的属性将被创建。
删除 Delete
在Gun
中,删除的工作方式有一点不同,可以通过将一个指针设置为null
来使它无法被发现,而不是消除一条记录,如:
db.get('8809').put(null);
在上面的代码中,我们使用get
来找到8809
键的引用。然后,将其设置为null
。只要有一个节点或属性的引用,就可以用put
来把它们设置为null
。
以下是直接从StackOverflow中得到的对
Delete
操作的简要解释。
GUN
中的删除工作就像Mac OSX
或Windows
或Linux
。nulling
告诉每台机器"把这些数据放到垃圾箱/回收站"。这一点很有用,因为它可以让你改变你对删除东西的看法,所以如果你想的话,你可以在以后恢复它。(恢复被删除的内容/文件的情况很多,但大多数人都没有想到)。
集合 Sets
Gun
允许对多条记录进行分组,并将它们加入一个集合。Gun
的集合,是一个具有唯一无序项的数学集合。假设我们有两个节点,我们想为它们创建一个组。首先,我们创建组节点(一个集合),然后我们使用集合方法将其他节点或普通对象添加到其中。
注意,如果是普通对象,就像更新操作一样,将被自动转换为Gun
节点。
const group = db.get('8871'); // create a group node
group.set(n1);
group.set(n2);
group
有两组记录,分别为n1和n2。
也可以用如下方式实现:
const group = db.get('8871');
group.set({
title: 'hello'
});
group.set({
title: 'world'
});
上述代码执行后,数据存储如下图:
[图片上传失败...(image-f62f39-1651928820273)]
关系 Relationship
对现实世界进行建模,就是要确定互相的关系并在数据库中实现它们。图形数据库最擅长于表达关系(Relationship)。在本节中,我将向你展示如何创建节点之间的关系。
正如我们之前看到的,创建关系的最简单方法是使用以下模式。
node1.get('related_to').put(node2)
或在制作节点时明确地创建一个关系。
const node1 = db.get('8891').put({
uuid: '8891',
name: 'node1',
related_to: {
uuid: '9911',
name: 'node2',
},
});
在上面的代码中,related_to
被Gun
自动变成了一个节点,并且引用被存储在node1
中。然后你可以用node.get('related_to')
访问这个链接的节点。
现在,如果你想给一个关系添加属性,你可以创建一个中间节点,并在中间节点内添加关系的属性和链接。
node1.get('related_to').put({
property: "value",
property2: "value",
});
node1.get('related_to').get('node').put(node2);
下图显示了数据关系:
[图片上传失败...(image-c25c14-1651928820273)]
正如你在上图中看到的,related_to
节点通过中间节点的node
属性指向node2
。然后你可以用node1.get('relate_to').get('node')
访问node2
。
订阅 Subscribing
Gun节点的行为类似于可观察物,这意味着它们会随着时间的推移而发出数值。你可以使用on
或once
来订阅枪节点。使用on
,你可以在发生时获得更新,除非你取消订阅。once
方法只检索当前的值,不订阅未来的更新。
遍历记录
给定一组记录,你可以使用map
来遍历它们
myset.map().once(v => console.log(v));
上面的代码将显示myset
中的每条记录一次。它也将获得随着时间推移而增加的记录,但只有一次。
这里有更多你可以使用的模式(直接取自文档)。
-
myset.map().on(cb)
:订阅每条记录的变化,并在未来添加更多记录时订阅myset
-
myset.map().once(cb)
:获取每条记录一次,包括随着时间推移添加的记录。 -
myset.once().map().on(cb)
:获取一次记录列表,但订阅每个myset上的变化,但不订阅以后添加的记录。 -
myset.once().map().once(cb)
:获取一次记录列表,只获取myset中的每条记录一次,而不是后来添加的记录。
结论
GunDB
正在改变我们思考数据库的方式,并且正在慢慢地将我们过渡到一个新的范式。Gun
以及它的相关项目,有很多方面与经典的集中式模型非常不同。如果你刚刚开始学习Gun
,你可能会发现它具有挑战性。首先,因为Gun是一个年轻的项目,你应该期待API的变化。其次,你可能会发现你很难理解文档的内容。
我希望这些系列的文章可以帮助你(和我)更好地理解GunDB
,并作为先前存在的指南的补充。
你可以访问所有的官方文档和指南:https://gun.eco/docs