0%

idb 程式庫使用範例 [1]

前言

自從W3C放棄Web SQL後,Indexed Database成了瀏覽器唯一資料庫部件。要注意的是,這是一套key-value資料庫系統,和SQL系統有很大的不同,他反而比較像是python的dict,這點非常重要,我曾在這邊卡了很久。
然而該套API是非同步式API,大量依賴callback,在使用上顯得比較繁瑣,因此網路上陸續出現一些包裝,下面是目前流行的4種indexed db的程式庫的下載排名
indexeddb 套件下載量
老實說在初步翻閱各官方docs後,dexie是裡面最簡單易用的一套,語法相當的淺顯易懂。然而不知為何本篇要介紹的idb在2019年中忽然使用率大幅攀升,可能是有大神推薦或是有知名套件使用吧。

在使用上,這個套件特色就是使用Promise將API包奘起來,搭配async/await就能寫出語法易懂的程式,另外它的程式庫大小在壓縮後也是4套中最小(僅1kb),相對的額外功能就比較沒這麼多。完全專住在資料庫最核心功能上

但是當我在找尋相關教學的過程中,發現許多文章都是2018/2019之前,然而該程式庫近期有過大改版,很多語法被破壞。此外也缺乏較完整的使用範例,如果原本對IndexedDB/async/promise就不熟的話,很多地方會容易有誤解。因此才有了這篇文章的誕生

P.S. 注意 Indexed Database API到2015才定稿,因此舊式的瀏覽器(尤其是IE)支援度會有問題
P.S.2 這邊專注在立即上手,若要idb詳細介紹可直接參考官網,API可以參考 中文教學

安裝

寫作當下,該套件為6.0.0版
引入該套件有三種方法

使用npm

npm install idb

從瀏覽器引用 - 用import

1
2
3
4
5
6
7
<script type="module">
import { openDB } from 'https://unpkg.com/idb?module';

async function doDatabaseStuff() {
const db = await openDB(…);
}
</script>

從瀏覽器引用 - 傳統引用

1
2
3
4
5
6
<script src="https://unpkg.com/idb/build/iife/index-min.js"></script>
<script>
async function doDatabaseStuff() {
const db = await idb.openDB(…);
}
</script>

建立資料庫

OpenDB - 開啟/建立資料庫

在多方探索下,我發現idb可以有幾種寫法

建立資料庫,這部份三種寫法都一樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// P.S. 這時database接到的是一個promise,故使用上後面要用.then接資料
const database = openDB("test", 1, {
// 新建/更新資料庫版本時執行此段程式
upgrade(db) {
// objectStoreNames是所有資料表名稱的集合 .contains() 可以確定資料表是否存在
if (!db.objectStoreNames.contains("sheet")) {
// 建立名為sheet的資料表,並設定 id 為key,
// 回傳的IDBObjectStore物件可以處理索引
// 注意資料的id不可以重複
const store = db.createObjectStore("sheet", { keyPath: "id" });
// 幫資料表建立索引 store.createIndex(<欄位名稱:str>, <索引名稱:str>)
// 注意:只有前面設定的keyPath及被索引的欄位才能被檢索,並沒有類似SQL模糊搜尋
store.createIndex('date', 'date');
}
}
});

openDB(<資料庫名稱[str]>, <版本[int]>, <新建/升級資料庫callback>)
.upgrade(IDBDatabase, 舊版本[int], 新版本[int], transaction)

開啟資料庫 - 參考
回傳 Promise -> IDBDatabase

IDBDatabase.createObjectStore(<資料表名稱[str]>, <設定項[object] 可省略>)
建立資料表 - 參考
回傳IDBObjectStore

IDBObjectStore.createIndex(<欄位名稱:str>, <索引名稱[str]>, <設定項[object] 可省略>)
建立索引 - 參考

寫法1 - ex:抓取所有資料

1
2
3
4
5
// 抓取並顯示所有資料
database.then(db => {
let data = await db.getALL("sheet");
console.log(data);
});

寫法2/3 - ex:抓取所有資料

1
2
3
4
5
6
7
8
9
10
11
(async() => {
let data;

// 取得資料方法2 db是IDBDatabase
data = await database.then(db => db.getAll("sheet"));

// 取得資料方法3 (await DB) 會得到IDBDatabase
data = await (await database).getAll("sheet");

console.log(data);
})();

寫法4 通通都寫在async函數裡面,適合短暫使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function get() {
// database = openDB在函數外面
const DB = await database;
// database = openDB宣告在裡面
const DB = await openDB("test", 1, {
upgrade(db) {
if (!db.objectStoreNames.contains("sheet")) {
db.createObjectStore("sheet", { keyPath: "id" });
}
}
});
data = (await DB).getAll("sheet");
console.log(data);
}
get().then(data => console.log(data));

以下沒特別說明的話一律使用寫法一

插入/更新資料

