2011年3月23日水曜日

Android SQLite テーブル定義変更

小遣い帳アプリに写真保存機能追加でテーブルにカラム追加が必要になった。WebシステムでのMySQLに置き換えて考えてしまうと、システムをメンテナンスモードにしてその間に直接SQL文を流して追加したり変更したりすると思うがAndroidアプリのSQLiteの場合だと少し勝手が違う。

方法はいくつかあるのだろうが、小遣い帳アプリでは初めてのAndroid 第3版の9.3.1のようにSQLiteOpenHelperのサブクラスとして実装してるので、それを前提とする。

バージョンがあがったときのテーブル定義変更はonUpgradeメソッドに実装してやれば良い。「初めてのAndroid」では、下記引用のような例になっている。
  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion,
      int newVersion) {
    db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
    onCreate(db);
  }
この例では、単純に古い表を削除しているが、読者がそのつもりになればもっと賢いコードを書けるはずだ。
そのつもりにならなくてももっと賢いコードを書かないと、バージョンアップでそれまでの登録データが消えるアプリになってしまう。そんなアプリはユーザからのガチの怒りを受け止めることになるだろう。

このonUpgradeは、参照しているデータベースが古くなっていると判断すると呼び出されるらしい。ソースを読んでないのでどこでDBのバージョン管理してどう判断しているかまではわからない。わからないが、upgrade前のバージョンがoldVersionとして、upgrade後のバージョンがnewVersionとして渡されるので、それを判断してバージョンアップにあったテーブル定義変更をしていけば良いのだと思う。
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion == 1 && newVersion == 2) {
            // バージョン1からバージョン2のテーブル定義変更のSQL
        }
    }
今回は1回目の変更だから1から2の変更を書けば良いけど、次バージョンアップで3になると、2から3に上がる場合と、1から3に上がる場合を考慮しないといけないのか。ユーザがDBバージョンアップしてるかわからないので全パターンのバージョンアップを考慮しないといけないとなると結構大変そうだな。やってみるとそんなに大変でもないのかな。経験がないのでわからないが。
あと、MySQLのALTER TABLEってレコードが何百万とあると、時間が数分から数十分かかったりもするけど、SQLiteだとどうなんだろう。データ量も多くなり得ないから気にするほどでもないのかな。

まぁ、だいたいこんな感じでonUpgradeに実装してAndroidのSQLiteのテーブルを管理していくみたいだ。





で、今回の小遣い帳アプリでの変更の話も書いておくが汎用的な話でもなく別におもしろいところもない。簡単に言えば読む必要はないということだ。

画像IDを持たせたいのでカラム追加をする。この画像IDはユニークである必要がある。もともとユニークなIDは持っているのだが、AUTO_INCREMENTでINSERT前にはわからない(MySQLでの経験に基づく考えなので違うかも)ので、別にカラムに画像IDを用意。バージョンアップ後の新規登録に関しては、登録画面時にアプリ側で時間を画像IDにして登録、事前に登録してあるデータに関しては、AUTO_INCREMENTのIDを画像IDにする、という設計。

そのためのonUpgradeメソッドはこんな感じの実装。
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion == 1 && newVersion == 2) {
            // バージョン1と同じカラムのテンポラリーテーブル作成
            db.execSQL("CREATE TEMPORARY TABLE tmp_" + TABLE_NAME + " (" +
                    _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + DATE + " TEXT, " + 
                    TYPE + " TEXT, " + AMOUNT + " INTEGER, " + MEMO + " TEXT);");
            // 既存テーブルから作成したテンポラリーテーブルにデータを入れる
            db.execSQL("INSERT INTO tmp_" + TABLE_NAME + " SELECT " +
                    _ID + ", " + DATE + ", " + TYPE + ", " + AMOUNT + ", " + MEMO + 
                    " FROM " + TABLE_NAME + ";");
            // 既存テーブル削除
            db.execSQL("DROP TABLE " + TABLE_NAME + ";");
            // テーブル作成
            onCreate(db);
            // テンポラリーテーブルから新テーブルにデータを入れる。画像IDは_IDを使用して
            db.execSQL("INSERT INTO " + TABLE_NAME + " SELECT " +
                    _ID + ", " + DATE + ", " + TYPE + ", " + AMOUNT + ", " + MEMO + ", " + _ID
                    + " FROM tmp_" + TABLE_NAME + ";");
        }
    }
ADD COLUMNして、画像ID=_IDのUPDATE文を実行でも良かったのかな。ちゃんとパフォーマンスを比較した方が良いか。なんかSQLite使い慣れないため(言い訳)かパフォーマンステストはやる気が出ないので、今度やる気が出たときにやる。