Corredor

ウェブ、プログラミングの勉強メモ。

cordova-sqlite-storage プラグインに関する Tips

SQLite入門 すぐに使える軽快・軽量データベース・エンジン

SQLite入門 すぐに使える軽快・軽量データベース・エンジン

前回、cordova-sqlite-storage プラグインを使って、Cordova iOS アプリ内に SQLite 製のローカル DB を作成することができた。

今回はこのプラグインに関する細かなヒント集。

このプラグインを使って動作するサンプルプロジェクトを以下に置いているので、feat/sqliteStorage ブランチをクローンして cordova prepare コマンドで環境を復元して試してみてもらいたい。

window.openDatabase() を使ってブラウザでも動作させる

簡易的な動作確認のため、cordova serve コマンドを使って Mac のブラウザ上でアプリを開くことがあるだろう。しかし、ブラウザ上では cordova-sqlite-storage プラグインが動作しない。そこで、HTML5 の API である window.openDatabase() を使って、ブラウザでもローカル DB を動作させることにする。

まず、ブラウザでの表示確認のため、ターゲットプラットフォームに browser を追加する。

$ cordova platform add browser --save

そして cordova serve browser コマンドを叩くと、ブラウザ表示向けにプラグインをモック化した cordova.jscordova_plugins.js が生成され、ブラウザ表示がしやすくなる。実際にブラウザで表示してみると、window.sqlitePlugin.openDatabase() の部分で以下のような内容がコンソール出力されている。

Error: exec proxy not found for :: SQLitePlugin :: open
OPEN database: sample.db FAILED, aborting any pending transactions
Could not open database

これは、cordova-sqlite-storage がネイティブ機能と連携する部分がネックとなって処理が続行できないよ、といっているエラーなので、window.sqlitePlugin.openDatabase() メソッドでは DB インスタンスが生成できていない。

しかし、実は window.sqlitePlugin.openDatabase() が返却する DB インスタンスが持つ API は WebSQL の API に即しているため、Chrome ブラウザなどで実行する場合は、HTML5 の window.openDatabase() を使って DB インスタンスを返し、Chrome ブラウザ内の WebSQL を使用してやることで、後続処理がブラウザで動かせるようになるのだ。

Chrome ブラウザの WebSQL も、実体は SQLite なので、window.openDatabase() を叩くと裏では sample.db ファイルが生成されている。また、開発者ツールを開き、「Application」タブから「Clear storage」を選び、最下部の「Clear site data」を押せばローカル DB を削除できるので、動作検証にももう少し使えるだろう。

前回の記事で少し触れたが、SQLite プラグインが用意している db.executeSql() など一部のメソッドは、WebSQL API で生成した DB インスタンスでは対応していない場合がある。各メソッドの対応状況を調べてモック化したりするのは面倒なので、あくまで Chrome ブラウザでちょっと確認したい時、ぐらいに留めておき、本来はちゃんと iOS シミュレータや実機で検証するようにしよう。

ちなみに、window.openDatabase() が使えるのは Chrome と Safari ぐらいで、Firefox は対応していない。

echoTest()selfTest() でプラグインの動作チェックを行う

window.sqlitePlugin.echoTest() というメソッドがあり、これで cordova-sqlite-storage プラグインがネイティブ機能にアクセスできそうかを検証してくれる。OK の場合、NG の場合に行うコールバック関数を引数で渡せる。

また、前回の記事でも紹介した、window.sqlitePlugin.selfTest() というメソッドもある。こちらは DB オープンや CRUD 操作を実際に行い、それが可能な環境かを検証してくれるものだ。ターゲットプラットフォームに browser を選択し、Chrome ブラウザで実行した場合は、echoTest()selfTest() もエラーコールバックが実行されるので、これで DB オープン処理を分けても良いだろう。

cordova serve ios と iOS 向けにビルドしたものを Chrome ブラウザなどで見ようとすると、実際にネイティブ機能にアクセスしようとしてその場で処理が止まってしまうので、ブラウザで確認する時は cordova serve browser を使うことを忘れずに。

.db ファイルの在り処

iOS シミュレータを起動して実際に .db ファイルを生成した場合は、ターミナルから以下の要領で実際のファイルを確認することができる。

# iOS シミュレータが保存されているディレクトリに移動する
$ cd ~/Library/Developer/CoreSimulator/Devices/

# 配下から「sample.db」という名前のファイルを探す
$ find . -name sample.db

