メモ2ブログ

メモtoウェブログ。旧ブログはこちら。 http://sakebook.blogspot.jp/

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 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してるじゃん!

ではどうして今回から起きたのか? 例外を吐く部分は次の箇所。

    /**
     * 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の中の人の回答なので、こちらの方が良いかもしれません。

参考

バグとは / IT用語辞典

realm-java / GitHub

migration quesstion / GitHub