Realmのマイグレーションで Configurations cannot be different if used to open the same file. が発生する
スキーマに変更があったので、マイグレーションを行ったら上記のエラーが発生した。
先に言っておくとマイグレーション自体は本質的な問題ではなかった。Realmのインスタンスの生成方法に問題があった。
Realmのバージョンは 0.87.4
を使用。
マイグレーション
サンプルにいくつか方法が紹介されているが次の方法で行った。
// Or you can add the migration code to the configuration. This will run the migration code without throwing // a RealmMigrationNeededException. RealmConfiguration config1 = new RealmConfiguration.Builder(this) .name("default1") .schemaVersion(3) .migration(new Migration()) .build(); realm = Realm.getInstance(config1); // Automatically run migration if needed
この方法だと、何度呼び出しても必要なときのみマイグレーションが行われる。
インスタンス生成
今後もマイグレーションをする場合があると考えて、メソッド化した。
今作っているアプリで、複数の呼び出しタイミングが存在しているため、インスタンスを生成・取得する部分もラッパーを作って、いつ呼び出されてもマイグレーションが正しく実行されるようにした。
public final class RealmManager { public static Realm getRealm(Context context) { return Realm.getInstance(getConfig(context)); } private static RealmConfiguration getConfig(Context context) { RealmConfiguration defaultConfig = new RealmConfiguration.Builder(context) .schemaVersion(1) .migration(new Migration()) .build(); return defaultConfig; } }
マイグレーション以前は
Realm realm = Realm.getInstance(context);
でRealmのインスタンスを取得していた。
java.lang.IllegalArgumentException: Configurations cannot be different if used to open the same file.
これで呼び出すと、上の例外が発生するようになった。
異なるRealmConfiguration
が同じファイルを使えないということなので、 RealmConfiguration
を都度生成しないように、次のように変更した。
public final class RealmManager { private static RealmConfiguration defaultConfig; public static Realm getRealm(Context context) { // configを作り直すとRealmのインスタンスも別のものを作る。 // そのため、同じファイルにアクセスし合うことになりクラッシュする。 if (defaultConfig == null) { defaultConfig = getConfig(context); } return Realm.getInstance(defaultConfig); } private static RealmConfiguration getConfig(Context context) { RealmConfiguration defaultConfig = new RealmConfiguration.Builder(context) .schemaVersion(1) .migration(new Migration()) .build(); return defaultConfig; } }
無事実行できた。
ちょっと納得いかない
これで解決はしたのだが、今まではどうして起きていなかったのか?
潜在バグだったのか確認した。
今までのContext
を渡す呼び出し方は、内部的には次のようになっている。
Realm realm = Realm.getInstance(context);
- Realm.java
/** * Realm static constructor for the default Realm file {@value io.realm.RealmConfiguration#DEFAULT_REALM_NAME}. * This is equivalent to calling {@code Realm.getInstance(new RealmConfiguration(getContext()).build())}. * * This constructor is only provided for convenience. It is recommended to use * {@link #getInstance(RealmConfiguration)} or {@link #getDefaultInstance()}. * * @param context a non-null Android {@link android.content.Context} * @return an instance of the Realm class. * @throws java.lang.IllegalArgumentException if no {@link Context} is provided. * @throws RealmMigrationNeededException if the RealmObject classes no longer match the underlying Realm and it must be * migrated. * @throws RealmIOException if an error happened when accessing the underlying Realm file. */ public static Realm getInstance(Context context) { return Realm.getInstance(new RealmConfiguration.Builder(context) .name(DEFAULT_REALM_NAME) .build()); }
RealmConfiguration
をnewしてるじゃん!
ではどうして今回から起きたのか? 例外を吐く部分は次の箇所。
- RealmCache.java
/** * Make sure that the new configuration doesn't clash with any cached configurations for the * Realm. * * @throws IllegalArgumentException if the new configuration isn't valid. */ private void validateConfiguration(RealmConfiguration newConfiguration) { if (configuration.equals(newConfiguration)) { // Same configuration objects return; } // Check that encryption keys aren't different. key is not in RealmConfiguration's toString. if (!Arrays.equals(configuration.getEncryptionKey(), newConfiguration.getEncryptionKey())) { throw new IllegalArgumentException(DIFFERENT_KEY_MESSAGE); } else { throw new IllegalArgumentException("Configurations cannot be different if used to open the same file. " + "\nCached configuration: \n" + configuration + "\n\nNew configuration: \n" + newConfiguration); } }
メンバ変数のconfiguration
は、1度目に生成したRealmConfiguration
が入っている。
引数のnewConfiguration
は2度目に生成したRealmConfiguration
が入っている。
マイグレーションの記述の有無に関わらず、2度目以降のRealm
インスタンスの取得にはここを通る。
マイグレーションの記述がない場合は、2度目でも同じRealmConfiguration
として扱われ、記述がある場合は異なるRealmConfiguration
として扱われているため、IllegalArgumentException
が発生している。
equalsとhasCodeのoverride
RealmConfiguration#equals
は次のようにoverrideされていた。
RealmConfiguration#hashCode
はequalsの条件を満たすように実装されている。
- RealmConfiguration.java
@Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; RealmConfiguration that = (RealmConfiguration) obj; if (schemaVersion != that.schemaVersion) return false; if (deleteRealmIfMigrationNeeded != that.deleteRealmIfMigrationNeeded) return false; if (!realmFolder.equals(that.realmFolder)) return false; if (!realmFileName.equals(that.realmFileName)) return false; if (!canonicalPath.equals(that.canonicalPath)) return false; if (!Arrays.equals(key, that.key)) return false; if (!durability.equals(that.durability)) return false; if (migration != null ? !migration.equals(that.migration) : that.migration != null) return false; if (!rxObservableFactory.equals(that.rxObservableFactory)) return false; return schemaMediator.equals(that.schemaMediator); }
Builderで設定したschemaVersion
, migration
のみに注目する。
schemaVersion
は単純に数値の比較をして、migration
はnullであるかどうかと、参照先の比較をしています。
schemaVersion
の初期値は0なので、未設定の場合は0
で等しくなります。
migration
は未設定の場合はnullなので等しくなります。
片方だけ異なる値を設定することは他の例外を投げるため、起きないことは保証されています。
なので、今まで例外が起きず、マイグレーションの記述を追加したときに例外が起きたのは、
migration.equals(that.migration)
条件に引っかかっていたようです。
確認のために次のようにインスタンスの生成を行ってみました。
Migration
クラスを2度目でも同じものを使うようにしました。
public final class RealmManager { private static Migration migration = new Migration(); public static Realm getRealm(Context context) { return Realm.getInstance(getConfig(context)); } private static RealmConfiguration getConfig(Context context) { RealmConfiguration defaultConfig = new RealmConfiguration.Builder(context) .schemaVersion(1) .migration(migration) .build(); return defaultConfig; } }
RealmConfiguration
をnewしても無事実行できました。
一応納得
どうして今までは例外になっていなかったのか理解できたので、スッキリしました。
しかし同じファイルにアクセスするのを防ぐためなら、マイグレーションを行わない状態でも同じ例外を吐くようにしてくれればいいのにと思いました。
そうすれば初めからそのように実装するのに。きっと何か理由があるのでしょうが、ひとまずこれで解決!
別解
Migration
クラスのequals
をoverrideするという方法もあるみたいです。
確かにそうすれば
!migration.equals(that.migration)
の条件を気にしなくて良くなります。
Realmの中の人の回答なので、こちらの方が良いかもしれません。