# 以下のようなランダム文字列を含んだフォルダの中に生成されていることが分かる
# 最初の「AD0…/」というディレクトリは iOS シミュレータの種類ごとに作られるディレクトリ
# 途中の「RE2…/」というディレクトリはココで作ったサンプルアプリのディレクトリになっている
./AD036A35-4FD2-4680-9A3D-359CB823FF13/data/Containers/Data/Application/RE2E1C48-D94E-4658-B2E7-EF7A5CC69D3B/Library/LocalDatabase/sample.db

思ったように DB 操作ができていなさそうなときは、.db ファイルが正しく作られているか、操作された形跡があるか確認してみよう。

Cordova アプリ内に SQLite でローカル DB を構築できる cordova-sqlite-storage

SQLite入門 第2版

SQLite入門 第2版

前回の記事で検証したとおり、Cordova で iOS アプリを作る時、大規模なデータをクライアントサイドで管理したければ「WebSQL およびその内部で使用している SQLite 一択」という結論に至った。

今回は、Cordova アプリ内に SQLite でローカル DB を構築できる、cordova-sqlite-storage というプラグインを紹介する。

今回は cordova-sqlite-storage プラグインを導入し、iOS シミュレータ上で動作させるところまでやってみようと思う。ローカル DB にテーブルを作り、データを書き込み、それを取得する、といった一連の処理をしてみようと思う。

前提と動作サンプル

以下、Cordova アプリの雛形ができている前提で話を進めるので、まだの場合は以下の記事を参考に Cordova アプリのたたき台を作っておいて欲しい。

neos21.hatenablog.com

上記記事に従って Cordova プロジェクトを作成すると、以下のリポジトリの feat/installCordovaProject ブランチのようになるはずだ。

また、今回の記事で紹介するコードを含む、実際に動作する Cordova プロジェクトは、以下のリポジトリの feat/sqliteStorage ブランチで確認できる

cordova-sqlite-storage プラグインのインストール

ターミナルで Cordova プロジェクトに移動し、以下のコマンドを叩く。

$ cordova plugin add cordova-sqlite-storage --save

--save コマンドにより、この Cordova プロジェクトが cordova-sqlite-storage プラグインを使用している、という情報が config.xml に追記される。config.xml に以下のような情報が追記されているはずだ。

<plugin name="cordova-sqlite-storage" spec="~2.0.4" />

プラグインのインストール作業はコレだけ。

HTML と CSS の実装

今回は $ cordova create コマンドで作成した直後のアプリをベースにする。既に

  • ./www/index.html
  • ./www/js/index.js
  • ./www/css/index.css

という3つのファイルができていると思うので、これらを編集していく。

まず、./www/css/index.css は余計なスタイリングをしなくて良いので、中身を空にしてしまおう。

次に、./www/index.html<div class="app"> をまるっと除去して、以下のような HTML にしよう。

最終的には、「データ取得」ボタンを押すと、<ul id="results"></ul> の中にローカル DB から取得した情報を表示する、といった作りにしようと思う。

JavaScript の実装

いよいよ JavaScript の実装だ。./www/js/index.js を一度空にし、以下のコードをまるっと貼り付ける。

コメントや console.log() が多いので行数がかさんでいるが、本質的なコードはさほど多くない。

動作確認

解説は一旦抜きにして、動作確認をしてみよう。以下のコマンドでビルドと iOS シミュレータ起動を一気にやらせる。

$ cordova emulate ios

iPhone シミュレータが起動したら、Safari を開いて「開発」メニュー → 「Simulator」 → 「(アプリ名)」と進み、Web インスペクタのコンソールタブを開く。動作ログが出力されるので、ココを見ながらアプリを操作してみよう。

アプリ起動時に DB 接続はするようにしてあるので、「DB 接続処理」ボタンは押さなくても良い。「テーブル作成・データ投入」ボタンを押すと、画面には変化がないが、コンソールを見ると内部で SQLite にテーブルを作成し、データが登録されていることが分かる。

「データ取得」ボタンを押すと、SELECT 文でローカル DB からデータを取得し、画面に表示する。Cordova アプリ内に SQLite が正しく構築され、動作していることが確認できる。

「テーブル削除」ボタンを押すと、画面には変化がないが、作成したテーブルが削除されている。この直後に「データ取得」ボタンを押すと、エラーメッセージが画面に表示されるはずだ。

実装解説

それでは実装解説をしていく。

