オフラインWebアプリケーションへの道　Indexed databaseをSQL風に書けるlovefieldを使ってみる



JavaScript Advent Calendar 2015　4日目の記事です。フロントエンド人ではないですがフロントエンドが好きなので参加させていただきます。
グダグダと前置きが長いのでlovefieldにのみ興味がある人は最初の方を飛ばして「[Indexed databaseをSQL風に書けるlovefieldを使ってみる](http://qiita.com/ryo-ma/items/33386ec4b4c3b0261ce3#indexed-database%E3%82%92sql%E9%A2%A8%E3%81%AB%E6%9B%B8%E3%81%91%E3%82%8Blovefield%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B "jump")」を見てください。


#背景
iPhoneやAndroid端末が普及している現代ではWebページのスマートフォン対応が盛んに行われております。スマートフォン対応のWebページを作成するときはスマートフォンだからこそ意識しなければならない問題点がいくつかあると思います。そのうちの一つとして、スマートフォンは移動しながら使用するためPCと比べてネットワークが不安定です。そのためWebページが表示されないなどの問題が発生します。トンネルなどを通過する際によく発生する問題かと思います。
その問題に対応する策としてネットワークにつながった際に必要なリソース（データ、htmlファイル等）を取得しローカル（ブラウザ）に保存しておくという考え方があります。リソースをローカルに保存しておくことにより、ネットワークに接続していない状況でもWebアプリケーションとして正常に振る舞うことができます。この考え方がオフラインWebアプリケーションと呼ばれるものです。オフラインWebアプリケーションを実現するためのAPIがHTML5にいくつか実装されています。

##バックグラウンドスクリプト
####ServiceWorker  
Webページとは別にブラウザのバックグラウンドでスクリプトを動作させることができます。
リソースをオフライン状態でも返却できるようになります。

##ファイルシステム
####FileSystem API
ローカル環境にサンドボックス化したファイルシステムを作成して操作することができます。


##データストレージ系
###Indexed Database
KeyValue形式でローカル（ブラウザ）にデータを保存し、インデックス機能やトランザクション機能も備えています。




これ以外にもいくつかのAPIがHTML5に実装されています。
今回はこれらのAPIの中でローカル（ブラウザ）にデータを保存するデータストレージ系のAPIに注目しました。

#小規模データ向けのwebStorage
HTML5の代表機能としてよく取り上げられる **webStorage**という技術があります。webStorageはKeyValue形式でデータをローカル（ブラウザ）に保存できる機能です。似たものでCookieが存在しますので以下に比較を示してみました。

| 　| webStorage | Cookie |
|:-----------|:------------:|:------------:|
| 有効期限       |        なし |     あり     |
| 容量     |   5MB   |    4KB    |
| データ送信      |    必要なし    |    　リクエストごとに必要     |
webStorageはCookieに比べてとても魅力的な仕様であることがわかります。また、利用方法もともてシンプルでnavigator.localStorageにアクセスするれば使うことができます。

```javascript
localStorage.setItem('メリー', 'クリスマス')
console.log(localStorage.getItem('メリー')) 
// - > クリスマス
```
しかし、webStorageにはいくつかの問題がありました。

###1.読み込みが同期的に行われる
webStorageでのデータ読み込みは同期的に行われるので、文章などのレンダリングをブロックしてしまいます。
そのためページのレンダリング速度が遅くなりユーザに負担をかける可能性があります。

###2.webStorageを使用した際にファイルのI/O処理が含まれる
webStorageを利用するとハードディスクに書き込みが行われるためシステムによってはインデックス作成時やウイルススキャンで時間がかかる可能性があります。

###3.データの初回読み込み時にブラウザがメモリ上にロードする
早く動いているように見せるために、データの初回読み込み時にブラウザがメモリ上にロードします。なのでタブを開けば開くほど、メモリ上にデータがロードされるので多くのメモリを食ってしまう可能性があります。

###4.データが永続的なためブラウザを開くたびにデータがロードされる
データを消してあげない限り永続的にデータがブラウザに残ります。そのため、サービスやアプリが消滅してもブラウザを開くたびにデータがロードされます。


このようにパフォーマンス面でかなりの問題を抱えています。しかし、小規模データを扱う分にはそこまで大きな問題にはならないかもしれませんがオフラインWebアプリケーションを実現するには少し厳しいと感じます。


#Indexed database
webStorageと同時期に話題となったIndexed databaseというものがあります。webStorageと同じくKeyValue形式でローカルにデータを保存することができます。

###webStorageとの比較

webStorageとの比較をしてみます。

| 　| Indexed database | webStorage |
|:-----------|:------------:|:------------:|
| 処理       |        非同期 |     同期     |
| 容量     |   ブラウザごとに異なるが大容量対応   |    5MB    |
| バイナリ保存      |    可    |    不可     |
| トランザクション対応|　対応| 非対応|
| インデックス対応|　対応| 非対応|
| Service Worker上での動作|　対応| 非対応|

性能、機能の面でIndexed databaseはかなり優秀なようです。


###問題点
* APIが複雑なため利用するするのが難しい
* RDBMSではないのでSQLのような操作はできない
* なんらかのプロパティから検索するためには事前にインデックスを設定する必要がある
* ソートを行うにもインデックスで設定する必要がある

調査してみた結果、性能や機能は素晴らしいようですが少し扱いにくいAPIのように感じます。機能が多い分コード量もwebStorageに比べてかなり多いようです。
しかし、Indexed databseはService Workerの登場によりオフラインWebアプケーションの実現の重要な要素として有望視されています。

#Indexed databaseをSQL風に書けるlovefieldを使ってみる
Indexed databaseをRDBMSのように扱えるlovefieldというgoogle製のライブラリがあります。
https://github.com/google/lovefield
実際にlovefieldを扱ってみたいと思います。

##導入

githubにライブラリがあるのでクローンしてきます。

```bash
$ git clone https://github.com/google/lovefield.git
```

ライブラリ本体は以下のディレクトリにあります。

```bash
lovefield/dist/lovefield.min.js
```

##動作検証

###データベース定義
まずデータベース名とデータベースのバージョンを設定します。

```js
var schemaBuilder = lf.schema.create('Christmas', 1); // create(DB名, DBバージョン)
```

###テーブル定義

テーブル名およびカラムを定義します。テーブルの定義はBuilderパターンを使って構築します。
カラムのデータタイプはlf.Typeに定義されています。
同時に主キーとインデックスの設定を行います。
1.先ほど作成したschemaBuilderからcreateTable()を呼び出しテーブル名を定義します。
2.メソッドチェインでaddColumn()にカラム名とデータタイプを指定します。
3.addPrimary()で主キーを設定します。
4.addIndex()でインデックスを指定します。

```js
schemaBuilder.createTable('Gift'). // createTable(テーブル名)
        addColumn('id', lf.Type.INTEGER). // addColumn(カラム名, データタイプ)
        addColumn('name', lf.Type.STRING).
        addColumn('price', lf.Type.INTEGER).
        addColumn('createTime', lf.Type.DATE_TIME).
        addColumn('soldOut', lf.Type.BOOLEAN).
        addPrimaryKey(['id']). // addPrimaryKey(主キー)
        addIndex('idxCreateTime', ['createTime'], false, lf.Order.DESC); // addIndex(インデックス名, カラム名, ユニークにかどうか, 順序)
```

###データ挿入
データを定義してテーブルに挿入します。
1.connect()を使ってBuilderの終端処理をおこない、データベースへ接続をします。
2.then()の引数にコールバックを設定し、接続をしたデータベースを使うことができます。
3.createRow()で呼び出しもとのテーブルに挿入したいデータを定義します。
4.insertOrReplace()でインサート処理をします。
5.into()で挿入するテーブルを指定します。
6.values()で挿入するデータを指定します。
7.exec()で実行してreturnします。

```js
var christmasDB;
var gift;
schemaBuilder.connect().then(function(db) { // then(dbを使うためのコールバック関数)
    christmasDB = db;
    gift = db.getSchema().table('Gift'); // table(テーブル名)
    var row = gift.createRow({
      'id': 1,
      'name': 'MacBookAir',
      'price' : 104800,
      'createTime': new Date(),
      'soldOut': false
    }); // createRow(Hash形式で挿入するデータ)
    return db.insertOrReplace().into(gift).values([row]).exec(); // into(テーブル)　value(挿入するデータの配列)
});
```

###問い合わせ
テーブルに問い合わせてみます。
一つ前で記述したコードの終端にあるthen()にメソッドチェインでさらに続けてthen()を呼び出します。
1.select()で列を選択します。
2.from()でテーブル名を選択します。
3.where()で条件を指定します。条件は以下の形式です。
テーブル.カラム.eq(期待する値)でカラムから期待する値をeq()に指定します。
sql文に置き換えるとこのようになります

```sql
SELECT * FROM gift WHERE soldOut = false;
```
4.exec()で実行してreturnします。


```js
var christmasDB;
var gift;
schemaBuilder.connect().then(function(db) { // then(dbを使うためのコールバック関数)
    christmasDB = db;
    gift = db.getSchema().table('Gift'); // table(テーブル名)
    var row = gift.createRow({
      'id': 1,
      'name': 'MacBookAir',
      'price' : 104800,
      'createTime': new Date(),
      'soldOut': false
    }); // createRow(Hash形式で挿入するデータ)
    return db.insertOrReplace().into(gift).values([row]).exec(); // into(テーブル)　value(挿入するデータの配列)
}).then(function() { // 追加
    return christmasDB.select().from(gift).where(gift.soldOut.eq(false)).exec(); // select(カラム名) from(テーブル名) where(条件) eq(期待する値)
});
```

###問い合わせ結果取得
問い合わせたデータの結果を取得して表示します。
再び、一つ前のコードの終端にメソッドチェインでthen()を呼び出します。
一つ前のthen()で問い合わせた結果は次のthen()で登録したコールバックの引数で取得することができます。

```js
var christmasDB;
var gift;
schemaBuilder.connect().then(function(db) { // then(dbを使うためのコールバック関数)
    christmasDB = db;
    gift = db.getSchema().table('Gift'); // table(テーブル名)
    var row = gift.createRow({
      'id': 1,
      'name': 'MacBookAir',
      'price' : 104800,
      'createTime': new Date(),
      'soldOut': false
    }); // createRow(Hash形式で挿入するデータ)
    return db.insertOrReplace().into(gift).values([row]).exec(); // into(テーブル)　value(挿入するデータの配列)
}).then(function() {
    return christmasDB.select().from(gift).where(gift.soldOut.eq(false)).exec(); // select(カラム名) from(テーブル名) where(条件) eq(期待する値)
}).then(function(results) { //追加
    results.forEach(function(row) {
        console.log(row['name'], row['price']);
    });// -> MacBookAir 104800
});
```

###lovefield記述の流れ
流れとしては以下のようイメージです。
1.データベースを定義します。
2.テーブルを定義します。
3.connect()を使ってデータベースに接続します。
4.一つ前のthen()に対してメソッドチェインでthen()を呼び出します。（最初はconnect()に対してメソッドチェイン）
5.then()の引数にコールバック関数を渡して中に処理を書いていきます。(一つ前のthen()で問い合わせを行った場合はコールバックの引数に結果が返ってきます)
6.なんらかのSQL的（挿入、問い合わせ）な命令をします。
7.終端処理としてexec()を呼び出します。
8.exec()の結果をreturnします。
9.あとは4~8までの繰り返しです。


###全体のコード

```js
var schemaBuilder = lf.schema.create('Christmas', 1); // create(DB名, DBバージョン)

schemaBuilder.createTable('Gift'). // createTable(テーブル名)
        addColumn('id', lf.Type.INTEGER). // addColumn(カラム名, データタイプ)
        addColumn('name', lf.Type.STRING).
        addColumn('price', lf.Type.INTEGER).
        addColumn('createTime', lf.Type.DATE_TIME).
        addColumn('soldOut', lf.Type.BOOLEAN).
        addPrimaryKey(['id']). // addPrimaryKey(主キー)
        addIndex('idxCreateTime', ['createTime'], false, lf.Order.DESC); // addIndex(インデックス名, カラム名, ユニークにかどうか, 順序)

var christmasDB;
var gift;
schemaBuilder.connect().then(function(db) { // then(dbを使うためのコールバック関数)
    christmasDB = db;
    gift = db.getSchema().table('Gift'); // table(テーブル名)
    var row = gift.createRow({
      'id': 1,
      'name': 'MacBookAir',
      'price' : 104800,
      'createTime': new Date(),
      'soldOut': false
    }); // createRow(Hash形式で挿入するデータ)
    return db.insertOrReplace().into(gift).values([row]).exec(); // into(テーブル)　value(挿入するデータの配列)
}).then(function() {
    return christmasDB.select().from(gift).where(gift.soldOut.eq(false)).exec();// select(カラム名) from(テーブル名) where(条件) eq(期待する値)
}).then(function(results) { //追加
    results.forEach(function(row) {
        console.log(row['name'], row['price']);
    });// -> MacBookAir 104800
});
```


#まとめ
今回はオフラインWebアプリケーションを実現するストレージ系のAPIに注目してみました。どのAPIも現状では発展途上で様々な問題を抱えています。オフラインWebアプリケーションを実現する上で一番有望視されているIndexed databseのAPIも扱いにくいという問題がありました。それを問題を補うlovefieldというライブラリがあります。そのライブラリを使ってみた結果、コード量も少なくSQL的に記述できるためかなり扱いやすいものと感じられました。特にメソッドチェインでテーブル定義、問い合わせができるのはとても直感的で素晴らしいと思います。

自由度との兼ね合いだとは思いますが標準のAPIにもこのぐらい直感的なものを導入すればいいのに・・・


#参考文献
There is no simple solution for local storage
https://hacks.mozilla.org/2012/03/there-is-no-simple-solution-for-local-storage/


オフラインWebアプリの再到来で今、再び注目されるAPIの本命　ーJavaScript SQL-like database
http://www.slideshare.net/yoshikawa_t/offline-webapps

WebアプリケーションなのにOffline Web Applicationって？
http://albatrosary.hateblo.jp/entry/2014/02/24/081629

モダンブラウザのストレージ容量まとめ
http://www.html5rocks.com/ja/tutorials/offline/quota-research/

Webアプリもオフライン実行？ Indexed Databaseを使いこなそう
http://www.atmarkit.co.jp/ait/articles/1202/14/news138.html