lovefield で IndexedDB を SQL風に操作する方法

lovefield で IndexedDB を SQL風に操作する方法

lovefield

lovefield は、Webブラウザ上にデータを永続化するためのライブラリです。Google製のライブラリで、Key-Valueのペアでデータを管理する Indexed Database を SQL風に操作することが可能です。

Firebaseと連携させたり、セッション内でのみMemory上にデータを保持することも可能です。

Lovefieldは、純粋なJavaScriptで書かれたリレーショナルデータベースです。SQLと似た構文を提供し、クロスブラウザ(Chrome 37+、Firefox 31+、IE 11+、Edge、Safari 10+)で動作します。

Indexed Database と Web SQL Database, LocalStorage

lovefield は、データの保存に Indexed DB を内部的に使用しています。

ブラウザ上にデータを永続化するための方法はいくつかあり、その一つが Indexed DB です。そのほかにも Web SQL Database や LocalStorage といったデータを保持するための仕組みがブラウザには搭載されています。それぞれの特徴は次の通りです。

項目 メリット デメリット
Indexed DB バイナリデータなど様々なデータを保存できる。
非同期で操作できる。
高パフォーマンス。
トランザクション対応。
APIが複雑で扱いつらい。
SQLで操作できない。
Web SQL DB SQLで操作できる。(SQliteが扱える?)
比較的多くのブラウザに搭載されている。
標準化の仕様から外れている。(いつまで維持されるか不透明)
LocalStorage シンプルなAPI。 すべて同期処理となる。
大容量対応不可(5MBまで)

LocalStorage はKey-Valueペアでデータを保持します。非常にシンプルなAPIで構成されており、その処理は同期処理として動作します。したがって実装も簡単ですが、大規模にデータを扱うのは向いていません。それに、同期処理の為レンダリングをブロックするなどの問題もあります。

Web SQL Databaseですが、注意点として、すでに標準化に向けた動きは終了しており、今後標準化されすべてのブラウザで動くようになることはありません。また今動いているブラウザでも今後サポートされなくなる可能性があります。そこで代替案として提案されているのが Indexed DB です。

Indexed Database とは

Indexed Database は、データをブラウザ上に保存するための仕組みで、Key-Valueのペアとしてデータを保存します。基本的な概念については上記URLが参考になります。

大容量に対応し、高パフォーマンスでトランザクションにも対応している優れものの Indexed DB ですが、提供されるAPIが非常に複雑です。MDNにも次のように注記されています。

注記: IndexedDB API は強力ですが、シンプルな事例でもとても複雑に見えるかもしれません。シンプルな API が好ましいのでしたら、IndexedDB をより開発者フレンドリーに扱える localForage や dexie.js 、ZangoDB、JsStoreなどのライブラリを検討してください。

つまり使いにくかったらラッパーライブラリがあるからそれを使うといいよとのことです。

lovefield も内部で Indexed DB を使用しています。シンプルで柔軟性のあるAPIでIndexed DB を操作することができます。

lovefield の特徴

lovefield は、Indexed DB に対してRDBのようなSQL風の操作を提供します。テーブル構造を定義し、各フィールドの型情報やNULL許容の設定、外部キーやインデックスの設定が可能です。もちろんテーブル同士の結合(JOIN)や集約(GROUP BY)、ソート(ORDER BY)なども可能です。

また、すべての非同期処理は Promise を返すようになっているので、コールバックによる書きにくさ・読みにくさが軽減されます。

lovefield の使い方

上記URLの「Quick Start Guide」をベースに使い方を見ていきます。

本体の読み込み

lovefield は、CDNで公開されているファイルを読み込むだけで利用可能です。

<script src="https://cdnjs.cloudflare.com/ajax/libs/lovefield/2.1.12/lovefield.min.js"></script>

本体ファイルを読み込むことでグローバル変数 lf が使えるようになります。

データベースの作成

まずはデータベースを作成します。

var schemaBuilder = lf.schema.create('todo-db', 1);
// CREATE DATABASE NOT EXISTS todo

引数にDB名とバージョンを指定します。lovefield は Indexed DB 同様DBをバージョン管理し、バージョンアップの際のイベントを定義できます。ここでは最初のバージョンとして1を指定しています。

DBがすでに作成済の場合何も実行されません。

テーブル定義

次にテーブル(スキーマ)の定義を行います。createTable 関数でテーブルを作成し、チェーンしながら各フィールドの型情報やテーブルのキーに当たる情報等を定義していきます。

例えば以下のように Item テーブルを定義します。この場合コメントに書いたようなSQLと同等の定義を行います。

schemaBuilder.createTable('Item').
    addColumn('id', lf.Type.INTEGER).
    addColumn('description', lf.Type.STRING).
    addColumn('deadline', lf.Type.DATE_TIME).
    addColumn('done', lf.Type.BOOLEAN).
    addPrimaryKey(['id']).
    addIndex('idxDeadline', ['deadline'], false, lf.Order.DESC);
