Android
Kotlin
Realm
Dagger2

RealmのinitalDataでハマったので覚書

はじめに

DaggerでRealmを使うために@Provideアノテーションつけて、Realmのオブジェクトを初期化しようとした。

ApplicationModule.kt
    @Provides
    fun provideRealm(application: MainApplication): Realm {
        val builder = RealmConfiguration.Builder().name(APP_KEY)
        return if (BuildConfig.DEBUG) {
            Realm.getInstance(builder.deleteRealmIfMigrationNeeded().build())
        } else {
            Realm.getInstance(builder.build())
        }
    }

で、このとき、旧SQLiteのDBからRealmへデータ引き継ぎ処理もしたかったのでinitialDataを使うことで解決しようとした。

ApplicationModule.kt
    @Provides
    fun provideRealm(application: MainApplication): Realm {
        val builder = RealmConfiguration.Builder().name(APP_KEY)
        builder.initialData { /*引き継ぎ処理*/ } //追加
        return if (BuildConfig.DEBUG) {
            Realm.getInstance(builder.deleteRealmIfMigrationNeeded().build())
        } else {
            Realm.getInstance(builder.build())
        }
    }

そしたら以下のようなエラーが発生した

java.lang.RuntimeException: java.lang.IllegalArgumentException: Configurations cannot be different if used to open the same file. 
    Cached configuration: 

異なるConfigurationではひらけねーよ、とのこと。

原因

エラーログをたどっていくと、最終的に以下の処理にたどり着いた

RealmCache.java
    private void validateConfiguration(RealmConfiguration newConfiguration) {
        if (configuration.equals(newConfiguration)) {
            // Same configuration objects.
            return;
        }

// 中略

            throw new IllegalArgumentException("Configurations cannot be different if used to open the same file. " +
                    "\nCached configuration: \n" + configuration +
                    "\n\nNew configuration: \n" + newConfiguration);
        }
    }

どうやら最初のequal比較でfalseになるとダメっぽい。
で、RealmConfigurationのequalを見ると、overrideされて色々してることがわかった。

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 (readOnly != that.readOnly) { return false; }
        if (isRecoveryConfiguration != that.isRecoveryConfiguration) { return false; }
        if (realmDirectory != null ? !realmDirectory.equals(that.realmDirectory) : that.realmDirectory != null) {
            return false;
        }
        if (realmFileName != null ? !realmFileName.equals(that.realmFileName) : that.realmFileName != null) {
            return false;
        }
        if (!canonicalPath.equals(that.canonicalPath)) { return false; }
        if (assetFilePath != null ? !assetFilePath.equals(that.assetFilePath) : that.assetFilePath != null) {
            return false;
        }
        if (!Arrays.equals(key, that.key)) { return false; }
        if (migration != null ? !migration.equals(that.migration) : that.migration != null) {
            return false;
        }
        if (durability != that.durability) { return false; }
        if (!schemaMediator.equals(that.schemaMediator)) { return false; }
        if (rxObservableFactory != null ? !rxObservableFactory.equals(that.rxObservableFactory) : that.rxObservableFactory != null) {
            return false;
        }
        if (initialDataTransaction != null ? !initialDataTransaction.equals(that.initialDataTransaction) : that.initialDataTransaction != null) {
            return false;
        }
        return compactOnLaunch != null ? compactOnLaunch.equals(that.compactOnLaunch) : that.compactOnLaunch == null;
    }

探ってくと、initialDataTransactionに関する条件式が見つかる。
読んで見るとどうやら単純なオブジェクト比較をしているだけっぽい。

で、自分の実装を振り返って見ると、ラムダ式で書いてしまっているため、処理のたびにインスタンスが生成されてしまっている。

kotlin
    @Provides
    fun provideRealm(application: MainApplication): Realm {
        val builder = RealmConfiguration.Builder().name(APP_KEY)
        builder.initialData { /*初期化処理*/ } //ラムダ式なので毎回異なるインスタンス
        return if (BuildConfig.DEBUG) {
            Realm.getInstance(builder.deleteRealmIfMigrationNeeded().build())
        } else {
            Realm.getInstance(builder.build())
        }
    }

なので、同一インスタンスを指し示してあげればOKという結論に至る。

結論

てなわけで、直したコードは以下のとおり。

    @Provides
    fun provideRealm(transaction: Realm.Transaction): Realm {
        val builder = RealmConfiguration.Builder().name(APP_KEY)
        builder.initialData(transaction)
        return if (BuildConfig.DEBUG) {
            Realm.getInstance(builder.deleteRealmIfMigrationNeeded().build())
        } else {
            Realm.getInstance(builder.build())
        }
    }

    @Provides
    fun provideTransaction(application: MainApplication) = application.transaction
MainApplication
class MainApplication : DaggerApplication() {
    val transaction: Realm.Transaction by lazy { 
    Realm.Transaction { /* 初期化処理 */}
 }

SQLiteを使う関係上、どうしてもContextオブジェクトが必要になったので、Transactionの実装自体はApplicationクラスに持って行った。もっと上手い方法があるかもしれないけど、まぁ、いいかという感じ。