Roomで管理するSQLiteのデータベースをエクスポートまたはインポートしたいときに、CSVの入出力を行うのが面倒なので、データベースファイルをそのままコピーするという方法を見つけた。
ほとんど何もバリデーションを行わないため、実際にリリースするアプリには怖くて使えないが、自分用のソフトウェアには問題なく利用できる。
実際に、RoomでデータベースをコピーするためのAPIがあれば良いのだが、簡単に動くものはなさそうだ。
エクスポート
データベースを現在の状態で外部のURIにエキスポートしたい。
元のデータベースファイルの取得
ApplicationContext
があれば、
File sourceFile = getApplicationContext().getDatabasePath(DATABASE_NAME);
でdbファイルを取得できる。このときのDATABASE_NAMEはRoomDatabase.Builderでビルドするときに使った名前。
WALのチェックポイント
RoomではWrite-Ahead Logging (WAL)というものを採用している。これは、ジャーナルファイルに全てのトランズアクションを記録し、ジャーナルファイルが一定のサイズを超えると元のデータベースファイルが更新される仕組みである。よって、エクスポートするときにデータベースファイルを最新の状態にしておく必要がある。
チェックポイントを以下の構文で作動させることで、データベースファイルの内容を手動で更新できる。
pragma wal_checkpoint(full)
fullの代わりに、passive、restart、truncateのオプションがある。詳しくはドキュメントを参照。
Javaでは色々な書き方があるがこれだけのためにDaoを作りたくなかったので、ダイレクトに実行した。(mDb
はRoomDatabase
のインスタンス)
SupportSQLiteDatabase db = mDb.getOpenHelper().getWritableDatabase();
try {
Cursor c = db.query(new SimpleSQLiteQuery("pragma wal_checkpoint(full)"));
if (c.moveToFirst() && c.getInt(0) == 1)
throw new RuntimeException("チェックポイントが正常に完了しませんでした。");
db.close();
} catch (Exception e) {
// エラー対処
}
参照:https://stackoverflow.com/a/52089040/9121127
データのコピー
以下のようにしてファイル間でデータを移した。Fileであればnew FileInputStream(file)
またはnew FileOutputStream(file)
、UriであればgetContentResolver().openInputStream(uri)
またはgetContentResolver().openOutputStream(uri)
を使う。
try {
FileInputStream is = new FileInputStream(sourceFile);
FileOutputStream os = (FileOutputStream)getContentResolver().openOutputStream(destinationUri);
FileChannel src = is.getChannel();
FileChannel dst = os.getChannel();
dst.transferFrom(src, 0, src.size());
src.close();
dst.close();
} catch (IOException e) {
// エラー処理
}
インポート
上のステップでエキスポートしたデータベースファイルを読み込みたい。明らかに、他のファイルをそのまま読み込むと危険なので注意する必要がある。
手順はほとんど同じだが、コピーをする前に既存のインスタンスへのコネクションを閉じる必要がある。
mDb.close();
File destinationFile = getApplicationContext().getDatabasePath(DATABASE_NAME);
try {
FileOutputStream os = new FileOutputStream(destinationFile);
FileInputStream is = (FileInputStream)getContentResolver().openInputStream(sourceUri);
FileChannel src = is.getChannel();
FileChannel dst = os.getChannel();
dst.transferFrom(src, 0, src.size());
src.close();
dst.close();
} catch (IOException e) {
// エラー処理
}
データベースを更新した後は、アクティビティをリスタートした。
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
createFromAsset
RoomにcreateFromAssetやcreateFromFileというAPIがあるが、使い方がドキュメントを読んだだけでは分からなかった。
本当は公式APIの方が安全なのだが、残念ながら動かすことはできずに終わった。