フィールドアプリでのオフライン優先ストレージ:SQLite と Realm の比較
フィールドアプリ向けに SQLite と Realm のオフライン優先ストレージを比較。マイグレーション、クエリ、コンフリクト処理、デバッグツール、実践的な選び方のヒントを解説します。

オフライン優先のフィールドアプリに本当に必要なもの
「オフラインで動く」だけがオフライン優先の意味ではありません。必要なデータを読み込み、新しい入力を受け取り、すべての編集を安全に保持して同期できるようにすることが本質です。
フィールド作業は予測できる制約をいくつかもたらします:電波が抜けたり入ったりする、作業セッションが長い、端末は古いことがある、バッテリーセーバーが有効なことが多い。人は素早く動きます。作業を開き、長いリストをスクロールし、写真を撮り、フォームに記入し、次の作業へと何も考えずに移ります。
ユーザーが気にするのは単純です。編集が消えると信頼を失いますし、オフラインでリストや検索が遅いと不満になります。「自分の作業は保存されていますか?」に明確に答えられないと不安になりますし、再接続後にレコードが重複したり消えたり、アップデートでおかしな挙動が出ると使えません。
だから、SQLite と Realm のどちらを選ぶかはベンチマークよりも日々の振る舞いが重要です。
ローカルデータベースを選ぶ前に、次の4点をはっきりさせてください:データモデルは変わる、クエリは実際のワークフローに合う必要がある、オフライン同期ではコンフリクトが生じる、そしてツールが現場の問題解決の速さを左右します。
1) データは変わる
安定して見えるアプリでも進化します:新しいチェックボックス、名称変更、新画面。モデル変更がつらいと、改善を出さなくなるか、実際のデバイスでデータを壊すリスクがあります。
2) クエリは実際のワークフローに合うこと
フィールドアプリには「今日の作業」「近くの現場」「未同期のフォーム」「過去2時間に編集された項目」などの高速フィルタが必要です。データベースがそれらのクエリを扱いにくいと、UI が遅くなるかコードが迷路になります。
3) オフライン同期はコンフリクトを生む
二人が同じレコードを編集する、あるいは一つの端末が古いデータを何日も編集する、ということが起きます。どちらが勝つのか、どうマージするのか、いつ人の判断が必要かを決めておく必要があります。
4) ツールが重要
現場で問題が起きたとき、データを調べ、再現し、何が起きたかを推測せずに理解できることが重要です。
マイグレーション:ユーザーを壊さずにデータモデルを変える
フィールドアプリは止まりません。数週間後にチェックボックスを追加したり、ステータス名を変えたり、"notes" を構造化フィールドに分割したりします。マイグレーションはオフラインアプリが失敗しやすい箇所です。電話に既に実データがあるからです。
SQLite はテーブルとカラムにデータを保存します。Realm はプロパティを持つオブジェクトとして保存します。この違いはすぐに顕著になります:
- SQLite では通常、明示的なスキーマ変更(ALTER TABLE、テーブル追加、データコピー)を書きます。
- Realm ではスキーマバージョンを上げ、アクセスされたオブジェクトを更新するマイグレーション関数を走らせることが多いです。
フィールド追加はどちらでも簡単です:SQLite ではカラム追加、Realm ではデフォルトを持つプロパティ追加。問題になるのはリネームや分割です。SQLite ではセットアップによってはリネームが制限されるため、新しいテーブルを作ってデータをコピーすることが多くなります。Realm では古いプロパティを読み、新しいプロパティに書き込む形でマイグレーションできますが、型やデフォルト、null の扱いに注意が必要です。
端末上の大量データを一度に書き換えるマイグレーションは慎重に計画してください。古い端末では全レコードを書き換えると時間がかかり、駐車場で技術者がスピナーを見続ける羽目になります。マイグレーション時間を見積もり、重い変換は複数リリースに分けることを検討してください。
マイグレーションを公平にテストするには、同期と同じ扱いにしてください:
- 古いビルドをインストールして現実的なデータを作り、アップグレードする。
- 小さなデータセットと大きなデータセットの両方をテストする。
- マイグレーション中にアプリを強制終了して再起動する。
- 低ストレージ環境をテストする。
- ロールバックできない前提で前方に進めることを想定する。
例:"equipmentId" を "assetId" にしてからさらに "assetType" と "assetNumber" に分割する場合、古い検査データが使えなくなるようなログアウトやデータ消去を強いるべきではありません。
クエリの柔軟性:データに何を尋ねられるか
フィールドアプリは一覧画面で生き残ります:今日の作業、近隣資産、未解決の顧客チケット、今週使った部品。ストレージはこれらの問いを簡単に表現でき、速く実行でき、6か月後でも読みやすい状態であるべきです。
SQLite は SQL を提供し、大量データのフィルタやソートに柔軟です。条件を組み合わせ、テーブルを結合し、グループ化し、画面が遅くなったらインデックスを追加できます。 "Region A の資産に紐づく検査で Team 3 に割り当てられ、チェックリストに失敗項目があるもの" のようなクエリは SQL で綺麗に表現できます。
Realm はオブジェクトと高レベルのクエリ API に寄ります。多くのアプリでは自然に感じられるでしょう:Job オブジェクトをクエリしてステータスで絞り、期限でソートし、関連オブジェクトを辿ります。トレードオフは、特に複数の関係を跨ぐレポート系のクエリが SQL に比べて表現しにくかったり、必要なクエリに合わせてデータ形状を変える必要が出ることです。
検索とリレーション
ジョブタイトル、顧客名、住所のように複数フィールドで部分一致検索をしたい場合、SQLite は注意深いインデックス設計か専用の全文検索アプローチに導きます。Realm でもテキストでフィルタできますが、パフォーマンスや「contains」の意味を大規模でも考える必要があります。
リレーションも実務上の痛点です。SQLite は結合テーブルで 1 対多や多対多を扱い、「これら2つのタグが付いた資産」のようなパターンが扱いやすいです。Realm のリンクはコード内で辿りやすい一方、多対多や"クエリを通過する"パターンは読みを速く保つための追加設計を要することが多いです。
生のクエリ vs メンテナンスのしやすさ
メンテしやすいパターンは、画面とレポートに直接対応する少数の名前付きクエリを保持することです:主要リストのフィルタとソート、詳細ビュー(単一レコード+関連レコード)、検索定義、いくつかのカウンタ(バッジやオフライン合計)、エクスポート/レポート用クエリなど。
ビジネスからのアドホックな質問が頻繁に来るなら、SQLite の生クエリの力は強力です。多くのデータアクセスを通常のオブジェクト操作のように書きたいなら、Realm は速く作れることが多いですが、最も難しい画面に対応できるかを確認してください。
コンフリクト解決と同期:得られるサポート
オフライン優先のフィールドアプリは、切断時にも同じ基本操作をサポートします:レコードの作成、更新、誤りの削除。ローカル保存自体が難しいわけではありません。難しいのは、同期前に二つの端末が同じレコードを変更したときに何が起きるかを決めることです。
コンフリクトは単純な状況で現れます。技術者が地下室で検査をタブレットに記録し、後で監督が同じ検査をラップトップで修正する。両者が再接続するとサーバーに二つの別バージョンが届きます。
大抵のチームは次のどれかに落ち着きます:
- 最終書き込みが勝つ(速いが有効なデータを静かに上書きするリスクあり)
- フィールド単位でマージ(異なるフィールドが変わる場合は安全だがルールが必要)
- 手動レビューキュー(最も遅いが高リスク変更に最適)
SQLite は信頼できるローカルデータベースを提供しますが、同期自体は提供しません。通常は保留中の操作を追跡し、API に送信し、安全に再試行し、サーバー側でコンフリクトルールを適用する仕組みを自分で作ります。
Realm はその同期機能を使えば配線量を減らせることがあります。オブジェクトと変更追跡を中心に設計されているためです。しかし「組み込みの同期」でもビジネスルールは自動で決まりません。どれをコンフリクトと見なすか、どのデータを勝たせるかはあなたが決めることです。
最初から監査トレイルを設計してください。フィールドチームは「誰がいつどのデバイスで何を変えたか」を明確に知る必要があります。最後に書いたものを勝たせるにしても、ユーザーID、デバイスID、タイムスタンプ、可能なら理由などのメタデータを保存してください。要件を素早く反復したいなら、AppMaster のようなノーコードプラットフォームでバックエンドを素早く作り、ルールを早期に試せると後で楽になります。
デバッグと検査:現場が壊れる前に問題を発見する
オフラインのバグは、サーバーとの会話を見られないときに起きるため難しいです。デバッグ体験は「端末上の状態とその変化をどれだけ簡単に見られるか」にかかっています。
SQLite はファイルなので検査が簡単です。開発や QA ではテスト機からデータベースを取り出し、一般的な SQLite ツールで開いて ad-hoc クエリを実行し、テーブルを CSV や JSON にエクスポートできます。これで「どの行が存在するか」と「UI が表示しているもの」が一致するかを確認できます。欠点はスキーマや結合、マイグレーションの足場を理解する必要があることです。
Realm はアプリらしい検査感があります。データはオブジェクトとして保存され、Realm のツールはクラス、プロパティ、リレーションをブラウズするのに向いています。オブジェクトグラフの問題(リンク切れ、予期しない null)を発見するのに優れますが、SQL ベースの検査に慣れたチームには ad-hoc 分析の柔軟性が劣ることがあります。
オフライン問題のログと再現
フィールドでの失敗は、静かな書き込みエラー、部分的な同期バッチ、マイグレーションの半端な完了が原因であることが多いです。どちらにせよ次を投資してください:レコードごとの「最終変更」タイムスタンプ、端末側の操作ログ、マイグレーションやバックグラウンド書き込み周りの構造化ログ、QA ビルドで冗長ログを有効にする方法、赤字を取り除いたスナップショットをエクスポート・共有する機能。
例:技術者が完了済みの検査がバッテリー切れ後に消えたと報告した場合、共有されたスナップショットでレコードが書かれていなかったのか、書かれているがクエリされていないのか、起動時にロールバックされたのかを確認できます。
失敗スナップショットの共有
SQLite では .db ファイル(と WAL ファイル)を共有するのが簡単です。Realm では Realm ファイルとサイドカーファイルを共有します。いずれの場合も、端末から何かを外に出す前に機微なデータを取り除く再現可能なプロセスを定義してください。
実運用での信頼性:失敗、リセット、アップグレード
フィールドアプリはありふれた理由で失敗します:保存中にバッテリーが切れる、OS がバックグラウンドでアプリを殺す、写真やログでストレージが埋まる。ローカルデータベースの選択は、それらの失敗がどのくらい頻繁に作業の消失につながるかに影響します。
クラッシュで書き込み途中になっても、適切に使えば SQLite も Realm も安全です。SQLite は変更をトランザクションでラップすると信頼できます(WAL モードは回復性とパフォーマンスに有利)。Realm は書き込みがデフォルトでトランザクショナルなので、追加作業なしに「全部かゼロか」の保存が得られます。一般的なリスクはデータベースエンジン自体ではなく、複数ステップで書き込みを行いコミットポイントがないアプリコードです。
破損は稀ですが、回復プランは必要です。SQLite なら整合性チェックを走らせ、既知の良いバックアップから復元するかサーバー再同期で再構築できます。Realm でファイルが破損した場合、実務的な回復は「ローカルを捨てて再同期する」になることが多く、サーバーがソースオブトゥルースであるなら問題ありませんが、端末にのみあるユニークなデータがある場合は痛手です。
ストレージの膨張も驚きになります。SQLite は削除後に VACUUM しないと肥大化することがあります。Realm も成長しうるのでコンパクションポリシーや古いオブジェクト(完了済みの作業など)の削除を考えておく必要があります。
アップグレードとロールバックも落とし穴です。アップデートでスキーマやストレージ形式が変わると、ロールバックしたユーザーが新しいファイルを読めなくなります。アップグレードは片道だと想定し、安全なマイグレーションと「ローカルデータをリセット」する選択肢(アプリを再インストールする手段ではない)を用意してください。
信頼性に効く習慣:
- 「ディスクがいっぱい」や書き込み失敗に対して明確なメッセージと再試行経路を用意する。
- 長いフォームは終わりだけで保存せず、チェックポイントで保存する。
- 回復とサポートのための軽量なローカル監査ログを保持する。
- データベースが大きくなりすぎないよう古いレコードを剪定・アーカイブする。
- 低スペック端末での OS アップグレードやバックグラウンドキルをテストする。
例:チェックリストと写真を保存する検査アプリは1か月でストレージが逼迫する可能性があります。アプリが早期に空き領域を検知できれば、写真撮影を止めたり、可能なときにアップロードしたり、チェックリスト保存を安全に保てます。どのローカルデータベースを使っていても同様です。
ステップバイステップ:ストレージ選択とセットアップ方法
ストレージはライブラリの選択ではなく製品の一部として扱ってください。最良の選択は、電波が落ちたときでもアプリが使えること、再接続時に予測可能に動くことです。
シンプルな意思決定の流れ
まずオフライン時のユーザーフローを書き出してください。具体的に:「今日の作業を開き、ノートを追加し、写真を添付し、完了にし、署名を取る」。リストのすべてがネットワークなしで毎回動作する必要があります。
次に短い手順で検討します:オフラインで重要な画面とそれぞれに必要なデータ量(今日の作業と全履歴の違い)、最小限のデータモデルと偽れないリレーション(Job -> ChecklistItems -> Answers のような)、エンティティごとのコンフリクトルール、障害のテスト方法(実機でのマイグレーション、同期再試行、強制ログアウト/再インストールの挙動)、そして現実的なデータで小さなプロトタイプを作り時間を測る(読み込み、検索、保存、1日オフライン後の同期)。
このプロセスで本当の制約が見えてきます:柔軟なアドホッククエリと簡単な検査が必要か、オブジェクトベースのアクセスと厳格なモデルを優先するか。
プロトタイプで検証すること
技術者がオフラインで30件の検査を完了し、その後カバレッジのある場所に戻るような現実的なシナリオを使って検証します。5,000件での初回読み込み時間、スキーマ変更がアップデートで生き残るか、再接続後にどれだけコンフリクトが出るか、それぞれを説明できるか、そして問題のあるレコードをサポートがどれだけ速く検査できるかを測ってください。
ワークフローを早く固めたいなら、AppMaster のようなノーコードのプロトタイプでバックエンドと管理画面、モバイルアプリを早期に作り、オンデバイスのデータベース選択の前にワークフローとデータモデルを固めるのが有効です。
オフライン優先アプリでよくある失敗
多くの失敗はデータベースエンジン自体ではなく、面倒な基本を省くことに起因します:アップグレード、コンフリクトルール、明確なエラーハンドリング。
1つの落とし穴はコンフリクトが稀だと仮定することです。フィールド作業では普通に起きます:二人の技術者が同じ資産を編集する、監督がチェックリストを変えるなど。ルール(最後に書いたものが勝つ、フィールド単位でマージ、両方を保持してレビュー)を定義していないと、本物の作業が上書きされます。
もう一つの失敗はデータモデルを「完成」と扱い、アップグレードの練習をしないことです。スキーマ変更は小さなアプリでも起きます。スキーマにバージョンを付け、古いビルドからのアップグレードをテストしないと、ユーザーはアップデート後にマイグレーション失敗や真っ白な画面に遭遇します。
パフォーマンス問題も後で出がちです。チームが「念のため全部ダウンロード」してしまい、検索が遅くなりミッドレンジ端末でアプリ起動に何分もかかることに驚きます。
気をつけるパターン:
- 書面化されたコンフリクトポリシーがないため編集が静かに上書きされる。
- 新規インストールでは動くが現実のアップグレードで失敗するマイグレーション。
- 無制限に増えるキャッシュでクエリが遅くなるオフラインキャッシング。
- スピナーの裏に隠れる同期失敗でユーザーが送信済みだと誤解する。
- 推測でのデバッグではなく、再現可能な repro スクリプトとサンプルデータで検査すること。
例:技術者がオフラインで検査を完了し、同期ボタンを押したが確認が出ない。実際には認証トークンの問題でアップロードが失敗しているのに、アプリがエラーを隠してユーザーは作業が完了したと勘違いする、というケースです。
どのストレージを選んでも、次をやってください:機内モード、低バッテリー、アプリアップデート、2台で同じレコードを編集、を組み合わせた「フィールドモード」テストを実行すること。AppMaster のようなノーコードで速く作る場合も、これらのテストをプロトタイプ段階に組み込みましょう。
コミット前のクイックチェックリスト
ストレージエンジンを選ぶ前に、あなたのフィールドアプリで「良い」とは何かを定義し、実データと実機でテストしてください。チームは機能で議論しますが、ほとんどの失敗は基本から来ます:アップグレード、遅い画面、曖昧なコンフリクトルール、ローカル状態を検査する方法の欠如。
ゲートとしてこれを使ってください:
- アップグレードを証明する:少なくとも2つ以上の古いビルドから今日のビルドにアップグレードして、データが開き、編集され、同期できることを確認する。
- コア画面を実データ量で高速に保つ:現実的なデータで最も遅い画面の時間をミッドレンジ端末で測る。
- レコード種ごとにコンフリクトポリシーを書く:検査、署名、使用部品、コメントなど。
- ローカルデータの検査とログ収集を可能にする:サポートと QA がオフライン時に状態を取得する方法を定義する。
- 回復を予測可能にする:いつキャッシュを再構築するか、再ダウンロードか、再ログインが必要かを決める。アプリの再インストールだけが解決策にならないようにする。
AppMaster でプロトタイピングするなら、同じ規律を適用してください。アップグレードをテストし、コンフリクトを定義し、ダウンタイムが許されないチームに出す前に回復を練習してください。
例シナリオ:電波が不安定な検査アプリ
フィールド技術者は朝、50件の作業指示を端末にダウンロードします。各ジョブには住所、必要なチェックリスト項目、参考写真が含まれます。その後、1日中電波が抜けたり入ったりします。
訪問中、技術者は同じ数件のレコードを何度も編集します:ジョブのステータス(Arrived、In Progress、Done)、使用した部品、顧客署名、新しい写真。編集は小さく頻繁なもの(ステータス)と、大きく失われてはいけないもの(写真)があります。
同期の瞬間:二人が同じジョブに触った
11:10 に技術者がジョブ #18 を Done にし、署名を添えてオフラインで保存します。11:40 にディスパッチャーがオフィスからジョブ #18 を再割り当てします。技術者が 12:05 に再接続すると、アプリは変更をアップロードします。
良いコンフリクトフローはこれを隠しません。分かりやすく表示します。スーパーバイザーには「ジョブ #18 の二つのバージョンがあります」とシンプルに示し、主要フィールドを並べて(ステータス、割り当て技術者、タイムスタンプ、署名の有無)選択肢を明確にします:フィールドの更新を保持、オフィスの更新を保持、またはフィールド単位でマージ。
ここでストレージと同期の判断が現実に表れます:変更履歴を追跡できるか、何時間もオフラインだった後で安全に再生できるか。
ジョブが「消えた」場合、デバッグは何が起きたかを証明する作業です。次をログに残してください:ローカルレコードID とサーバーID のマッピング(作成時を含む)、すべての書き込みのタイムスタンプ/ユーザー/デバイス、同期試行とエラーメッセージ、コンフリクト判断と勝者、写真アップロード状況はジョブレコードとは別に追跡すること。
これらのログがあれば、クレームから推測するのではなく再現して原因を突き止められます。
次のステップ:素早く検証し、フルのフィールドソリューションを作る
SQLite と Realm の議論に入る前に、1ページのオフラインフロースペックを書いてください:技術者が見る画面、端末に置くデータ、接続がなくても動かなければならない操作(作成、編集、写真、署名、キュー化されたアップロード)。
次にシステム全体を早期にプロトタイプしてください。オフライン優先アプリは継ぎ目で失敗します:モバイルフォームがローカルに保存しても、管理チームがレコードをレビューして修正できないと意味がありませんし、バックエンドが後で更新を拒否することもあります。
実務的な検証プラン:
- 薄いエンドツーエンドのスライスを作る:1つのオフラインフォーム、1つのリストビュー、1回の同期試行、1つの管理画面。
- 変更テストを行う:フィールド名をリネームする、1つのフィールドを2つに分割する、テストビルドを配りアップグレードの挙動を観察する。
- コンフリクトをシミュレートする:2台で同じレコードを編集し、異なる順で同期して何が壊れるかを記録する。
- フィールドでのデバッグを練習する:実機でローカルデータ、ログ、失敗した同期ペイロードを検査する方法を決める。
- リセットポリシーを書く:ローカルキャッシュをいつ消すか、ユーザーが作業を失わずに回復する方法。
スピードが重要なら、最初にノーコードで試作してワークフローを固めると良いです。AppMaster はバックエンドサービス、ウェブ管理パネル、モバイルアプリを早期に構築し、要件が変わっても生成されたソースコードを更新できる一例です。
リスクに応じて次の検証ステップを選んでください。フォームが毎週変わるならマイグレーションを優先し、複数人が同じジョブに触るならコンフリクトを優先し、「オフィスでは動いた」は信じられないならフィールドデバッグワークフローに注力してください。
よくある質問
オフライン優先とは、接続がないときでもアプリが有用であることを意味します。必要なデータを読み込み、新しい入力を受け取り、同期可能になるまで変更を安全に保持します。重要なのは、電波が切れても、OSにアプリが殺されても、バッテリーが途中で切れても、ユーザーが作業を失わないことです。
複雑なフィルタ、レポート系クエリ、多対多の関係、そして汎用ツールでのアドホックな検査が必要なら SQLite が無難な選択です。オブジェクト志向で書きやすく、トランザクション書き込みがデフォルトで欲しいなら Realm が合うことが多いです。ただし、Realm を選ぶならクエリ要件を Realm の得意分野に合わせる必要があります。
マイグレーションをワンタイム作業ではなくコア機能として扱ってください。古いビルドをインストールして実機に現実的なデータを作り、アップグレードしてアプリが開き、編集し、同期できることを確認します。大容量データ、低ストレージ、マイグレーション中にアプリを強制終了するケースもテストしましょう。
どちらのシステムでもフィールドの実機上で危険なのはリネームや分割です。フィールド追加は比較的簡単ですが、名前の変更やフィールドを分ける作業は注意深く計画し、妥当なデフォルト値を決め、null を扱い、古いデータを一度に全件書き換えないように配慮してください。
現場で重要なのはリスト画面とフィルタです:「今日の作業」「未同期フォーム」「過去2時間に編集された項目」などが速く返ること。これらのクエリを表現するのが難しいなら、UI が遅くなるか、保守困難なコードになります。
SQLite でも Realm でもコンフリクトを自動的に“解決”するわけではありません。エンティティごとに明確な方針を決めてください(最後に書いたものが勝つ、フィールド単位でマージ、または手動レビュー)。アプリは同じレコードを2台が変更したときに何が起きたか説明できるようにしておくべきです。
サポートが「作業が消えた」を診断できるように、再現と再生に必要なメタデータを残してください:ユーザーID、デバイスID、タイムスタンプ、各レコードの「最終変更」マーカー。端末側の操作ログを残し、何がキューに入っていたか、何が送信されたか、何が失敗したかを追えるようにします。
SQLite はファイルとして取り出して直接クエリできるため、現地の機器での突発的な検査に便利です。Realm はオブジェクトグラフのブラウズがしやすく、リンクやプロパティの確認には向いていますが、SQL に慣れたチームにはアドホック分析の柔軟性が劣るかもしれません。
ローカルデータベースが良くても、データ損失はアプリロジックに起因することが多いです:複数ステップの書き込みにコミットポイントがない、同期失敗を隠す、ディスクフルや破損時の回復策がない、などです。トランザクションやチェックポイント、明確な保存と同期の状態表示、予測可能な「ローカルデータのリセット」オプションを用意してください。
現実的なシナリオを1つ作り、計測して検証してください:何千件の初回読み込み、検索、長いフォームの保存、1日オフライン後の同期など。少なくとも2つ古いビルドからのアップグレード、2台でのコンフリクト、現地でのログとローカル状態の検査ができることを確認します。


