2011年1月26日水曜日

Android の SQLite と ContentProvider

X メール開発の中で SQLite & ContentProvider について解った事を書いておきます。

ただ、ここは深いところまで研究をしていないので
内容については曖昧なところがありますが、所詮ブログですのでご容赦を


X メールは始め「シンプルに手早く」作るつもりだったので、SQLite(以下DB)のように
複雑で処理コストのかかる仕組みは使わないでいく方針だったんです。

なので、始めは様々なデータを ObjectOutputStream を使ってファイル吐き出し
していたんです。

ただやっているうちに、処理の多重化やネットワーク断裂などの問題が
意外に大きい事が解りまして、I/O系の例外を担保したり、将来的な連携等も
視野に入れたいという思いから少しずつDB化を進めて、バージョン 2.0.0 では
添付ファイル以外は全てDB化してあります。

添付ファイルをDB化しない理由はコピーなどを簡単に他のアプリケーションから
行えるようにするという観点からです。

というわけで、X メールはメールデータも共有しています。アカウントデータ以外は
ContentProvider を通して公開しています。
※これはマニフェストの android:protectionLevel="signatureOrSystem" || "normal" で決定しています。


ContentProvider って?

というわけで、普通DBの使用はまず ContentProvider のマニフェストへの登録から始まります。

ごっちゃになりやすいんですが、ContentProvider は別にDBに特化したAPIではないです。
データ操作(update、query、insert、delete 以下DML)をURIを通して行いましょうという統一的な
方法のフレームワークです。

だから、別にDB使うから ContentProvider 使わなきゃいけないという訳じゃないのですが、
なかなか使いやすい世界観ですので、使うに越したことは無いと思います。
標準化・共有が ContentProvider の存在意義でもありますし。

考え方や動きはインテントを簡単にした感じで、マニフェストで受け付けるURIと処理する
クラス、公開レベルを宣言し、データ操作処理の実装をクラスで記述するという流れです。


ただ、Activity でいう Zygote のように、呼び出しの仕組みがどうなっているのか
という部分までは掘り下げ切れていません。ただ、この仕組みの構造上、何らかの
管理タスクがあるのだろうというところまでは想像できますが…。


【例】ContentProvider マニフェスト
<provider android:name=".mail.MailProvider"
android:protectionLevel="normal"
android:authorities="jp.sn.xmailer.provider.mails"/>

上記は実際のX メールのメールデータ ContentProvider の宣言です。
jp.sn.xmailer.provider.mails というURIを受け取りますよ、という宣言ですね。

で、処理するクラス MailProvider を書けばいいんですが、この中にDBに関する
記述をしていくので、少し頭を切り替える必要があります。




SQLiteOpenHelper の実装

SQLiteOpenHelper は一言で言うとバージョンアップに対応するために使用します。
例えばテーブルAをカラムB、Cでリリースした後に、次のアップデートでカラムDが
追加された時の事を考えれば解りやすいのではないでしょうか?

カラムDがカラムB・Cの値によってあるべき値が変わる場合、単純にテーブルに
addColumn するだけではシステムが成り立たない場合ってありますよね?

こういったときにDBのデータ操作プログラムを仕込む事が出来るのが
SQLiteOpenHelper なのです。


まず、システム上にDBヴァージョンを持たせておきます。これは、データ操作処理が
走る必要がある変更をした時にカウントアップさせます。

次に SQLiteOpenHelper を実装したクラスを作成します。


【例】
public class DBOpenHelper extends SQLiteOpenHelper{
public DBOpenHelper(Context context){
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase arg0) {
try {
arg0.execSQL("テーブルのCreate文");
...
} catch (SQLException e) {
;
}
}
@Override
public void onUpgrade(SQLiteDatabase arg0, int arg1, int arg2) {
更新時の処理
例えば、DBを読み込んでテーブルを削除
onCreate を呼び出し、読み込んだデータを修正しながら insert していくとか
  arg1 は更新前のDBバージョン、arg2 は新しいDBバージョン
}
}

この実装の意味するところは、「新規(DB_NAMEが見当たらない)なら onCreate が呼び出され
て終了、あった場合には DB_VERSION を比較して、現在よりも大きい値なら onUpgrade を
呼び出し、そうでなければ何もせずに処理を終了し、コンストラクトを終える」

という意味です。

この後に getWritableDatabase() を呼び出すなどして DML(さっきの insert 等) を発行できる
インスタンスを取得できるようになります。

この SQLiteOpenHelper は必ずDBに触れられる前にロードされるように仕込んでおく必要が
あるのでそこは意識しておきましょう。

例えば、アプリケーションの開始で必ず確認するようにしても構いませんし、ContentProvider
の onCreate でインスタンス化するのが最も効率的かもしれません。

SQLiteOpenHelper#onCreate に渡す引数がDB_NAMEである事と、DBインスタンスのDML
メソッドの引数の始めが TABLE_NAME である事から察しが付いている人もいると思いますが、
SQLiteOpenHelper は DB_NAME 毎に作成し、アップグレードもその単位で行われるので、
DB_NAME 毎のアップグレードプログラムを実装する必要があります。


ContentProvider の実装


後は ContentProvider のDMLメソッドで、取得したDBインスタンスのDMLメソッドを
呼び出すだけです。これらは上手く実装することで、画一的な実装になる可能性が高いでしょう。
多くの場合は、URIの記述方法で単一のデータに対する操作なのか複数なのかを判断し、
DBインスタンスのDMLメソッドを効率的に呼び出す実装をするだけです。



あまり深堀り出来ていませんが、解ったところはこんなところです。
自分はSQLに慣れているんで、まんま発行出来てもよかったんですが、
そういう所で結局独自的な記述方法が出来てしまう可能性を考えるなら
こういったAPIだけで処理をさせるというのも良いのかもしれないと感じています。

下手にSQLで何でも出来てしまうから、テーブルの正規化を怠るんじゃないの?
という逆説的な問題点に気づかせてもらったのもこれのお陰ですね。
(もしかしたら Google はそれを訴えたくてこういう仕様にしたのかも知れません)


こんなところですかねー。

0 件のコメント:

コメントを投稿