原文鏈接:GunDB, a Graph Database in JavaScript
最近我一直在使用GunDB,想和你分享我目前學(xué)到的一些東西。GunDB不僅僅是一個(gè)圖形數(shù)據(jù)庫(kù)(Graph DB)。它更是一個(gè)項(xiàng)目組,旨在簡(jiǎn)化擴(kuò)展,提高數(shù)據(jù)安全性,節(jié)約成本,并賦予應(yīng)用程序開(kāi)發(fā)人員更多的能力。
在這篇文章中,我將完全把Gun作為一個(gè)數(shù)據(jù)庫(kù)來(lái)探討,并在接下來(lái)的文章中隨著我的進(jìn)展和學(xué)習(xí),探索其他方面。
所有文章中的源碼在這里可以找到。
簡(jiǎn)介
一般來(lái)說(shuō),數(shù)據(jù)庫(kù)是一個(gè)軟件,安裝在你的電腦或遠(yuǎn)程服務(wù)器上,用來(lái)存儲(chǔ)數(shù)據(jù)。這些數(shù)據(jù)可以存儲(chǔ)在磁盤(pán)上或內(nèi)存中。
數(shù)據(jù)庫(kù)有不同的類(lèi)型:關(guān)系型(Relational DB)、面向文檔型(Document DB)、鍵值型(Key-value DB)或圖形型(Graph DB)。下面是一些例子:
- 關(guān)系型:MySql, PostgreSQL, SQL Server
- 面向文檔的:MongoDB, CouchDB
- 鍵值型:Redis, LevelDB
- 基于圖形:Neo4j、OrientDB
與其他數(shù)據(jù)庫(kù)不同,Gun沒(méi)有二進(jìn)制文件需要安裝,Gun是用JavaScript編寫(xiě)的,這意味著你可以在任何運(yùn)行JavaScript的地方使用它。開(kāi)始使用Gun就像下載一個(gè)JavaScrip文件或用npm安裝一個(gè)插件一樣容易。
開(kāi)始使用
開(kāi)始使用Gun的最簡(jiǎn)單方法是將其作為一個(gè)單一的JavaScript文件下載。你可以從以下網(wǎng)址下載最新的簡(jiǎn)化版本: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>
然后你可以在瀏覽器中打開(kāi)Html文件,并使用控制臺(tái)console與Gun互動(dòng)。在大多數(shù)瀏覽器中,你可以在頁(yè)面上點(diǎn)擊右鍵,在彈出菜單中選擇檢查元素來(lái)打開(kāi)控制臺(tái)。下面例子可以用它來(lái)創(chuàng)建一個(gè)汽車(chē)記錄并打印它的制造商。
const db = window.Gun();
const car = db.get("123").put({
make: "Toyota",
model: "Camry",
});
car.once(v => console.log(v.make)); // --> Toyota
在上面的代碼中,首先,我們創(chuàng)建一個(gè)Gun的實(shí)例。然后,使用123為鍵key的get方法來(lái)引用一個(gè)空節(jié)點(diǎn)。接下來(lái),我們使用put方法和一個(gè)普通的JavaScript對(duì)象添加一些數(shù)據(jù)。最后,我們使用相同的key來(lái)獲取記錄(節(jié)點(diǎn)),并使用once方法讀出值。注意,普通對(duì)象會(huì)自動(dòng)轉(zhuǎn)換為Gun節(jié)點(diǎn)。我將在后面的"基礎(chǔ)知識(shí)"部分更詳細(xì)地解釋每個(gè)方法。
下面是一個(gè)示意圖,幫助你更好地理解正在發(fā)生的事情。
[圖片上傳失敗...(image-96c992-1651928820273)]
現(xiàn)在讓我們用Node來(lái)實(shí)驗(yàn)Gun。首先,創(chuàng)建一個(gè)目錄并使用npm安裝Gun。
cd ~ && mkdir gun-demo && cd gun-demo
npm init -y
npm i gun -S
創(chuàng)建一個(gè)main.js文件如下:
const db = require('gun')();
const car = db.get("123").put({
make: "Toyota",
model: "Camry",
});
car.once(v => console.log(v.make)); // --> Toyota
然后通過(guò)node main.js執(zhí)行該文件,你應(yīng)該能看到控制臺(tái)中輸出Toyota。另外,你也可以在Node的REPL中使用Gun,首先在終端中調(diào)用node,然后運(yùn)行以下程序。
const db = require('gun')();
然后就可以與db對(duì)象進(jìn)行交互,如果想關(guān)閉REPL,按Ctrl+c兩次。
Gun的基礎(chǔ)知識(shí)
在以下章節(jié)中,我將向你展示Gun的基礎(chǔ)知識(shí),并探索其創(chuàng)建Create、讀取Read、更新Update和刪除Delete記錄(即CRUD)的基本方法。我還將向你展示如何創(chuàng)建集合Sets和關(guān)系Relationships。我還將簡(jiǎn)要地提到訂閱記錄subscribing以獲得更新的不同方法。請(qǐng)注意,我將交替使用記錄record和節(jié)點(diǎn)node這個(gè)術(shù)語(yǔ),因?yàn)?code>Gun是一個(gè)圖數(shù)據(jù)庫(kù),記錄被表示為節(jié)點(diǎn)node。
CRUD
創(chuàng)建 Create
示例代碼:
const entry = db.get('8899').put({
uuid: '8899',
some_prop: 'some value',
});
在上面的代碼中,我們使用get方法來(lái)創(chuàng)建一個(gè)使用8899為key的節(jié)點(diǎn)的引用。然后,我們使用put方法,用一個(gè)普通的JavaScript對(duì)象將數(shù)據(jù)添加到該節(jié)點(diǎn)。普通對(duì)象會(huì)自動(dòng)轉(zhuǎn)換為Gun節(jié)點(diǎn)。
注意,如果給定的key已經(jīng)存在,添加的數(shù)據(jù)可能會(huì)覆蓋現(xiàn)有的數(shù)據(jù)。我將在"更新"部分更詳細(xì)地介紹更新。下圖展示了數(shù)據(jù)庫(kù)中的給定鍵是如何指向一個(gè)節(jié)點(diǎn)的。
[圖片上傳失敗...(image-e33be2-1651928820273)]
關(guān)于Key的說(shuō)明:你應(yīng)該總是使用唯一的鍵。你可能想對(duì)通用節(jié)點(diǎn)使用uuids,并為索引目的使用與可讀字符串相結(jié)合的Hash字符串。在使用Gun時(shí),命名是非常重要的,因?yàn)樗袛?shù)據(jù)都存在于全局空間。你可能需要這個(gè)Reticle擴(kuò)展,以幫助你對(duì)你的鍵進(jìn)行命名。
讀取 Read
我們可以使用get方法來(lái)查詢(xún)一個(gè)給定Key的節(jié)點(diǎn)。然后我們可以使用on或once來(lái)訂閱它。使用on,你可以在更新發(fā)生時(shí)獲得更新,但once只發(fā)出一次當(dāng)前值。
const node = db.get("1122").once(v => console.log(v));
你可以不斷地調(diào)用get。如果引用不存在,它們會(huì)被創(chuàng)建。否則,將返回給定路徑上的值。讓我們來(lái)看看一個(gè)例子。下面我們將創(chuàng)建一個(gè)名為node1的節(jié)點(diǎn),它有一些屬性。
const node1 = db.get("3344").put({
name: "node1"
});
node1.get("doc1").put({
name: "doc1",
});
node1.get("doc1").get("sub_doc").put({
name: 'sub_doc',
});
執(zhí)行以上代碼,會(huì)形成如下的數(shù)據(jù)鏈:
[圖片上傳失敗...(image-140be0-1651928820273)]
為了獲取node1.doc1.sub_doc,可以使用一連串的get獲得:
node1.get('doc1').get('sub_doc').once(v => console.log(v));
注意:當(dāng)你使用put時(shí),如果沒(méi)有明確指定鍵,就會(huì)自動(dòng)生成一個(gè)鍵。此外,db對(duì)象會(huì)保存一個(gè)對(duì)該鍵的引用。例如,當(dāng)我們做node1.get("doc1").put時(shí),一個(gè)具有唯一鍵的新節(jié)點(diǎn)在幕后被生成。我們可以看到,如果我們記錄node1.doc1的值并查看內(nèi)部屬性_,如下圖:
[圖片上傳失敗...(image-37df8d-1651928820273)]
現(xiàn)在,如果你知道這個(gè)節(jié)點(diǎn)的唯一鍵,你可以直接從db對(duì)象中訪問(wèn)它所指向的節(jié)點(diǎn):
db.get('unique_key')...
為了更好的理解上述節(jié)點(diǎn)的關(guān)系,見(jiàn)下圖:
[圖片上傳失敗...(image-7b95fa-1651928820273)]
注意其他兩個(gè)自動(dòng)生成的唯一鍵是如何從db中直接指向新創(chuàng)建的節(jié)點(diǎn)的。
更新 Update
示例代碼:
db.get('9871').put({
name: 'Tom',
});
請(qǐng)注意,所有的更新都是部分更新。在上面的代碼中,只有name字段被更新。只要你有一個(gè)對(duì)節(jié)點(diǎn)的引用,你就可以簡(jiǎn)單地使用put來(lái)更新值。讓我們看一下另一個(gè)例子:
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);
在上面的代碼中,我們創(chuàng)建了兩個(gè)節(jié)點(diǎn):n1和n2。n1節(jié)點(diǎn)有屬性name,prop和doc1。doc1屬性定義了一個(gè)子對(duì)象,它被自動(dòng)轉(zhuǎn)化為一個(gè)節(jié)點(diǎn),并被一個(gè)自動(dòng)生成的鍵所引用。
然后我們創(chuàng)建n2節(jié)點(diǎn),該節(jié)點(diǎn)有兩個(gè)屬性name和doc2,與n1相似。最后我們?cè)?code>n1上創(chuàng)建一個(gè)名為related_to的屬性,指向n2。下圖展示了這些關(guān)系:
[圖片上傳失敗...(image-f57de-1651928820273)]
現(xiàn)在我們開(kāi)始更新數(shù)據(jù):
- 更新
n1.doc1.prop的數(shù)據(jù)
n1.get('doc1').put({
prop: 'other value'
});
- 更新
n1.related_to
n1.get('related_to').put(
db.get('9185').put({
new_prop: 'some value',
}
)
);
在上面的代碼中,我們通過(guò)創(chuàng)建一個(gè)新的節(jié)點(diǎn)完全改變了n1所指向的對(duì)象。注意,n2并沒(méi)有改變,我們只是更新了related_to指針。
- 更新
n2,該節(jié)點(diǎn)被n1引用
n1.get('related_to').put({
new_stuff: 'some value',
other_stuff: 'some value',
})
在上面的代碼中,new_stuff和other_stuff將被添加到n2上已有的內(nèi)容。如果一個(gè)屬性已經(jīng)存在,它將被覆蓋,否則新的屬性將被創(chuàng)建。
刪除 Delete
在Gun中,刪除的工作方式有一點(diǎn)不同,可以通過(guò)將一個(gè)指針設(shè)置為null來(lái)使它無(wú)法被發(fā)現(xiàn),而不是消除一條記錄,如:
db.get('8809').put(null);
在上面的代碼中,我們使用get來(lái)找到8809鍵的引用。然后,將其設(shè)置為null。只要有一個(gè)節(jié)點(diǎn)或?qū)傩缘囊茫涂梢杂?code>put來(lái)把它們?cè)O(shè)置為null。
以下是直接從StackOverflow中得到的對(duì)
Delete操作的簡(jiǎn)要解釋。
GUN中的刪除工作就像Mac OSX或Windows或Linux。nulling告訴每臺(tái)機(jī)器"把這些數(shù)據(jù)放到垃圾箱/回收站"。這一點(diǎn)很有用,因?yàn)樗梢宰屇愀淖兡銓?duì)刪除東西的看法,所以如果你想的話(huà),你可以在以后恢復(fù)它。(恢復(fù)被刪除的內(nèi)容/文件的情況很多,但大多數(shù)人都沒(méi)有想到)。
集合 Sets
Gun允許對(duì)多條記錄進(jìn)行分組,并將它們加入一個(gè)集合。Gun的集合,是一個(gè)具有唯一無(wú)序項(xiàng)的數(shù)學(xué)集合。假設(shè)我們有兩個(gè)節(jié)點(diǎn),我們想為它們創(chuàng)建一個(gè)組。首先,我們創(chuàng)建組節(jié)點(diǎn)(一個(gè)集合),然后我們使用集合方法將其他節(jié)點(diǎn)或普通對(duì)象添加到其中。
注意,如果是普通對(duì)象,就像更新操作一樣,將被自動(dòng)轉(zhuǎn)換為Gun節(jié)點(diǎn)。
const group = db.get('8871'); // create a group node
group.set(n1);
group.set(n2);
group有兩組記錄,分別為n1和n2。
也可以用如下方式實(shí)現(xiàn):
const group = db.get('8871');
group.set({
title: 'hello'
});
group.set({
title: 'world'
});
上述代碼執(zhí)行后,數(shù)據(jù)存儲(chǔ)如下圖:
[圖片上傳失敗...(image-f62f39-1651928820273)]
關(guān)系 Relationship
對(duì)現(xiàn)實(shí)世界進(jìn)行建模,就是要確定互相的關(guān)系并在數(shù)據(jù)庫(kù)中實(shí)現(xiàn)它們。圖形數(shù)據(jù)庫(kù)最擅長(zhǎng)于表達(dá)關(guān)系(Relationship)。在本節(jié)中,我將向你展示如何創(chuàng)建節(jié)點(diǎn)之間的關(guān)系。
正如我們之前看到的,創(chuàng)建關(guān)系的最簡(jiǎn)單方法是使用以下模式。
node1.get('related_to').put(node2)
或在制作節(jié)點(diǎn)時(shí)明確地創(chuàng)建一個(gè)關(guān)系。
const node1 = db.get('8891').put({
uuid: '8891',
name: 'node1',
related_to: {
uuid: '9911',
name: 'node2',
},
});
在上面的代碼中,related_to被Gun自動(dòng)變成了一個(gè)節(jié)點(diǎn),并且引用被存儲(chǔ)在node1中。然后你可以用node.get('related_to')訪問(wèn)這個(gè)鏈接的節(jié)點(diǎn)。
現(xiàn)在,如果你想給一個(gè)關(guān)系添加屬性,你可以創(chuàng)建一個(gè)中間節(jié)點(diǎn),并在中間節(jié)點(diǎn)內(nèi)添加關(guān)系的屬性和鏈接。
node1.get('related_to').put({
property: "value",
property2: "value",
});
node1.get('related_to').get('node').put(node2);
下圖顯示了數(shù)據(jù)關(guān)系:
[圖片上傳失敗...(image-c25c14-1651928820273)]
正如你在上圖中看到的,related_to節(jié)點(diǎn)通過(guò)中間節(jié)點(diǎn)的node屬性指向node2。然后你可以用node1.get('relate_to').get('node')訪問(wèn)node2。
訂閱 Subscribing
Gun節(jié)點(diǎn)的行為類(lèi)似于可觀察物,這意味著它們會(huì)隨著時(shí)間的推移而發(fā)出數(shù)值。你可以使用on或once來(lái)訂閱槍節(jié)點(diǎn)。使用on,你可以在發(fā)生時(shí)獲得更新,除非你取消訂閱。once方法只檢索當(dāng)前的值,不訂閱未來(lái)的更新。
遍歷記錄
給定一組記錄,你可以使用map來(lái)遍歷它們
myset.map().once(v => console.log(v));
上面的代碼將顯示myset中的每條記錄一次。它也將獲得隨著時(shí)間推移而增加的記錄,但只有一次。
這里有更多你可以使用的模式(直接取自文檔)。
-
myset.map().on(cb):訂閱每條記錄的變化,并在未來(lái)添加更多記錄時(shí)訂閱myset -
myset.map().once(cb):獲取每條記錄一次,包括隨著時(shí)間推移添加的記錄。 -
myset.once().map().on(cb):獲取一次記錄列表,但訂閱每個(gè)myset上的變化,但不訂閱以后添加的記錄。 -
myset.once().map().once(cb):獲取一次記錄列表,只獲取myset中的每條記錄一次,而不是后來(lái)添加的記錄。
結(jié)論
GunDB正在改變我們思考數(shù)據(jù)庫(kù)的方式,并且正在慢慢地將我們過(guò)渡到一個(gè)新的范式。Gun以及它的相關(guān)項(xiàng)目,有很多方面與經(jīng)典的集中式模型非常不同。如果你剛剛開(kāi)始學(xué)習(xí)Gun,你可能會(huì)發(fā)現(xiàn)它具有挑戰(zhàn)性。首先,因?yàn)镚un是一個(gè)年輕的項(xiàng)目,你應(yīng)該期待API的變化。其次,你可能會(huì)發(fā)現(xiàn)你很難理解文檔的內(nèi)容。
我希望這些系列的文章可以幫助你(和我)更好地理解GunDB,并作為先前存在的指南的補(bǔ)充。
你可以訪問(wèn)所有的官方文檔和指南:https://gun.eco/docs