./www/js/index.js は、全体的には、app という1つのグローバル変数が全ての処理を持っており、初期処理の app.init() を最終行で実行しているだけである。以下、メソッドごとに説明をしていく。

init()

init() では、画面上のボタンを押した時に対応する処理を呼ぶようイベント登録しているのと、deviceready という Cordova が用意したイベントに登録している。

deviceready イベントはその名の通り、アプリが起動し、端末の準備ができた時に発火する。だいたい DOMContentLoadedonload の間ぐらいだ。ココで DB 接続する処理を呼んでいる。

openDb()

DB 接続をする openDb() では、window.sqlitePlugin.openDatabase() メソッドを叩いて、DB インスタンスを取得している。

this.db = window.sqlitePlugin.openDatabase({
  name: 'sample.db',
  location: 'default'
});

SQLite は .db ファイル1つでデータベース全体を表現する作りなので、このサンプルでは name プロパティに設定している、sample.db という DB ファイルを用意しようとしている。sample.db が未生成な場合は新たに生成し、既に生成済みであればそのファイルを読み込んで、DB インスタンスを返却している。

オプションの location: 'default' は、iOS で SQLite を使用する場合は指定しないと動かなかった。

window.sqlitePlugin.openDatabase() を囲んでいる window.sqlitePlugin.selfTest() などはこの次に紹介する。

_openWebSqlDb()

さて、先程の openDb() メソッドの本質は window.sqlitePlugin.openDatabase() メソッドによる DB ファイルの生成と DB インスタンスの取得であった。では、それ以外のところでは何をしているのか。

これは、cordova-sqlite-storage プラグインが動作しない場合に _openWebSqlDb() メソッドに飛ばし、HTML5 標準 API である window.openDatabase() を使って DB インスタンスを取得しようと試みている。いわば例外ハンドリングの一種だ。

まず cordova-sqlite-storage プラグインの存在チェックのため、if(!window.sqlitePlugin) という条件を入れている。プラグインがありそうであれば、次はプラグインが正常に動作しそうかどうか、window.sqlitePlugin.selfTest() というテストメソッドで検証する。このメソッドは DB インスタンスの生成や CRUD 操作を実際に行って、DB 操作が可能な状態かどうかを検証して OK or NG を返している。プラグインが正しく動作しなさそうであれば _openWebSqlDb() メソッドに飛ばしている。

で、_openWebSqlDb() メソッドでは window.openDatabase() が存在しているかの検証と、try catch で DB 生成処理が完了したかチェックしたりしている。window.openDatabase() の引数は、「.db ファイル名」「バージョン番号 (普段意識しないので固定値決め打ちで良い)」「スキーマ名」「容量 (バイト単位・大きめにとっておく)」の順。大体上記の設定そのままで良いはずだ。

このメソッドの使い道はというと、Chrome や Safari など、Mac 上で動作確認する時、SQLite プラグインが動作しない中の代替処理として使用する。この辺の話は別途紹介する。

create()

create() がテーブル作成とデータ登録を行っている処理。

冒頭で、念のため DB インスタンスが生成されていなければ生成処理を呼ぶようにしているが、まず問題ないだろう。

SQL 実行処理の書き方

cordova-sqlite-storage で DB 操作する時の基本構文は以下のとおり。

db.transaction(【SQL を実行する関数】, 【SQL 実行失敗時のコールバック関数】, 【SQL 実行成功時のコールバック関数】);

「コールバックって何」というと、「手前の処理が終わったら次に呼ぶ関数」ということ。この場合、第1引数の「SQL を実行する関数」内でもしエラーがあったら、「SQL 実行失敗時のコールバック関数」が実行され、「SQL 実行成功時のコールバック関数」は実行されない、という動きになる。

3つとも関数なので、別々に宣言しておいても良いが、大抵は中に直接関数を書くことになるだろう。

this.db.transaction(function(tx) {
  tx.executeSql('CREATE TABLE IF NOT EXISTS SampleTable (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)');
  tx.executeSql('REPLACE INTO SampleTable VALUES (?, ?, ?)', [1, 'ほげ', 18]);
  tx.executeSql('REPLACE INTO SampleTable VALUES (?, ?, ?)', [2, 'ぴよ', 20]);
}, function(error) {
  console.log('テーブル作成・データ投入 失敗 : ' + error.message);
}, function() {
  console.log('テーブル作成・データ投入 成功');
});

エラーハンドリングはした方が良いと思うが、特になければ、第2・第3引数にコールバック関数を渡さない、ということもできる。

