拡張機能で mozStorage を使ってみた

必要に迫られたので mozStorage を使った拡張機能を作っている。
ドキュメントとしては Storage - MDC日本語訳 は翻訳途中) とか Firefox 3 Hacks の Piro さんの解説とかあるのだが、いざ実際に SQLite を使用した拡張機能を作ろうとしてみると、落とし穴がいくつかあった。
以下は「SQLite って何?おいしいの?」から始めた人の、試行錯誤の記録である。
後に続く人のご参考になれば幸い。
また、もっとエレガントな方法をご存じの方はご教示いただきたく。


1. データベースの選択

ユーザーごとのデータベースが必要な拡張機能なので mozStorage を使っているわけだが、Firefox でデータベースとして使用できるのは mozStorage だけではない。
昔から使われている RDF というチョイスもある。
実を言えば、最初は mozStorage を使うつもりはなかった。
データ数は多目に見積もっても 50×5 くらいと想定していたので SQLite を使うまでもないかもだし、何より mozStorage の API はまだフローズンされていない。
今後の仕様変更にいちいち追随する手間を考えると、古くても枯れた仕様の RDF の方がメンテナンス性がよろしかろう、と考えたのだ。

で、MDC の RDF からたどれる文書群とか、XUL チュートリアルの RDF の概要 から始まる文書を(日本語・英語とも)読んでお勉強しました。
結論: 何か違う。

「トリプル」を使ってデータを関連付けて保存できる、というのはわかった。
しかし、私がやりたいのはそんなまどろっこしいことではなく、データベースからデータを読み込んで JavaScript であれこれしたり、UI に表示したりすることなのだ。
RDF Datasource How-To がもっとまともな文書だったら RDF で進行していたかもしれないが、理論的な基礎からお勉強しなければならない RDF は個人的にはお勧めできない。

実用 Perlプログラミング で有名な Simon Cozens さんもこう言っている(URL は後述)。

...the third (引用者注:RDF) is goddamn impenetrable.

拙訳: ガッデム、RDF はわけわからん。

なお、Simon Cozens さんの現職は宣教師である。
神に仕える身で「ガッデム」とか口走っていいのだろうかという素朴な疑問もわくが、それだけひどかったと解釈できるだろう。


2. NG ワード

まずはテスト用のデータベースを作らないと話にならない。
MyMiniCity の巡回用リストを格納するデータベースなので、ファイル名を "mmclist.sqlite" として、

var file = Components.classes["@mozilla.org/file/directory_service;1"]
.getService(Components.interfaces.nsIProperties)
.get("ProfD", Components.interfaces.nsIFile);
file.append("mmclist.sqlite");

で、プロファイルフォルダのルートにデータベースファイルを作成できる。

中身がないので、次はテーブルの作成。
テーブル名は "city_table" とし、カラムには都市名、支援方法、巡回順が必要なので、

// 接続を確立
const storageService = Components.classes["@mozilla.org/storage/service;1"]
.getService(Components.interfaces.mozIStorageService);
const dbConnection = storageService.openDatabase(file);
// SQL 文を実行
dbConnection.executeSimpleSQL
("CREATE TABLE city_table (name TEXT, infra TEXT, order INTEGER UNIQUE)");

として、テーブルを追加してみた。
エラーが出て作成できない。

構文を何回も見直して誤りの無い事を確認。
さらに、記述方法をいろいろ変えてみてもやはりエラーとなった後で、ふと「カラム名が悪いのかも」と思いついた。
SQLite Query Language: SQLite Keywords にキーワード一覧があって、キーワードはテーブル名やカラム名には使えない NG ワードになっている。
私の場合は、カラム名の "order" が NG だったようだ。

ちなみに、この NG ワードは「多すぎて全部暗記している人は殆どいない」とか、

For most SQL code, your safest bet is to never use any English language word as the name of a user-defined object.

「一番安全なのは英語を使わないこと」とか書いてあるので、カラム名を "junban" とか "namae" にすることも考えた。
"namae" と "name" で typo の元なので、r_order とか c_name とかにしたけど。

テーブルができれば、あとはテスト用のセルデータを入れていけばいいわけで、JavaScript でも書き込めるが、SQLite Manager を使った方が楽。


3. カラムのデータ個数を収得する

JavaScript の場合は、

var MyArray = new array;
...
var count = MyArray.length;

とかで簡単に収得できるデータの個数。
1つのカラムに含まれている個数を収得する方法を探したが、どこにも書いてない。(当社調べ)
SQLite の関数 にある "count" ってのが使えそうだと考えて、自分で書いてみた。

var statement = dbConnection.createStatement("SELECT count(*) FROM city_table");
try {
while (statement.executeStep()) {
var rowlength=statement.getDouble(0);
}
}
finally {
statement.reset();
}

言うまでもなく、rowlength がデータの個数になる。

