Migrate ActiveAndroid to Room
以前作成したAndroidアプリのORMとしてActiveAndroidを使用していたのですが、Android 8.0からreceiver上でactive android経由でデータベースにアクセスすると下記のようなエラーが発生するようになりました。
java.lang.RuntimeException: at android.app.ActivityThread.handleReceiver (ActivityThread.java:3652) at android.app.ActivityThread.-wrap18 (Unknown Source) at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1985) at android.os.Handler.dispatchMessage (Handler.java:109) at android.os.Looper.loop (Looper.java:166) at android.app.ActivityThread.main (ActivityThread.java:7377) at java.lang.reflect.Method.invoke (Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:469) at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:963)
ActiveAndroidのrepositoryを見る限り、ActiveAndroidの開発自体も止まってるし、この機にRoomへ移行してみました。 ActiveAndroidからRoomへの移行は公式ホームページ通り進めていくと、いくつかつまづくポイントが出てきたので、そちらについてまとめたいと思います。
つまづきポイント
1: Dbにアクセスできない
公式ホームページではDbへのアクセスのために
AppDatabase db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "database-name").build();
とデータベースの名前に拡張子がありませんが、ActiveAndroidからの移行の場合 database-name.db
と拡張子をつけないとアクセスできません。
2: マイグレーションが必要
マイグレーションの追加
Migrationをしない状態でアプリを起動すると下記エラーが発生します。
Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.
このエラーが発生した場合、マイグレーションの設定を記述する必要があります。
Room.databaseBuilder(context, AppDatabase::class.java, "app.db") .addMigrations(object: Migration(activeAndroidMigrationVersion, activeAndroidMigrationVersion + 1){ override fun migrate(database: SupportSQLiteDatabase) { } }) .build();
上記のように設定するんですが、 activeAndroidMigrationVersion
にはActiveAndroidのmigration バージョンを指定してください。
整合性が取れたマイグレーションを記述
RoomではEntityで記述された内容がDBにそのまま書き込まれます。 この時ActiveAndroidで作成したテーブルとRoomで作成されるテーブルの整合性が取れない場合下記エラーが発生します。
Caused by: java.lang.IllegalStateException: Migration didn't properly handle
ActiveAndroidとRoomのテーブルが異なる原因として下記が考えられます。
- NOT NULL 成約の有無
Roomではfieldの型として int
や boolean
などのプリミティブ型を指定するとデフォルトでNOT NULLになるのですが、ActiveAndroidではこの制約がない場合が多い(アノテーションを加えなければいけない)ので、確認が必要です。
- カラム名が異なる
Roomではメンバ変数の名前がそのまま、tableのカラム名になります。もし、変数名と異なる名前をつけたい場合は
@ColumnInfo(name = "first_name")
のようにカラム名を直接していしてください。
また上記の不整合メッセージはそのままテキストで吐き出されるため、カラム数が多いとどのカラムが違うのか確認するのが大変です。 そこで比較が容易になるようスクリプトを作成しました。(Macでのみ動作します) 是非使ってみてください。
#!/bin/bash # Clean up on exit function finish { rm -f expected found } trap finish EXIT # How to parse JSON JQ="jq --sort-keys" # Convert log to JSON format toJson () { # First sed removes spaces between indices ("columns:[id, col1, col2]" becomes "columns:[id,col1,col2]") # Indeed, spaces are later replaced to quotes, and we don't want that (would cause parse errors: columns:[id,"col1,"col2]) # Second sed ensures single column indices have quotes sed -E ':a;s#(\[[[:alnum:],]*) ([[:alnum:] ,]*\])#"\1\2"#;ta' \ | sed -E 's/(\[[[:alnum:]]+\])/"\1"/g' \ | sed -e 's/} ,/},/g' \ -e 's/Column//g' \ -e 's/Index//g' \ -e "s/'/\"/g" -e 's/=/":/g' -e 's/ /"/g' -e 's/{/{"/g' \ | sed -e 's/TableInfo//g' } # Check arg in="$1" if [ "$in" == "" ] then echo "Usage: $0 <dump of error message>" >&2 echo "For example:" >&2 echo "Usage: $0 \"java.lang.IllegalStateException: Migration failed. expected:TableInfo{name='...', columns={...}, foreignKeys=[], indices=[]} , found:TableInfo{name='...', columns={...}, foreignKeys=[], indices=[]}\"" >&2 exit 1 fi # Parse input: split into expected/found splitInput=$(cat "$in" | sed -e $'s/expected:TableInfo/\\\n/g' -e $'s/ , found:TableInfo/\\\n/g') echo "$splitInput" # Diff 'expected' and 'found' as JSON echo "$splitInput" | head -n 3 | tail -n 1 | toJson | $JQ . >expected echo "$splitInput" | tail -1 | toJson | $JQ . >found vimdiff expected found
使用方法
- 上記スクリプトを
diff.sh
という名前で保存。 jq
を使うので、なければbrew install jq
コマンドを打ってインストール。- エラーログを
error.txt
というファイルに保存。 bash diff.sh error.txt
とコマンドを打つ。
上記の手順を踏めば、ActiveAndroidテーブルとRoomテーブルの差分が表示されるので、容易に修正することができます。 またAndroidではSqliteを使用しているので、カラムの成約変更ができないため、tmpテーブルを作成して移行してください。