由於idb將便捷功能都直接綁定在 IDBDatabase 上(OpenDB回傳的物件),因此很多資料相關動作都能在上面解決。

新增資料 .add()

.add(<資料表名稱[str]>, <資料>, <[object]指定給主鍵的值,可省略>)
注意:如果插入數據的 keyPath 跟資料表內的資料有重複的話會引起失敗

1
2
3
4
database.then(async(db) => {
await db.add("sheet", {"name": "test", "date": "2020-10-02"});
//...後續處理
});

新增/更新資料 .put()

.put(<資料表名稱[str]>, <資料>, <[object]指定給主鍵的值,可省略>)
根據資料內或指定的主鍵找出舊有資料並更新
如果資料內沒有主鍵欄位,或是主鍵資料沒在資料表內找到,會變成插入該筆資料

事務模式

indexeddb支援多筆資料更動,其中一筆失敗也能回復到更動前的狀態

1
2
3
4
5
6
7
8
9
10
11
const datas = [...];
database.then(async(db) => {
// 進入事務模式,在結束之前不可以await idb以外的promise,否則會被中斷
const tx = db.transaction('sheet', 'readwrite');
const store = tx.objectStore('sheet');
for (data of datas) {
await store.put(data);
}
// 結束事務模式
await tx.done();
});

搜尋/刪除資料

主鍵

抓取資料 .get()

.get(<資料表名稱[str]>, <比對的主鍵值或範圍[str, IDBKeyRange]>)

1
2
3
4
// 抓取sheet內 id(keyPath) 為 1 的資料
database.then(async(db) => {
const value = await db.get('sheet', 1);
});

抓取全部資料 .getAll()

.getAll(<資料表名稱[str]>, <比對的主鍵值或範圍[str, IDBKeyRange],可省略>, <[int] 抓幾筆資料,可省略>)

1
2
3
4
// 抓取 sheet 內所有的資料
database.then(async(db) => {
const values = await db.getAll('sheet');
});

抓取所有key值 .getAllKeys()

1
2
3
4
// 抓取 sheet 內每筆資料的keyPath
database.then(async(db) => {
const keys = await db.getAllKeys('sheet');
});

P.S.其實還有抓取key值得getKey(key),但是你都知道key值了還抓個鳥…

取得資料總筆數 .count()

1
2
3
database.then(async(db) => {
const total = await db.count('sheet');
});

刪除資料 .delete()

.delete(<資料表名稱[str]>, <比對的主鍵值或範圍[str, IDBKeyRange],可省略>)

1
2
3
4
// 刪除sheet內 id(keyPath) 為 1 的資料
database.then(async(db) => {
await db.delete('sheet', 1);
});

清空資料表內所有資料 .clear()

.clear(<資料表名稱[str]>)

1
2
3
database.then(async(db) => {
await db.clear('sheet');
});

index

看到這邊可以注意到,似乎在搜尋上只能使用一開始設定的主鍵keyPath
如果要針對別的欄位做比對搜尋,就得再建立資料庫的時候,對日後預計會使用到的欄位建立index
store.createIndex( <索引名稱[str]>, <欄位路徑[str]>, {uniqle[bool]是否允許index重複-可省略, multiEntry[bool] 是否支援陣列內索引-可省略})
這邊稍微介紹一下:
假設資料結構是:

1
2
3
4
5
6
7
8
9
{
name: "Peter",
age: 14,
score: {
chinese: 90,
math: 40,
},
girlfriends: ["Mary", "Susan", "Lily"]
}

如果要針對chinese來下index可以用這個語法
store.createIndex("chinese", "score.chinese")
如果要針對陣列內所有的值可以用以下語法
store.createIndex("girlfriends", "girlfriends", {multiEntry:true})

範圍搜尋

indexeddb的搜尋是依賴 IDBKeyRange 物件,該物件除了index外野適用於keyPath,一共有4種用法:

  • IDBKeyRange.lowerBound(x, [bool]是否包含x ):指定下限。
  • IDBKeyRange.upperBound(x, [bool]是否包含x ):指定上限。
  • IDBKeyRange.bound(x, y, [bool]是否包含x, [bool]是否包含y):同时指定上下限。
  • IDBKeyRange.only():指定只包含一个值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 搜尋chinese分數大於50的同學
database.then(async(db) => {
const query = IDBKeyRange.lowerBound(50);
const students = await db.getAllFromIndex('sheet', 'chinese', query);
});

// 搜尋math分數 50 < score <= 80
database.then(async(db) => {
const query = IDBKeyRange.bound(50, 80, false, true);
const students = await db.getAllFromIndex('sheet', 'math', query);
});

// math分數剛好 60 的有幾位
database.then(async(db) => {
const query = IDBKeyRange.only(60);
const total = await db.countFromIndex('sheet', 'math', query);
});

以上是大致上的運用,後續會在談到其他比較進階的功能,下回見~