後述する select() では、db.transaction() にはコールバック関数を設定せず、tx.executeSql() にコールバック関数を指定している。

this.db.transaction(function(tx) {
  tx.executeSql('SELECT * FROM SampleTable', [], function(tx, sqlResultSet) {
    console.log('データ取得 成功');
    // ココが成功時のコールバック関数
  }, function(tx, error) {
    console.log('データ取得 失敗 : ' + error.message);
    // ココが失敗時のコールバック関数
  });
});

もしくは、単一の SQL 文を実行するだけなら以下のようにも書ける。

db.executeSql('SELECT * FROM SampleTable', [], function(sqlResultSet) {
  console.log('成功' + sqlResultSet.rows.length);
}, function(error) {
  console.log('失敗' + error.message);
});

tx.executeSql() および db.executeSql() はコールバック関数を「成功時 → 失敗時」の順で記述するが、db.transaction() は「失敗時 → 成功時」の順で記述するため、間違えないよう注意。また、別途紹介する「Chrome ブラウザでの実行時」は、db.executeSql() という関数がなくエラーになるため、基本は db.transaction()tx.executeSql() を使う方式を取ると良いだろう。

俗にいう「コールバック地獄」(コールバック関数が入れ子になりすぎて何がなんだか分からなくなる状態) に陥りやすいので注意されたし。

create() で書いた SQL について

create() 内に書いた SQL は大きく2種類、CREATE TABLEREPLACE INTO だ。これらは SQLite の文法が使えるので、確認しておきたい。

まず CREATE TABLE から。

CREATE TABLE IF NOT EXISTS SampleTable (
  id INTEGER PRIMARY KEY,
  name TEXT,
  age INTEGER
)

何度実行しても大丈夫なように、IF NOT EXISTS を書いている。これにより、「テーブルがなければ作る、あれば何もしない」が可能になる。

SQLite には DATE 型がないので、カラムの型は INTEGERTEXT (VARCHAR 相当) が主になるだろう。PRIMARY KEY を指定したカラムが INTEGER 型の場合は、INSERT 時にオートインクリメントさせることができる。

続いて REPLACE 文。これは他の DB だと UPSERT とか MERGE と云われるものと同じで、要するに「PK などでバッティングするデータがあれば UPDATE 扱い、なければ INSERT 扱い」が可能になる。今回はプライマリキー指定をした id カラムまでパラメータ指定しているので、初回は INSERT、2回目以降は UPDATE がかかることになる。REPLACE の構文は INSERT と同じ。

tx.executeSql('CREATE TABLE IF NOT EXISTS SampleTable (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)');
tx.executeSql('REPLACE INTO SampleTable VALUES (?, ?, ?)', [1, 'ほげ', 18]);

SQL を書く時は、末尾にセミコロンは不要。前後や間にどれだけスペースを入れても無視してくれるので、SQL 文を適宜整形して記述しても問題ない。

// こんな風に整形して書いても OK
tx.executeSql(' CREATE TABLE IF NOT EXISTS SampleTable ( '
            + '     id   INTEGER  PRIMARY KEY '
            + '   , name TEXT '
            + '   , age  INTEGER '
            + ' ) ');

executeSql() の第2引数は、SQL 文中に ? を書いた順に、配列でパラメータを指定する。Java の PreparedStatement と同じ作りだ。パラメータが特になければ第2引数は不要。第3・第4引数がコールバック関数の指定になるが、なければ未指定で良い。

select()

データ取得処理。もう先ほど紹介してしまったが、以下のような構成で書いている。

this.db.transaction(function(tx) {
  tx.executeSql('SELECT * FROM SampleTable', [], function(tx, sqlResultSet) {
    console.log('データ取得 成功');
    // ココで取得したデータをアレコレしている
  }, function(tx, error) {
    console.log('データ取得 失敗 : ' + error.message);
    // 失敗時は画面にエラーメッセージを表示する
  });
});

SELECT した結果は、コールバック関数の第2引数 sqlResultSet で受け取れる。この中の rows プロパティが ResultSetRowList オブジェクトになっており、配列チックに取得することができる (厳密には配列ではなく、rows オブジェクトの中に length プロパティと item() 関数が宣言されている、という作りのようなので、配列だと思って forEach 等で処理しようとするとコケる)。

tx.executeSql('SELECT id, name AS onamae FROM SampleTable', [], function(tx, sqlResultSet) {
  var rows = sqlResultSet.rows;
  // 1行目のレコードを取得する
  var row = rows.item(0);
  // 1行目のレコードの「onamae」カラムの値を取得する
  var name = row.onamae;
});

