如何在 Node.js 中使用 SQLite
了解如何創建 SQLite 數據庫並從 Node.js 訪問它以創建表、插入數據和讀取數據。
開始使用
因為我們為本教程編寫的代碼是“獨立的”(意味著它不是更大的應用程序或項目的一部分),所以我們將從頭開始創建一個 Node.js 項目。如果您的計算機上尚未安裝 Node.js,請先閱讀本教程,然後再返回此處。
在計算機上安裝 Node.js 後,從計算機上的項目文件夾(例如,~/projects
),為我們的工作創建一個新文件夾:
終端
mkdir app
接下來,cd
進入該目錄並創建一個 index.js
文件(這是我們編寫教程代碼的地方):
終端
cd app && touch index.js
接下來,我們要安裝兩個額外的依賴項,sqlite3
和 lorem-ipsum
:
終端
npm i sqlite3 lorem-ipsum
第一個將讓我們訪問 SQLite 的 Node.js 驅動程序(我們將在代碼中使用它來連接到數據庫),而第二個將幫助我們生成一些測試數據以插入到我們的數據庫中。
最後一步:在 package.json
為您創建的文件,請確保添加字段 "type": "module"
作為財產。這將啟用 ESModules 支持並允許我們使用 import
如下代碼所示。
有了這些,我們就可以開始了。
SQLite 簡介
當大多數人想到數據庫時,他們會想到從磁盤(如 PostgreSQL)或直接從內存(如 Redis)寫入和檢索數據的東西。但是,為了讓這些數據庫正常工作,它們需要一個數據庫服務器 :處理入站連接的長時間運行的進程。
對於成熟的應用程序,這些類型的數據庫很有幫助,因為它們提供了豐富的功能集並允許您管理大量數據。
但是,在某些情況下,這些類型的數據庫是有問題的,即,當您試圖盡可能減少佔用空間或限制與應用程序一起運行的“昂貴”(就 CPU 和內存而言)進程的數量時。為了解決這個問題,我們有一種不同形式的數據庫,稱為嵌入式數據庫 .這些是不的數據庫 需要服務器才能運行,這意味著它們可以在資源有限的環境中運行(例如,Raspberry Pi)。
這種類型的數據庫最流行的選項是 SQLite。一個基於 SQL 的數據庫,它作為單個文件使用特殊格式存儲其數據。 SQLite 中的“數據庫”只是一個像 posts.db
這樣的文件 或 users.db
.當您使用驅動程序與 SQLite 交互時,您會讀取和寫入此文件。為了簡單起見,SQLite 提供了一組有限的數據類型(只有五種:NULL
, INTEGER
, REAL(FLOAT)
, TEXT
, 和 BLOB
)。
添加新的數據庫和表
要開始使用我們的代碼,我們要確保有兩件事可供我們使用:
- 將數據寫入到的數據庫 .
- 該數據庫中用於組織數據的表。
讓我們從連接到數據庫的連接開始(如果它不存在,我們的驅動程序將為我們創建它),然後向其中添加一個表。
/index.js
import sqlite3 from 'sqlite3';
const SQLite3 = sqlite3.verbose();
const db = new SQLite3.Database('posts.db');
就幾行。首先,我們需要導入 sqlite3
來自我們之前通過 NPM 安裝的包。請記住:這是驅動程序 包(我們用來與數據庫通信的)而不是 SQLite 本身。接下來,我們新建一個變量SQLite3
(我們使用這個大小寫是因為我們希望收到一個 JavaScript 類作為回報——這個大小寫是一種表示這一點的常用模式),它被分配給對 sqlite3.verbose()
的調用 .這為我們提供了一個類的副本,我們將使用 verbose 來啟動驅動程序 模式,這意味著它將包含遇到的任何錯誤的完整堆棧跟踪(有助於調試)。
接下來,在我們的類中,我們再創建一個變量 db
這使我們可以通過調用 new SQLite3.Database('posts.db')
訪問我們的實際實例/與數據庫的連接 .這裡,posts.db
是我們要連接的數據庫的名稱。如果這個數據庫(我們項目根目錄的一個文件)不存在,驅動程序會為我們創建它。
創建一個承諾包裝器
在我們開始創建數據之前,為了讓我們的工作更輕鬆,我們將快速編寫一個包裝函數,為我們提供一個承諾的 sqlite3
版本 司機。我們想要這樣做是因為默認情況下包使用回調模式(這會導致代碼混亂)。
/index.js
import sqlite3 from 'sqlite3';
import { LoremIpsum } from 'lorem-ipsum';
const SQLite3 = sqlite3.verbose();
const db = new SQLite3.Database('posts.db');
const query = (command, method = 'all') => {
return new Promise((resolve, reject) => {
db[method](command, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
在我們的設置代碼下方,我們在這裡添加了一個新函數 query
(名稱是任意的)它有兩個參數:command
這是我們要運行的 SQL 語句和 method
這是 sqlite3
司機 我們要調用的方法。
在該函數內部,我們返回一個新的 JavaScript Promise,它包裝了對 db[method]
的調用 db
是我們剛剛在上面設置的連接/實例和[method]
我們是否使用 JavaScript 括號表示法來表示“調用與 method
的當前值同名的方法 變量。”例如,如果我們不 為 method
傳遞任何內容 , 默認情況下我們通過 all
這意味著我們將在這裡運行 db.all()
.如果我們將方法傳遞為 get
,我們會做 db.get()
.
因為我們希望該方法是一個函數,所以我們稱它為 db[method]()
,傳遞我們的 SQL command
作為第一個參數,然後傳遞一個回調函數接收 error
或 result
作為第二個參數。
在該函數內部,如果我們有一個 error
我們要調用 reject()
我們的 Promise 中的方法傳遞 error
發生了,如果一切正常,我們想調用 resolve()
來自我們的 Promise 的方法,傳遞 result
我們收到了。
有了這個,我們就可以開始在我們的數據庫上運行命令了。
向表中插入數據
顧名思義,SQLite 只是一個 SQL 數據庫。如果您熟悉 other 使用的基本 SQL 語法,則保留某些限制 數據庫(例如 PostgreSQL 或 MySQL),你會覺得在家裡寫。首先,為了真正放 數據進入我們的數據庫,我們需要在該數據庫中存在一個表。為此,我們將使用 query()
我們剛剛連接的函數。
/index.js
import sqlite3 from 'sqlite3';
const SQLite3 = sqlite3.verbose();
const db = new SQLite3.Database('posts.db');
const query = (command, method = 'all') => { ... };
db.serialize(async () => {
await query("CREATE TABLE IF NOT EXISTS posts (date text, title text, author text, content text, tags text)", 'run');
});
在我們文件的底部,我們調用了一個新函數 db.serialize()
它本身接收一個功能。這個函數告訴 sqlite3
我們想要序列化對數據庫的調用的驅動程序,這意味著我們在傳遞給它的函數中執行的每個 SQL 命令都會運行並完成之前 允許執行下一條SQL命令。
現在,我們裡面只有一個命令。為了運行它,我們使用我們的 query()
我們剛剛連接的函數,以 await
為前綴 關鍵字(這就是為什麼我們有 async
關鍵字作為我們傳遞給 db.serialize()
的函數的前綴 ——沒有那個,我們的 await
語句會拋出錯誤)。
我們將要運行的 SQL 命令作為第一個參數傳遞給它,然後是 method
我們想在我們的數據庫驅動程序上運行作為第二個參數:run
.如果我們仔細查看命令,我們的目標是創建一個名為 posts
的新表 在我們的數據庫中如果它不存在 .對於該表,我們定義了五列:
date
這是一個 ISO-8601 日期字符串,例如2022-04-29T00:00:00.000Z
.title
這是我們帖子的標題作為字符串。author
這是帖子作者的字符串形式的名稱。content
這是我們帖子的字符串內容。tags
這是一個以逗號分隔的字符串標籤列表。
有了這個,當我們運行我們的 index.js
文件(從我們的終端,在項目的根目錄,我們可以運行 node index.js
運行代碼),如果 posts
posts.db
中不存在表 , SQLite 將使用指定的列創建它。
/index.js
import sqlite3 from 'sqlite3';
import { LoremIpsum } from 'lorem-ipsum';
const SQLite3 = sqlite3.verbose();
const db = new SQLite3.Database('posts.db');
const query = (command, method = 'all') => { ... };
const createPostsIfEmpty = async () => {
const existingPosts = await query('SELECT * FROM posts');
if (existingPosts?.length === 0) {
const lorem = new LoremIpsum();
for (let i = 0; i < 1000; i += 1) {
const tags = [...Array(3)].map(() => lorem.generateWords(1));
await query(`INSERT INTO posts VALUES ("${new Date().toISOString()}", "${lorem.generateWords(10)}", "Ryan Glover", "${lorem.generateParagraphs(5)}", "${tags}")`, 'run');
}
}
};
db.serialize(async () => {
await query("CREATE TABLE IF NOT EXISTS posts (date text, title text, author text, content text, tags text)", 'run');
await createPostsIfEmpty();
});
接下來,使用我們的表,我們要創建一些測試數據。為此,我們將在上方添加另一個函數 我們對 db.serialize()
的調用 稱為createPostsIfEmpty()
.
顧名思義,我們的目標是檢查我們的 posts
table 為空,如果是,插入一些測試數據供我們讀取。
就像我們在上面看到的,我們在這裡定義的函數需要以 async
為前綴 所以我們可以安全地使用 await
關鍵字不會觸發 JavaScript 錯誤。
在該函數內部,我們要做的第一件事是檢查是否有任何帖子。為此,我們調用 await query()
傳遞 SQL 語句 SELECT * FROM posts
上面寫著“從 posts
中選擇所有列 表。”請注意,我們這樣做不是 將第二個參數傳遞給 query()
在這裡,意思是,我們要使用默認的 all
驅動程序的方法(返回 all 與我們的查詢匹配的行作為數組)。
如果我們收到的數組返回——這裡,存儲在 existingPosts
變量——長度為 0
(表示表是空的),我們要插入一些數據。
為此,我們在頂部導入了 LoremIpsum
lorem-ipsum
中的類 我們之前安裝的軟件包。顧名思義,這個包將幫助我們即時生成一些假數據。
要使用它,首先,我們需要通過調用 new LoremIpsum()
創建一個實例 ,我們將其存儲在變量 lorem
中 這裡。接下來,要創建我們的數據,我們將使用 JavaScript for
循環將在我們的 posts
中創建 1000 個帖子 表。
在那個 for
裡面 循環,首先,我們創建一個變量tags
這將生成一個包含 3 個字符串的數組,其中每個字符串都是調用 lorem.generateWords(1)
的結果 .為此,我們使用 Array(3)
的小技巧 說“創建一個由 3 個元素組成的數組”,這將是 undefined 元素,然後使用 ...
展開運算符將它們解包到另一個數組中(技術上沒有必要,但可以確定我們的 .map()
調用在實際數組值上運行)。接下來,我們使用 .map()
遍歷 undefined
的數組 值,對於每個值,通過 lorem.generateWords()
返回一個字符串 .
有了這個,我們再次使用我們的 query()
執行 SQL 命令的函數,這次執行的是 INSERT
進入我們的 posts
桌子。作為第二個參數,我們傳遞 run
作為表示我們只想運行的方法 這個命令,不要期望返回值。
這就是將數據放入表中的過程。現在,對於我們的最後一步,讓我們學習如何讀回我們剛剛插入的數據。
讀取數據
只需一個班輪即可完成此任務。退回到我們的 db.serialize()
函數,現在,我們應該有一些可以查詢的數據了:
/index.js
import sqlite3 from 'sqlite3';
import { LoremIpsum } from 'lorem-ipsum';
const SQLite3 = sqlite3.verbose();
const db = new SQLite3.Database('posts.db');
const query = (command, method = 'all') => {
return new Promise((resolve, reject) => {
db[method](command, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
const createPostsIfEmpty = async () => {
const existingPosts = await query('SELECT * FROM posts');
if (existingPosts?.length === 0) {
const lorem = new LoremIpsum();
for (let i = 0; i < 1000; i += 1) {
const tags = [...Array(3)].map(() => lorem.generateWords(1));
await query(`INSERT INTO posts VALUES ("${new Date().toISOString()}", "${lorem.generateWords(10)}", "Ryan Glover", "${lorem.generateParagraphs(5)}", "${tags}")`, 'run');
}
}
};
db.serialize(async () => {
await query("CREATE TABLE IF NOT EXISTS posts (date text, title text, author text, content text, tags text)", 'run');
await createPostsIfEmpty();
const existingPosts = await query('SELECT rowid as id, date, title, author, content, tags FROM posts');
console.log(existingPosts);
});
在底部,我們使用 query()
最後一次執行 SELECT
命令,這次傳遞我們要檢索的特定字段(這裡的突出之處是讀回 rowid as id
其中 rowid
是 SQLite 為我們添加的默認 ID,但我們沒有在創建表時指定)。因為我們默認是all
方法,我們希望這會返回我們插入到 posts
中的全部 1000 行 .
如果我們註銷 existingPosts
,我們有一個正常運行的 SQLite 數據庫!
總結
在本教程中,我們學習瞭如何連接 SQLite 數據庫。我們學習瞭如何即時創建數據庫文件以及如何創建可以插入數據的表。接下來,我們學習瞭如何插入數據,然後查詢該數據。為了保持我們的代碼乾淨,我們還學習瞭如何編寫一個返回 JavaScript Promise 的包裝函數,從而使我們能夠輕鬆地編寫對數據庫的異步調用。