Dexie.jsとTypeScriptでIndexedDBを操作する
Web技術を使用してリッチクライアントを作成するにはクライアント側でデータを保持することが時に求められます。データの保持はHTML5のAPIとしてkey-valueストアのWeb Storage、NoSQLのIndexedDB、2種類が存在します。今回はIndexedDBをTypeScriptで操作してみます。
Dexie.jsとTypeScriptでIndexedDBを操作する
Dexie.jsとは
Dexie.jsはIndexedDBの操作をラップするライブラリです。IndexedDBはとても強力ですが、全ての操作が非同期のため、直接APIを使用するとコールバック地獄に陥り辛い思いをします。また定義したIndexに対する検索を行うことができますが使い方が少し複雑です。 Dexei.jsはIndexedDBの操作をPromiseでラップしてコールバック地獄を解消し、また直感的にデータの出し入れを行うことができます。
環境
- TypeScript 2.6.2
- Dexie.js 2.0.2
Dexieのインストール
Dexieを使用するには、まずnpmからインストールします。
npm install --save dexie
データベースを作成する
Dexie.jsを使用してデータベースを作成するには2つ方法があります。1つはDexieのサンプルページにあるDexieを継承したデータベースのクラスを作成すること、もう1つはDexieをインスタンス化して操作することです。
データベース作成(クラスとして)
まずはデータベースクラスを作成してみます。データベースクラスはDexieを継承し、コンストラクタ内にデータベース名とスキーマを指定します。
import Dexie from 'dexie'; /** * IndexedDBに保存するオブジェクト */ export interface SampleEntity { id?: number; name?: string | null; age?: number | null; } /** * IndexedDBをラップするDexieクラス */ export class SampleDatabase extends Dexie { // テーブルをプロパティとして定義 // ジェネリックの1つ目はストアするオブジェクト、2つ目はキーの型 table_1: Dexie.Table<SampleEntity, number>; table_2: Dexie.Table<SampleEntity, number>; constructor() { super('sample-database'); // データベース名をsuperのコンストラクタに渡す // テーブルとインデックスを定義する this.version(1).stores({ table_1: 'id', table_2: 'id, name' }); } }
Dexie.jsは table_1: 'id'
のように簡単にIndexedDBのテーブルを生成することができます。この文字列はカンマ区切りで、1つ目がプライマリーキーのプロパティ名、2つめ以降がインデックスのプロパティ名となります。 table_1: 'id'
は「テーブル名 table_1
をプライマリーキー id
で作成する」、 table_2: 'id, name'
は「テーブル名 table_2
をプライマリーキー id
で作成し、 name
プロパティでインデックスを生成する」ことを意味します。
後述しますが、Dexie.jsではテーブルの操作を Dexie.Table
経由で行います。このSampleDatabaseクラスでは table_1
table_2
プロパティが使用されます。
データベース作成(Dexieインスタンス)
次にDexieをインスタンス化してデータベースを作成します。これは普通のJavaScript操作と何ら変わりはありません。
const db: Dexie = new Dexie('sample-database'); db.version(1).stores({ table_1: 'id', table_2: 'id, name' }); const table1 = db.table('table_1') as Dexie.Table<SampleEntity, number>;
とてもシンプルですが、1つ欠点があります。先にクラスで定義したときとはことなり、事前に型定義を行ったプロパティが存在しないため、テーブルアクセス時に毎回キャストが発生することです。そこで解決策として、Dexieの型を別に定義し、インターフェースを通じて操作します。
/** * Dexieのプロパティ名と型を持つ型を作成 */ export type DexieDatabase = {[P in keyof Dexie]: Dexie[P]}; /** * SampleDatabaseのテーブルプロパティだけ作成 */ export interface SampleDatabase extends DexieDatabase { table_1: Dexie.Table<SampleEntity, number>; table_2: Dexie.Table<SampleEntity, number>; } const db = new Dexie('sample-database') as SampleDatabase; db.version(1).stores({ table_1: 'id', table_2: 'id, name' }); db.table_1;
結局は一度キャストが必要になるのですが、テーブル単位では無くデータベース単位でのキャストになるため個人的にはこちらの方がお勧めです。
Dexie.jsのスキーマ定義
IndexedDBにはプライマリーキーのAutoIncrement機能やユニークインデックスの機能があり、Dexie.jsはこれらをサポートしています。
記号 | 意味 |
---|---|
++ | AutoIncrementプライマリーキー。 |
& | ユニークインデックス。 |
* | マルチエントリーインデックス(配列の要素をインデックスにする)。 MultiEntry Index |
[A+B] | 複合インデックス。 Compound Index |
{ friends: '++id,name,shoeSize', // AutoIncrementなプライマリーキー pets: 'id, name, kind', // AutoIncrementでないプライマリーキー cars: '++, name', // AutoIncrementなプライマリーキーだがオブジェクト外。 enemies: ',name,*weaknesses', // AutoIncrementでないプライマリーキーかつオブジェクト外。 // 'weaknesses' は配列の要素をインデックスにする。 users: 'meta.ssn, addr.city', // ネストされたオブジェクトのプロパティ指定。 people: '[name+ssn], &ssn' // 複合プライマリーキーかつ 'ssn' はユニークなインデックス。 }
オブジェクトを保存する
Dexie.jsでオブジェクトを保存するには add
または bulkAdd
を使用します。前者が1つのオブジェクトだけ、後者が複数のオブジェクトを一気に保存するのに使用します。
const db = new Dexie('sample-database') as SampleDatabase; db.version(1).stores({ table_1: 'id, name' }); await db.table_1.add({ id: 1, name: 'hoge', age: 10 }); await db.table_1.bulkAdd([ { id: 2, name: 'hoge', age: 11 }, { id: 3, name: 'fuga', age: 12 }, { id: 4, name: 'hoge', age: 13 }, { id: 5, name: null, age: 14 }, { id: 6, name: null, age: 15 }, ]);
このコードを実行するとIndexedDBに6つのオブジェクトが保存されます。ちなみにIndexedDBのデータベースやテーブル、保存されているオブジェクトはChromeであれば開発者ツールのApplicationタブから確認することができます。
オブジェクトを更新する
Dexie.jsでオブジェクトを更新するには put
または bulkPut
を使用します。このメソッドはキーが同一のオブジェクトが存在すれば更新、存在していなければ追加を行います。
const db = new Dexie('sample-database') as SampleDatabase; db.version(1).stores({ table_1: 'id, name' }); await db.table_1.put({ id: 1, name: 'hoge', age: 10 }); await db.table_1.bulkPut([ { id: 2, name: 'hoge', age: 11 }, { id: 3, name: 'fuga', age: 12 }, { id: 4, name: 'hoge', age: 13 }, { id: 5, name: null, age: 14 }, { id: 6, name: null, age: 15 }, ]);
オブジェクトを削除する
Dexie.jsでオブジェクトを削除するには delete
またはbulkDelete
を使用します。引数にはオブジェクトでは無くプライマリーキーの値を渡します。
const db = new Dexie('sample-database') as SampleDatabase; db.version(1).stores({ table_1: 'id, name' }); await db.table_1.delete(1); await db.table_1.bulkDelete([2, 3, 4, 5, 6]);
オブジェクトを全て取得する
Dexie.jsでオブジェクトを全件取得するには toArray
を使用します。
const db = new Dexie('sample-database') as SampleDatabase; db.version(1).stores({ table_1: 'id, name' }); await db.table_1.put({ id: 1, name: 'hoge', age: 10 }); await db.table_1.bulkPut([ { id: 2, name: 'hoge', age: 11 }, { id: 3, name: 'fuga', age: 12 }, { id: 4, name: 'hoge', age: 13 }, { id: 5, name: null, age: 14 }, { id: 6, name: null, age: 15 }, ]); const data = await db.table_1.toArray(); console.log('data: ', data);
オブジェクトを検索する(プライマリーキー)
Dexie.jsでプライマリーキーを使用して特定のオブジェクトを取得するには get
を使用します。
const db = new Dexie('sample-database') as SampleDatabase; db.version(1).stores({ table_1: 'id, name' }); await db.table_1.put({ id: 1, name: 'hoge', age: 10 }); await db.table_1.bulkPut([ { id: 2, name: 'hoge', age: 11 }, { id: 3, name: 'fuga', age: 12 }, { id: 4, name: 'hoge', age: 13 }, { id: 5, name: null, age: 14 }, { id: 6, name: null, age: 15 }, ]); const data = await db.table_1.get(1); console.log('data: ', data);
オブジェクトを検索する(インデックス)
Dexie.jsで特定のインデックスを使用してオブジェクトを検索するには where
を使用します。
const db = new Dexie('sample-database') as SampleDatabase; db.version(1).stores({ table_1: 'id, name, age, [name+age]' }); await db.table_1.put({ id: 1, name: 'hoge', age: 10 }); await db.table_1.bulkPut([ { id: 2, name: 'hoge', age: 11 }, { id: 3, name: 'fuga', age: 12 }, { id: 4, name: 'hoge', age: 13 }, { id: 5, name: null, age: 14 }, { id: 6, name: null, age: 15 }, ]); console.log('nameがhogeに等しい:\n', await db.table_1.where('name').equals('hoge').toArray()); console.log('nameがhogeに等しいまたはageが11に等しい:\n', await db.table_1 .where('name').equals('hoge') .or('age').equals(15) .toArray()); console.log('ageが12より大きい;\n', await db.table_1.where('age').above(12).toArray());
Whereで使用出来るAPI
テーブルのWhereの後に使用出来るものの一覧です( WhereClause )。微妙な使いづらさは元々のIndexedDB自体があまり使い勝手が良くないことに由来します。
API | 説明 |
---|---|
above | 数値比較で、指定した値よりも大きい。 |
aboveOrEqual | 数値比較で、指定した値と同じか大きい。 |
anyOf | 配列を渡し、配列の要素のいずれかと等しい。 |
anyOfIgnoreCase | 文字列の配列を渡し、配列の要素のいずれかと大文字小文字を区別せず等しい。 |
below | 数値比較で、指定した値よりも小さい。 |
belowOrEqual | 数値比較で、指定した値と同じか小さい。 |
between | 数値比較で、指定範囲。 |
equals | 指定の値と等しい。 |
equalsIgnoreCase | 文字列比較で、指定の値と大文字小文字を区別せず等しい。 |
inAnyRange | betweenを複数条件設定する。 |
noneOf | 配列を渡し、配列要素のいずれとも等しくない。 |
notEqual | 指定の値と等しくない。 |
startsWith | 文字列が指定の文字列を先頭に含む。 |
startsWithIgnoreCase | 文字列が指定の文字列を大文字小文字区別せず先頭に含む。 |
startsWithAnyOf | 配列を渡し、文字列が配列の要素のいずれかを先頭に含む。 |
startsWithAnyOfIgnoreCase | 配列を渡し、文字列が配列の要素のいずれかを大文字小文字区別せず先頭に含む。 |