カラム名は、SELECT 文で AS hoge などと別名を付与すればそれがプロパティ名になって格納されるので、上記のように実際のテーブル上は name カラムだが、取得結果としては onamae プロパティとなる。

この SQLResultSet オブジェクト、実は INSERT などの場合もココに実行結果が格納されている。

tx.executeSql("INSERT INTO 【省略】", [], function(tx, sqlResultSet) {
  // INSERT 成功時の行番号
  sqlResultSet.insertId;
  // 当該 SQL 文で何行の INSERT・UPDATE・DELETE が行われたかの行数
  sqlResultSet.rowsAffected
});

取得結果を li 要素で囲んで、ul#resultsappendChild() しているところは、単なる DOM 操作なので今回は説明を割愛。

drop()

テーブルを削除する処理。書き方はこれまでどおり。CREATE TABLE IF NOT EXISTS の逆で、DROP TABLE の場合は対象のテーブルが存在しなくてもエラーにならないよう、DROP TABLE IF EXISTS と書くことができる。

以上!

これで、cordova-sqlite-storage プラグインを使ったローカル DB の基本的な操作はできるようになったであろう。不明点や詳細は公式の README を熟読すればほとんどのことは書いてあるので、よく読んでいただきたい。

Cordova アプリでローカル DB を実現するには

SQLite ポケットリファレンス

SQLite ポケットリファレンス

Cordova アプリで、ローカル DB 的なことを実現する方法を検討する。今回対象にするのは iOS アプリの実装を前提とするため、Android への考慮は少なめ。

ローカル DB の種類

Cordova アプリはフロントエンド技術だけでアプリを構築できるため、基本的には Web 標準、HTML5 で採用されている技術が使用できると考えて良い。

ローカル DB として使えそうな技術の種類は以下のとおり。

このあたりの技術が、クライアントサイドだけでデータを保持できる仕組みとなる。

ではどの技術を使おうか、もしくは使えるか、という話になる。

iOS の対応状況

結論からいうと、iOS では IndexedDB にバグが多く使わない方が良い。小規模なら WebStorage、大規模なら WebSQL のバックエンドエンジンに採用されている SQLite を直に利用するのが良さそう、ということだ。

以下、それぞれの技術の問題点などをまとめる。

WebStorage (SessionStorage・LocalStorage) は問題なし

Cordova で作った iOS アプリ上での、WebStorage の利用は特に問題ないようだ。

SessionStorage の「セッション」の扱いがどこになるのか、細かく書いてあるサイトが見当たらなかったのだが、恐らくは「アプリをアプリスイッチャーから終了させるまで」になるのではないかと思われる。ちゃんと試していないので情報募集。

iOS における IndexedDB の対応状況

なかなか新しめの文献がなく苦労したのだが、それぞれ参考にしたサイトを以下に列挙しておく。

iOS における IndexedDB の対応状況はというと、

  • iOS7 以前は IndexedDB のサポートなし。
  • iOS8 からネイティブサポートされたが、バグが多い。

とのこと。これ以上新しい iOS ではどうなのか、文献がなかった。ちなみに Android は v4.3 以前がネイティブサポートなし。

IndexedDBShim は、こうした IndexedDB がサポートされていない部分に割り込んで動作し、サポートされていれば何も影響しない、という作りになっているようだ。

IndexedDB は心配。

というわけで、iOS で IndexedDB を使おうとすると、IndexedDBShim を噛ませて、実際には WebSQL を使用することになりそうだ。

であれば、最初から WebSQL を使った方が、余計なバグに遭遇しなくて済むであろう。

もっといえば、WebSQL 自体は HTML5 の標準仕様から外れているので、できれば WebSQL が内部で使用している SQLite を Cordova アプリ内で直接持って操作できれば、一番確実かと思う。

Cordova アプリで SQLite を扱えるプラグイン

すると既にそういうプラグインが存在していた。cordova-sqlite-storage というプラグインだ。基本的には WebSQL の API に近い記述方法で、SQLite に向かって生の SQL を投げ付けて実行する方式になる。

iOS で「WebSQL」に対応しているか、という話を調べようとすると、すぐこの cordova-sqlite-storage プラグインに行き着いてしまったので、WebSQL API がほとんどそのまま使えることだし、この際これで良いだろう。

次回はこの cordova-sqlite-storage の導入について解説する。