はじめに (対象読者・この記事でわかること)
本記事は、JavaでAndroidアプリ開発を行っているエンジニア、特にRealmデータベースを利用している方を対象としています。
Realm にデータを書き込んだ直後に、findAll() などで取得した結果が更新前の状態のままになるという現象に遭遇したことはありませんか?この記事を読むことで、以下が理解でき、実際のコードにすぐに適用できるようになります。
- 更新トランザクションが正しくコミットされていないケースの判別方法
- Realm のキャッシュ機構とスレッド間のデータ同期の仕組み
- 具体的なコード例を交えた「最新データを確実に取得する」実装パターン
本記事を書いたきっかけは、社内プロジェクトで同様のバグに長時間苦しんだ経験です。原因究明と対策を整理し、同じ悩みを抱える開発者に少しでも早く解決策を届けたいと思い執筆しました。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Java の基本文法とオブジェクト指向の概念
- Android Studio でのプロジェクト構成と依存関係の設定方法
- Realm の簡単な使い方(Model 定義、
Realm.getInstance()、トランザクションの概念)
問題の概要と背景
Realm は「リアルタイム」かつ「ローカル」なデータベースとして、モバイルアプリ開発者に広く利用されています。その特徴として、Live Objects が挙げられます。Live Objects は、Realm が内部で保持しているキャッシュと同期しながら、常に最新状態を保持しようとしますが、スレッド境界 を跨いだときに意図せず古いスナップショットが返されることがあります。
具体的に問題が起きるシーンは次の通りです。
- UI スレッド で
RealmResultsを取得し、リスト表示に利用 - 別スレッド(例えば
AsyncTaskやExecutorService)でデータを更新 - 更新が完了した直後、UI スレッドで再度同じ Realm オブジェクトを取得すると、更新前のデータ が返ってくる
この挙動は、以下の要因が絡み合って発生します。
-
トランザクションのコミットが非同期
Realm はデフォルトで「自動コミット」モードですが、内部的に書き込みは一時的にバッファに蓄積され、バックグラウンドで確定します。そのため、コミット直後に別スレッドがクエリを走らせても、まだキャッシュが更新されていない可能性があります。 -
スレッドローカルな Realm インスタンス
Realm インスタンスはスレッドごとに独立しており、あるスレッドでの変更は他スレッドのインスタンスに自動的に反映されません。realm.refresh()を明示的に呼び出すか、RealmChangeListenerで変更を購読しなければ、古いスナップショットが残ります。 -
RealmResultsのコピー
copyFromRealm()でオフラインコピーを取得した場合、コピーは immutable であり、以後の更新は反映されません。UI がコピーを保持したまま再描画すると、結果として古い情報が表示され続けます。
以上が典型的な「更新後に古い情報が取得される」現象の根本原因です。次章では、実際に動作を確認しながら確実に最新データを取得できる実装手順をご紹介します。
具体的な対策と実装例
ステップ1 Realm の設定を見直す
まず、Realm のインスタンス生成時に RealmConfiguration を適切に設定します。特に builder().allowWritesOnUiThread(true).deleteRealmIfMigrationNeeded() のようなオプションは開発段階で便利ですが、本番環境では write on UI thread は避ける ことがベストプラクティスです。ここでは、シングルスレッドモデルで安全に動作させる設定例を示します。
JavaRealm.init(context); RealmConfiguration config = new RealmConfiguration.Builder() .name("app.realm") .schemaVersion(1) .migration(new MyMigration()) .build(); Realm.setDefaultConfiguration(config);
ステップ2 更新トランザクションは明示的に executeTransactionAsync を使用
非同期更新を行う際は executeTransactionAsync を使い、完了コールバックで UI スレッドに通知させます。このコールバック内で realm.refresh() を呼び出すと、同スレッドのキャッシュが即座に最新状態に更新されます。
JavaRealm.getDefaultInstance().executeTransactionAsync( realm -> { // 例: Task オブジェクトのステータスを更新 Task task = realm.where(Task.class) .equalTo("id", targetId) .findFirst(); if (task != null) { task.setStatus(Task.Status.COMPLETED); } }, // 成功コールバック(UI スレッドで呼ばれる) () -> { Realm uiRealm = Realm.getDefaultInstance(); uiRealm.refresh(); // キャッシュを即時更新 // UI を再描画 taskAdapter.notifyDataSetChanged(); }, // エラーハンドラ error -> Log.e("RealmUpdate", "更新失敗", error) );
ポイントは refresh() の呼び出しです。これにより UI スレッドの Realm がバックグラウンドで確定した変更を即座に取得できます。
ステップ3 LiveData と RealmChangeListener で自動更新を実装
Realm の変更を自動的に UI に反映させたい場合は、RealmChangeListener か RealmResults を LiveData にラップしたものを利用します。以下は LiveData を使ったサンプルです。
Javapublic class TaskRepository { private final Realm realm = Realm.getDefaultInstance(); public LiveData<RealmResults<Task>> getAllTasks() { MutableLiveData<RealmResults<Task>> liveData = new MutableLiveData<>(); RealmResults<Task> results = realm.where(Task.class).findAllAsync(); results.addChangeListener((data, changeSet) -> { // データが更新されたら LiveData にセット liveData.postValue(data); }); return liveData; } }
findAllAsync() で非同期取得し、addChangeListener が呼ばれるたびに最新データが LiveData に流れ込みます。UI 側は Observer を登録すれば、手動で refresh() を呼ぶ必要がなくなります。
JavataskViewModel.getAllTasks().observe(this, tasks -> { taskAdapter.submitList(realm.copyFromRealm(tasks)); });
ハマった点やエラー解決
1. RealmTransactionException: The Realm is not in a write transaction.
- 原因: UI スレッドで
executeTransactionを呼んだが、allowWritesOnUiThreadが無効だった。 - 対策: 書き込みは必ず
executeTransactionAsyncか、allowWritesOnUiThread(true)の設定を検討。実運用では非同期で行うのが安全。
2. IllegalStateException: Realm accessed from incorrect thread.
- 原因: UI スレッドで取得した
RealmResultsを別スレッドで直接操作した。 - 対策: スレッドごとに
Realm.getInstance()を取得し、データはcopyFromRealm()でディープコピーしたオブジェクトに変換して渡す。
3. データが更新されたのに UI が変わらない
- 原因:
RealmResultsをaddChangeListenerで監視していなかった、またはnotifyDataSetChanged()を忘れていた。 - 対策:
RealmChangeListenerで UI のアダプタに通知、もしくはLiveData経由で自動更新させる。
解決策の総まとめ
- 更新は非同期 (
executeTransactionAsync) で行い、完了時に UI スレッドでrealm.refresh()を呼ぶ。 - LiveData / RealmChangeListener を活用し、データ変更をリアルタイムで UI に反映させる。
- スレッドごとの Realm インスタンス を意識し、必要に応じて
copyFromRealm()でデータを安全に渡す。 - テスト環境での遅延(バックグラウンドの書き込み確定までの時間)を考慮し、
awaitパターンやコールバックで確実に完了を待つ。
これらを組み合わせれば、Realm の更新後に古いデータが取得される問題はほぼ解消できます。
まとめ
本記事では、Java + Realm 環境で「データ更新後に古い情報が取得され続ける」問題の原因と、確実に最新データを取得するための実装パターンを解説しました。
- 原因:トランザクションの非同期コミット、スレッドローカルな Realm、Live Objects のキャッシュ機構
- 対策:
executeTransactionAsync+ コールバックでrealm.refresh()、RealmChangeListenerやLiveDataを用いた自動更新、スレッド安全なデータコピー - ベストプラクティス:UI スレッドでの直接書き込みを避け、非同期更新とリスナで状態同期を行う
これにより、開発者はデータ整合性の不具合に悩むことなく、スムーズに UI を更新できるようになります。次回は、Realm のマイグレーション戦略とパフォーマンスチューニングについて掘り下げる予定です。
参考資料
- Realm Java 公式ドキュメント – Transactions
- Realm Java 公式ガイド – Threading
- Android Architecture Components – LiveData
- 「Realm 入門」 (O'Reilly Japan, 2022)