// CREATE TABLE IF NOT EXISTS Item (
//   id AS INTEGER,
//   description AS INTEGER,
//   deadline as DATE_TIME,
//   done as BOOLEAN,
//   PRIMARY KEY ON ('id')
// );
// ALTER TABLE Item ADD INDEX idxDeadLine(Item.deadline DESC);

定義できる内容

型定義ファイルを見ると、createTable 関数は TableBuilder という型を返すのですが、それが持つメソッドは次のように定義されていることがわかります。

主キーやNULL許容は配列で列名を渡すことで複数列に対して設定できます。

interface TableBuilder {
    // 列の型定義
    addColumn(name: string, type: Type): TableBuilder;
    // 外部キーの定義
    addForeignKey(name: string, spec: RawForeignKeySpec): TableBuilder;
    // インデックスの定義
    addIndex(
        name: string, columns: string[]|IndexedColumn[],
        unique?: boolean, order?: Order): TableBuilder;
    // NULL許容の定義
    addNullable(columns: string[]): TableBuilder;
    // 主キーの定義
    addPrimaryKey(
        columns: string[]|IndexedColumn[],
        autoInc?: boolean): TableBuilder;
    // ユニークキーの定義
    addUnique(name: string, columns: string[]): TableBuilder;
}

DBへの接続

ここまででDBとテーブルの定義を行いました。定義した schemaBuilder でDBに接続を行うには connect 関数を実行します。

schemaBuilder でのDB定義自体はすべて同期的に行われますが、まだ Indexed DB には反映されません。DBへの接続を行う段階で定義に沿ったDBが作成されることになります。

// DBへの接続処理(非同期)
schemaBuilder.connect().then(function(db) {
    // 接続に成功したときの処理を書く
    // DBが存在しない場合はここですでに作成されている
});

connect 関数は非同期でDBへの接続を行い、接続に成功した場合そのDBを Promise で返します。

DBの操作

DBへの接続に成功したら、Promiseで取得した接続済DBに対して各種操作を行うことができます。

var todoDb, item; // DBとスキーマを退避する用の変数
schemaBuilder.connect().then(function(db) {
    // 接続に成功したDB
    todoDb = db;

    // Item テーブルのスキーマ情報取得(同期)
    item = db.getSchema().table('Item');

    // lovefieldでは生のObjectを直接登録はできない
    // createRow関数でRowオブジェクトを生成してこれを登録する
    var row = item.createRow({
        'id': 1,
        'description': 'Get a cup of coffee',
        'deadline': new Date(),
        'done': false
    });

    // INSERT OR REPLACE INTO Item VALUES row;
    var query = db.insertOrReplace().into(item).values([row]);
    console.log(query.toSql()); // 実行されるSQLを確認できる

    // クエリ実行(非同期処理でPromiseを返す)
    return query.exec();
}).then(function() {
    // Insert完了後の処理
    // 暗黙のトランザクションコミット後に呼ばれる。

    // SELECT * FROM Item WHERE Item.done = false;
    // SELECT文実行結果をPromiseで返す
    return todoDb.select().from(item).where(item.done.eq(false)).exec();
}).then(function(results) {
    // SELECTで取得した行データをループ
    results.forEach(function(row) {
        // 列名を指定してフィールドの値を参照できる
        console.log(row['description'], 'before', row['deadline']);
    });
});

実行結果はブラウザの開発者ツールから確認できます。Chromeだと開発者ツールの Application タブに Storage で関する情報が確認できます。Indexed DB の項目を開くと作成されたデータベースに登録されたデータが確認できます。

上記サンプルで実行している内容は次の通りです。

  1. DBに接続
  2. ItemテーブルにInsertOrReplaceでデータ更新(コミット)
  3. Selectで更新したデータ取得
  4. 取得したデータをコンソールに出力

1~3 は非同期処理でPromiseを返すので、メソッドチェーンで処理をつなげています。

query の作成のためのコマンドが多数用意されており、これらを組み合わせることでSQLのように様々なデータ操作を行うことができます。各種コマンドはこちらの仕様を確認してください。

作成した query は toSql 関数を使うことで、SQL文として取得することができます。想定通りクエリが構築できているか確認するのに使えます。

現在のセッションでのみ有効なデータを管理する

lovefieldでは、現在のセッションでのみ永続化され、セッション終了後には破棄されるデータも管理することができます。connect 関数でDBに接続する際に、ストレージの種類を指定できます。デフォルトだと Indexed DB になりますが、これを MEMORY とすることでメモリ上にデータを保持することができるようになります。

// 
schemaBuilder.connect({ storeType: lf.schema.DataStoreType.MEMORY }).then(function(db) {
    // ...
});

上記ページで選択可能なストレージの種類が確認できます。WEB SQL も選択できますが、削除予定なので使うなとのことです。また、Firebaseと連携させることもできるみたいです。(未確認)

雑感

シンプルなAPIで Indexed DB を制御でき、SQL風のクエリをかけるのでとても便利です。Promiseベースの非同期処理で書けるので、async/await が使える環境であればもっと then によるメソッドチェーンを使わず同期的にかけるので便利でした。

JOINなどを使った複雑なクエリやFirebaseとの連携も近いうちに試してみようと思います。

以上。

Javascriptカテゴリの最新記事