アプリの設定や、ユーザが作成したデータを保存するために android では sqlite3データベースが使用できます。
さて、アプリリリース後、保存するデータを拡張したい、設定項目を増やしたい(変更したい)などの理由でdatabeseのスキーマ(テーブル定義)を変更したいという欲求は、初回リリース時にどんなにしっかりと設計したとしてもやはり多かれ少なかれ出てくると思っています。
そのあたり、androidでは、SQLiteOpenHelperという、DBへの接続やセッション管理などを行ってくれるclassが定義されていて、それを使うと、テーブル定義(などの)バージョン管理的なことができるようになっています。
使うだけなら簡単です。
SQLiteOpenHelperを継承したclassを作成し(以後OpenHelperとします)、アプリが期待するテーブル定義のバージョンを基底クラス(SQLiteOpenHelper)のコンストラクタ引数としてわたします。
そうすると、もし、現在アプリが保存しているテーブル定義のバージョンが、(アプリのバージョンアップなどにより)アプリが期待するテーブル定義のバージョンより低い場合、
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
という定義のメソッドが呼び出され、この中でテーブル定義を更新することができます。
具体的にはこんな感じです。
public static class MyAppOpenHelper extends SQLiteOpenHelper {
private static final int CURRENT_SCHEMA_VERSION = 3;
public MyAppOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {
super(context, name, factory, CURRENT_SCHEMA_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// データベースを新規作成する処理
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// データベースが oldVersion から newVersion(CURRENT_SCHEMA_VERSION) にアップグレードされた場合に呼ばれる
if (oldVersion == 1) {
// version1 から version3 への更新処理
} else if (oldVersion == 2) {
// version2 から version3 への更新処理
}
}
}
onUpgrade()内で、if文により現在のテーブル定義毎に処理を分岐させていますが、このままだと、CURRENT_SCHEMA_VERSION = 10 くらいまで更新されたときのことをご想像いただくと、なんとなくだんだんしんどくなってくるのがご想像できるかと思います。
今回は僕がやっている方法を載せてみたいと思います。
そこそこ見栄え良くという趣旨なので、最善の方法とは言えないかもしれませんが、if文をずらっと並べるよりは見栄えも保守性も良いんじゃないかなと思っています。
public interface DBUpdate {
public int update(SQLiteDatabase db);
}
public class DBUpdateTo2 implements DBUpdate {
public int update(SQLiteDatabase db) {
db.execSQL( .....); // テーブル定義バージョン1から2への更新処理
return 2; // 更新後のバージョンを返す
}
}
public class DBUpdateTo3 implements DBUpdate {
...
...
public class ExecDBUpdate {
private static Map<Integer, DBUpdate> updatorMap = new HashMap<>();
static {
updatorMap.put(1, new DBUPdateTo2());
updatorMap.put(2, new DBUPdateTo3());
updatorMap.put(3, new DBUPdateTo4());
updatorMap.put(4, new DBUPdateTo5());
}
public static final int updateAll(int currentVersion, SQLiteDatabase db) {
int processingVersion = currentVersion;
int result = -1;
DBUpdate updator = null;
do {
updator = updatorMap.get(processingVersion);
if (updator != null) {
result = updator.update(db);
if (result > processingVersion) {
processingVersion = result;
} else {
result = -1; // error
break;
}
}
} while (updator != null);
return result;
}
}
public class MyAppOpenHelper extends SQLiteOpenHelper {
private static final int CURRENT_SCHEMA_VERSION = 3;
public MyAppOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {
super(context, name, factory, CURRENT_SCHEMA_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// データベースを新規作成する処理
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
int result = -1;
try {
result = ExecDBUpdate.updateAll(oldVersion, db);
} catch (Exception e) {
Log.e(SendToNoteDataBase.class.getSimpleName(), String.format("db update failed %d -> %d", oldVersion, newVersion), e);
}
if (result != newVersion) {
// エラーが起きたらあきらめて作り直す
dropAllTables(db);
onCreate(db);
}
}
}
テーブル定義のバージョンを1つ上げる処理を記述するためのDBUpdateインタフェースと、それを実装したclassを、アプリのバージョンアップでテーブル定義の更新が必要になるびに新しく追加していきます。
追加したクラスは、アプリ初期化時にテーブル定義更新管理クラスのExecDBUpdateに登録しています。
onUpgrade() が呼び出されたら、ExecDBUpdate.updateAll() を呼び出し、その中で DBUpdate.update(db); をこれ以上更新できなくなるまで呼び出しています。
最終的に、更新後のバージョンがアプリが要求するバージョンと同じになれば更新成功です。
なお、何らかの理由(更新中のエラーなど)で更新が失敗した場合には、僕は今のところあきらめてデータを初期化しています。
この方法の利点は、テーブル定義が何度更新されても、修正箇所はは一つ前のバージョンからの更新クラスの新規追加と登録のみで完了することです。既存コードの修正も基本的にはありません。
以上となります。最後まで読んでいただいてありがとうございました。
補足:この手のことをやってくれるフレームワーク的なもの、きっとすでにありますよね。。。。