次項で述べる tree を使っている場合だったら、nsITreeView で rowCount を読み出す、という手が使えるシーンもあるだろう。

余談だが、デバッグには alert() ではなく、Application.console.log() 推奨。
alert() でやってたら、そのつもりはないのにテーブルの全セルを参照しちゃったらしく、alert ダイアログを 100個開こうとしてフリーズ状態になった。


4. リストかツリーか

データベースができても、それを UI で操作できなければ意味がない。
データベースから読み込んだデータを XUL 要素に表示するには listbox と tree という二つのチョイスがある。
いずれにしても template を使用する事になるので、MDC の Template Guide、特に SQLite Templates は必読。
(MDC をさまよっていて、この SQLite Templates を見つけたときはどんだけ嬉しかったか)

さて、SQLite Templates の解説例は listbox を使用している。
なので私も最初は listbox を使って作っていたのだが、複数のカラムを表示するところでつまづいた。
どこをどうやっても、最初のカラムにカラム二つ分の内容が表示されてしまうのだ。
私の listbox の理解が不足しているだけ、というオチも考えられるが、個人的には複数のカラムを実装する予定があるのなら tree をお勧めする。
言うまでもなく XUL チュートリアルの ツリー は必読だが、Gomita さんの SCRAPBLOG : カスタムツリービューの基本的な使い方(その1~表示) から始まる解説もある。

参考までに tree を使った実装例。(骨組みだけ)











5. データベーステンプレートの配布

XUL, mozStorage and SQLite
1. で引用した Simon Cozens さんが書いたエントリで、mozStrage を使った XUL アプリのコード片がいろいろ公開されている。
"Object relational mapping" あたりは、スキルのある人なら便利に使いこなせるかもしれない。
また、よく読んでみると 3. でえらそうに書いた count を使ったデータ個数収得も、下の方にしっかり載ってたりするorz

さて、「これは使える」と思ったのが、最初の "Preparations" にあるデータベーステンプレートの配布。
あらかじめ最低限のデータを入れておいた SQLite ファイルをテンプレートとして、ユーザーのプロファイルフォルダにコピーする、という方法だ。
で、素直にコードをコピペして動かしてみた。
ダメじゃん。

前述したように、これはスタンドアロンの XUL アプリのコードであり、

// This is the template file
var ours = dserv.get("AChrom", Components.interfaces.nsIFile);


の "AChrom" はプログラムの Chrome フォルダなんですな。
ここいら辺がわかっている人だったら無駄にコピペしてみるなんて事はしないはず、とか考えると落ち込むが、落ち込んでばかりもいられない。

MDC の File I/O とか Stylish の StylishStartup.js では、ファイルを読むのに Torisugari さんのコード を使っているようなので一応試してみたけど、sqlite ファイルがバイナリのためかうまく行かなかった。
で、最終的に File I/O の英語版の "Getting your extension's folder" のコードと Cozens さんのコードを組み合わせて、

// load (or copy from template) db file
var file = Components.classes["@mozilla.org/file/directory_service;1"]
.getService(Components.interfaces.nsIProperties)
.get("ProfD", Components.interfaces.nsIFile);
file.append("mmclist.sqlite");

if (!file.exists()) {
var MY_ID = "{E71D11C3-BEDF-475a-B0FF-A82AB905988F}"; // 拡張の GUID
var em = Components.classes["@mozilla.org/extensions/manager;1"]
.getService(Components.interfaces.nsIExtensionManager);
var template = em.getInstallLocation(MY_ID).getItemFile(MY_ID, "mmclist_template.sqlite");
var dserv = Components.classes["@mozilla.org/file/directory_service;1"]
.getService(Components.interfaces.nsIProperties);
template.copyTo(dserv.get("ProfD", Components.interfaces.nsIFile), "mmclist.sqlite");
}

このコードで、もしユーザーのプロファイルフォルダに mmclist.sqlite がなければ、拡張のルートフォルダ(って言うのかな? install.rdf と同じ場所)に同梱した mmclist_template.sqlite をコピーして mmclist.sqlite にリネームする、という動作ができた。


あとがき

見る人が見れば「何でこんな基礎的なことをぐじゃぐじゃと」なのだろうが、ドキュメントがありそうでないのが XUL の常。
取っかかりの部分で道に迷うと無駄に数時間、下手すると数日を費やす事になる(経験者談)。

作者の予想に反して、定時巡回先が 200件以上という人もいたので SQLite の使用は正解だったわけだが、ものが MyMiniCity 限定の拡張機能だから、いまだに MyMiniCity やってる人でなければその存在すら知られないだろう。
mozStorage の実装回りは汎用性があるはずなので、ちょっとでも役に立つ部分があれば何より。

まだベータ版だし、泥縄式のコードなのであまりさらしたくはないのだが、拡張を見てみたいという物好きな人がもしいたら、MMC toolbar に置いてあるのでご自由に。

この記事へのコメント

この記事へのトラックバック