2012/06/29

バージョンアップを続けるAndroidアプリでのDBスキーマの更新方法

さて、AndroidアプリケーションにはSQLite3への接続を管理するためのクラスとしてandroid.database.sqlite.SQLiteOpenHelperクラスがあります。

通常はこのクラスを継承して、子クラスを自分のアプリケーションで定義するわけですが、今回はスキーマをバージョンアップするためのonUpgradeメソッドの書き方についてです。

よくあるサンプルは直前のバージョンと自分のバージョンを比較して、バージョンアップ用のコードを定義していますが、アプリを使うユーザーが次のようなシナリオに遭遇する場合を想定しているでしょうか。

  • Version 1.0のアプリをダウンロードして使う
  • Version 2.0がリリースされるが、ユーザーは更新を行なわなかった
  • Version 3.0がリリースされ、ユーザーはアプリを更新した

アプリは毎回のバージョンアップで、ALTER TABLE ... ADD COLUMN ...を行なっているとします。

SQLiteOpenHelperクラスを紹介するWebサイトはいろいろあったのですが、こういう使い方を説明しているところはなかったのでまとめる事にしました。

まぁ良く考えれば分かる事ですが、いまのところの自分のベストプラクティスをメモしておきます。

解決するべき課題

前提として毎回ユーザーはアプリを更新しない、 Version 1.0からVersion 20.0へアップグレードしても、アプリケーションの内部データは一貫性を保ちたい、という要求があるとします。

これ自体は日本的というか、こういうのは諦めて、onUpgradeでDROP TABLE 〜 CREATE TABLEを行なってデータを初期化してしまうのも、まま、見ることです。

ここでは敢えて、この課題を解決する方法を目指します。

定石1:バージョン毎にスキーマやデータを変更するメソッドを定義する

DBのバージョンは1から始まりますが、この時のDB定義はonCreate(SQLiteDatabase db)メソッドの中でdbオブジェクトにSQLを発行して定義します。

バージョン2からは例えば、次のようなメソッドを用意してあげます。 これは直前のバージョン1からのアップグレードだけを前提にしています。

ExifPMで使用しているversion 2、3用メソッドの例

    private void updatedb2(SQLiteDatabase db) {
        Log.d(this.toString(), "upgrading db 1 to 2");
        StringBuilder sql = new StringBuilder();
        ...
        db.execSQL(sql.toString());
    }
    private void updatedb3(SQLiteDatabase db) {
        ...
    }

定石2:onCreate、onUpgradeメソッドに一度記述した内容は消してはいけない

もちろんこれは内容を変更しないという意味ではありません。

定石1で作成したバージョン毎のメソッドを追記する操作だけを許可するというルールです。

ExifPMで使用しているonUpgradeメソッドの全体


    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if(oldVersion < 2 && 2 <= newVersion) {
            updatedb2(db);
        }
        if(oldVersion < 3 && 3 <= newVersion) {
            updatedb3(db);
        }
    }

よくみる例はここで、if (oldVersion == 1 && newVersion == 2) {...}みたいにしていますが、(oldVersion, newVersion) = (1,3)の組み合せもあるわけで、まぁそれは破綻するわけです。

ちなみにonCreateメソッドの方は初期化時に1回しか呼ばれないためもっと簡単です。

    @Override
    public void onCreate(SQLiteDatabase db) {
         ...
         updatedb2(db);
         updatedb3(db);
    }

念のためにコンストラクタの書き方

SQLiteOpenHelperのサブクラスとしてSQLDBHelperというクラスを定義していて、この中で親クラスのコンストラクタを呼び出し、DBバージョンを指定しています。

コンスタクタの例


    public SQLDBHelper(Context context) {
        super(context, "appdb", null, 3);
    }

実際にはバージョン番号はアプリケーション全体で定数を管理しているクラスの中でfinal static intで定義されていますが、こんな感じでSQLDBHelperを使う側はDBバージョンを意識する必要がなくなっています。

さいごに

この方法でDBバージョン Nに対して、N-1との差分を記述していって、場合によってはデータメンテナンスも行ないます。

まぁ初期化した方が楽だったりもするんですが、ユーザーデータをできるだけ開発側の理由で、消したくないという思いもあるので、こういう方法を採用しています。

あんまりこういう事が検索に引っかからないのは、もっと良い方法があるからなのか、かなり気になっています。 なにか理由があるのかなぁ…。

0 件のコメント: