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

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

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

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

前提と動作サンプル

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

上の記事に従って 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 コマンドで作成した直後のアプリをベースにする。既に

という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

# replace が undefined なナンタラ…とエラーが出るようなら、内部で使用している ios-sim のせいと思われるので以下で回避
$ cordova emulate --target=iPhone-7

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 を熟読すればほとんどのことは書いてあるので、よく読んでいただきたい。