AndroidStudioのRoomでの事前取り込み

# アプリ初回起動時に特定データセットを取り込む(Prepopulate)

以下、公式ページ「[Room データベースを事前に取り込む](https://developer.android.com/training/data-storage/room/prepopulate?hl=ja)」より引用

>事前パッケージ化済みデータベース ファイルから Room データベースに事前取り込みする際、データベース ファイルがアプリの assets/ ディレクトリ内にある場合には、次のように、RoomDatabase.Builder オブジェクトから createFromAsset() メソッドを呼び出してから、build() を呼び出します。

>``` java
    Room.databaseBuilder(appContext, AppDatabase.class, "Sample.db")
        .createFromAsset("database/myapp.db")
        .build();
```

で、「**事前パッケージ化済みデータベースファイル**って何？？どうすんのこれ？？」ってなったので解決策。




## 環境
MacOS BigSur　11.3
AndroidStudio(日本語化済)　4.1.3
room_version　2.2.6
エミュレータ　API30 Android11.0
openjdk version "1.8.0_242-release"
OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)
OpenJDK 64-Bit Server VM (build 25.242-b3-6915495, mixed mode)
※既にRoomを利用してデータベースが保存されているものとします
>参考：[Room を使用してローカル データベースにデータを保存する](https://developer.android.com/training/data-storage/room?hl=ja)

## 作業の流れ
1. エミュレータに保存されているデータベースを取得
2. 外部アプリで新たにデータベースを編集
3. 追加したデータをデータベースが作成されるタイミングで読み込まれるようにする（事前取り込み）

### 1.エミュレータに保存されているデータベースを取得
予め、PC内の任意の場所に新しくフォルダ(今回は「predatabase」にしました)を作っておきます。

エミュレータを起動した後に
1. 「表示」タブをクリック
2. 「ツールウィンドウ」をクリック
3. 「デバイスファイルエクスプローラー」をクリック
4. /data/data/[パッケージ名]にあるdatabaseフォルダを右クリック、「別名保存」から予め作成したフォルダを選択

この際、databseフォルダそのものはダウンロードされず、databaseフォルダ内の以下の3つのファイルが選択した箇所(predatabase)保存されます。

* [データベース名]
* [データベース名]-shm
* [データベース名]-wal

これらのファイルが文字化けしていても問題ありません。
ここで、**「[データベース名]」ファイルを「[データベース名].db」に名前を変更**します。
詳細不明ですが、古いAndroidOSの場合は.dbがついていることがある模様です。元から.dbがついている場合はリネームは不要と思われます。

<pre>predatabase
├── [データベース名].db
├── [データベース名]-shm
└── [データベース名]-wal</pre>

なお、以下のようなコードをMainActivityで実行すると、データベースファイルの場所がわかります。こちらで表示される場所はセキュリティ対策などの観点から実機などでは閲覧できないそうです。が、私の環境ではエミュレータだからか分かりませんが閲覧や保存できました。
とりあえずは/data/data/[パッケージ名]/databaseで良いと思います

```java
//DBPath:data/user/0/[パッケージ名]/databases/[データベース名]
Log.d("DBPath:",getDatabasePath("[データベース名]").getAbsolutePath());
```




### 2.外部アプリで新たにデータを追加
AndroidではSQLiteが利用されており、データベースを管理できるアプリを使って中身の確認とデータの追加をおこないます。
今回は「[DB Browser for SQLite](https://github.com/sqlitebrowser/sqlitebrowser)」を利用しました。
HomeBrewが利用できるならば、以下のコマンドで利用できるようになります。

```
brew install --cask db-browser-for-sqlite
```

インストール後はLaunchpadからを起動し、「データベースを開く」から1で保存した[データベース名].dbを選択します。
エラーで開けない場合や、開けてもデータベース構造のテーブルやインデックスが(0)となる場合は以下の2点を確認してください。

* 同一フォルダに3つのファイルがある
* 開こうとしているファイルの拡張子が.db

編集は「データ閲覧」をクリックし、テーブルを選択した後に、「新しいレコード」や「レコードを削除」、「SQL実行」からデータベースを編集します。
編集後はバツマークを押して、保存するを選択すればOKです。

### 3.追加したデータをデータベースが作成されるタイミングで読み込まれるようにする（事前取り込み）
まず、Assetsフォルダーが存在しない場合はAssetsフォルダーを作成します。右クリックから、新規→フォルダー→Assetsフォルダーを選択し、完了をクリックすれば作成できます。

次に、Assetesフォルダーを右クリックし、新規→ディレクトリーから、新しいディレクトリを作成します。ここでは、predbdataディレクトリと名付けます。
作成したディレクトリに、2で編集したファイルを含む
* [データベース名].db
* [データベース名]-shm
* [データベース名]-wal
をドラッグアンド&ドロップで移動させ、中身が空になったpredatabaseフォルダは削除します。
<pre>Assets
├─predbdata
│├── [データベース名].db
│├── [データベース名]-shm
│└── [データベース名]-wal
└ (その他のディレクトリ)</pre>

ここでようやく冒頭のコードを参考に記述します。
以下は私が作成中のアプリのコードです。シングルトンパターンを使ってImgDBクラスのインスタンスを作っています。ImgDBクラスの中身は話がそれるので割愛します。ご自身のデータベースクラスに置き換えて読んでください。

1にてデータを取得した後にデータベースのバージョンを引き上げると、
predbdataにある事前読み込みするデータベースのバージョン(ex.2)<今のバージョン(ex.3)
となり、Migrationの設定が必要になります。

Migrationについては公式の「[Room データベースを移行する](https://developer.android.com/training/data-storage/room/migrating-db-versions?hl=ja)」を参考にしてください。（Migration関連は私が学習中なのもので。。）

```java
//importは省略

public class DBSingleton {
    private static ImgDB instance = null;
    private DBSingleton() {}

    public static ImgDB getInstance(Context context) {
        if (instance != null) {
            return instance;
        }
        instance = Room.databaseBuilder(context, ImgDB.class, "imgDB")
//破壊的Migration。データベースのバージョンアップでカラム変更などが行われた際に元々存在したデータが全て削除される。
//                .fallbackToDestructiveMigration()
                .createFromAsset("predbdata/[データベース名].db")//事前読み込み
                .addCallback(rdc)
//                .addMigrations(MIGRATION_2_3)//必要に応じてMigrationを設定しておきます(*1)
                .build();
        return instance;
    }

     //データベースを開くときについでにデータベースのバージョン表示します
     static RoomDatabase.Callback rdc = new RoomDatabase.Callback() {
        //データベース作成時に一度だけ呼ばれる
        public void onCreate (SupportSQLiteDatabase db) {
            //事前読み込み出来なさ過ぎてここでINSERTしてやろうかと考えてたのは秘密。
            Log.d("CreatRoomDatabase","vsersoin:"+db.getVersion());
        }
        //データベースを開くたびに呼ばれる
        public void onOpen (SupportSQLiteDatabase db) {
            Log.d("OpenRoomDatabas","vsersoin:"+db.getVersion());
        }
    };

    //バージョン2から3へ移行する時に実行される コメントアウトされている(*1)の箇所
    static final Migration MIGRATION_2_3 = new Migration(2,3) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            //適切な移行処理(省略)
            Log.d("db migration","2-3:"+database.getVersion());
        }
    };
}
```

これで、事前読み込みの準備完了です。
一度エミュレータからアプリをアンインストールなどして再度データベースを作成すると事前読み込みが行われます。