tsurutanのつぶやき

備忘録としてつぶやきます

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の型として intboolean などのプリミティブ型を指定するとデフォルトで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

使用方法

  1. 上記スクリプトを diff.sh という名前で保存。
  2. jqを使うので、なければbrew install jqコマンドを打ってインストール。
  3. エラーログをerror.txtというファイルに保存。
  4. bash diff.sh error.txt とコマンドを打つ。

上記の手順を踏めば、ActiveAndroidテーブルとRoomテーブルの差分が表示されるので、容易に修正することができます。 またAndroidではSqliteを使用しているので、カラムの成約変更ができないため、tmpテーブルを作成して移行してください。