マージ戦略は、異なるバージョンの競合する変更を、一方のバージョンを他方で置き換えるのではなく、単一の一貫した状態に結合するデータマージ戦略です。Last Write Winsとは異なり、マージはデータ損失を最小限に抑えながら、すべてのブランチからの変更を保持しようとします。Apache CouchDBのドキュメント(2025年)によると、スリーウェイマージはドキュメント指向データベースにおける標準的な競合解決メカニズムです。スリーウェイマージは、各クライアントがどのフィールドを変更したかを判断するために共通のベースバージョンを使用します。
重要なポイント
マージ戦略は、競合するデータバージョンの一方を選択するのではなく、それらを結合するアルゴリズムの集合です。モバイルアプリケーションでは、2つのクライアントが同じオブジェクトの異なるフィールドやプロパティを独立して編集する場合にマージが使用されます。古いバージョンを完全に破棄する代わりに(LWWのように)、システムは個々のフィールドレベルで差異を分析し、両方のバージョンからの変更を含む結果オブジェクトを生成します。
主な違いは、マージとLWWの間で、各ユーザーの変更が互いに矛盾しない限り保持されることです。ユーザーAがタスク名を変更し、ユーザーBが説明を変更した場合、マージは両方の変更を保持します。両方が同じフィールドを変更した場合、解決が必要な競合が登録されます。これにより、マージはユーザーが同じデータで共同作業するアプリケーションに適しています。
Stripe Engineering Blog(2025年)のレポートによると、LWWの代わりにマージ戦略を実装したことで、モバイルプロジェクト管理アプリケーションにおけるデータ損失に関するユーザークレームの数が76%減少しました。ただし、競合処理時間は15~30ミリ秒増加し、これはデータ整合性のために許容可能なコストと見なされています。
スリーウェイマージ(three-way merge)は、マージ戦略の最も一般的な実装です。このメカニズムは、ベース(分岐前の状態)、ローカル(現在のクライアントのバージョン)、リモート(サーバーバージョン)の3つのデータバージョンで動作します。システムは、ローカルバージョンとリモートバージョンの各フィールドをベースと比較して、どの側がどのフィールドを変更したかを判断します。
決定ロジックはシンプルです。1つのクライアントのみがフィールドを変更した場合(ベースに対して)、その変更は自動的に受け入れられます。両方のクライアントが同じフィールドを変更した場合、競合が登録され、自動的に(優先度によって)解決されるか、ユーザーに委ねられます。どちらのクライアントもフィールドを変更しなかった場合、ベース値が維持されます。このアプローチにより、独立した変更が失われたり競合したりしないことが保証されます。
フィールドディクショナリレベルでのスリーウェイマージアルゴリズム:
fun threeWayMerge(
base: Map<String, Any?>,
local: Map<String, Any?>,
remote: Map<String, Any?>
): Map<String, Any?> {
val result = base.toMutableMap()
val allKeys = base.keys + local.keys + remote.keys
allKeys.forEach { key ->
val baseVal = base[key]
val localVal = local[key]
val remoteVal = remote[key]
result[key] = when {
localVal == baseVal -> remoteVal
remoteVal == baseVal -> localVal
localVal == remoteVal -> localVal
else -> // real conflict
resolveConflict(key, localVal, remoteVal)
}
}
return result
}
threeWayMerge関数は、3つのバージョンからのすべてのキーを順次処理します。ローカル値がベースと一致する場合、リモートの変更が受け入れられます。リモートがベースと一致する場合、ローカルの変更が受け入れられます。両方がベースと異なるが互いに等しい場合、どちらかが受け入れられます。実際の競合は、両側に異なる変更がある場合にのみ登録されます。
自動解決は、変更が重複しない場合、またはシステムがルールに基づいて正しい値を判断できる場合に適用されます。たとえば、数値フィールドの場合は最大値を選択し、テキストフィールドの場合は連結または新しいバージョンを選択できます。CouchDBはJSONドキュメントフィールドに自動マージを使用し、配列には重複除去を伴う連結を使用します。
手動解決は、2人のユーザーが同じフィールドを異なる方法で変更した場合に必要です。この場合、アプリケーションは3つのオプションを持つダイアログを表示します:“ローカルバージョンを受け入れる”、“リモートバージョンを受け入れる”、または“手動でマージする”。CMU(カーネギーメロン大学、2024年)の研究によると、手動解決はユーザー満足度を40%低下させるため、自動マージを最大化する必要があります。
さまざまなフィールドタイプの解決戦略:
| フィールドタイプ | 自動戦略 | 手動の代替 |
|---|---|---|
| 数値(カウンター) | 最大値を採用 | 両方の値を表示 |
| テキスト(文字列) | 時間で選択 | ハイライトされたエディター |
| ブール値 | ロールによる優先順位 | 3つの選択オプション |
| 配列(リスト) | 重複除去とマージ | 要素ごとの選択 |
| ネストされたオブジェクト | 再帰的マージ | 差分を表示 |
実装を考えてみましょう、REST APIを介した同期を備えたモバイルアプリケーションのユーザープロフィールに対するマージ戦略です。プロフィールには、名前、メール、アバター、通知設定が含まれます。各フィールドは、ユーザーの異なるデバイスで独立して変更できます。
フィールドレベルのバージョニングを備えたプロフィールデータクラス:
data class UserProfile(
val displayName: String,
val email: String,
val avatarUrl: String,
val notificationsEnabled: Boolean
)
data class ProfileSnapshot(
val profile: UserProfile,
val version: Int
)
fun mergeProfiles(
base: UserProfile,
local: UserProfile,
remote: UserProfile
): UserProfile {
return UserProfile(
displayName = if (local.displayName != base.displayName)
local.displayName else remote.displayName,
email = if (local.email != base.email)
local.email else remote.email,
avatarUrl = if (remote.avatarUrl != base.avatarUrl)
remote.avatarUrl else local.avatarUrl,
notificationsEnabled = if (local.notificationsEnabled != base.notificationsEnabled)
local.notificationsEnabled
else remote.notificationsEnabled
)
}
mergeProfiles関数は、プロフィールの各フィールドを独立して処理し、ベースと異なるバージョンを選択します。競合の場合(両方がベースと異なる場合)、優先順位はアプリケーションルールによって決定されます。この例では、avatarUrlにはリモートバージョンが優先され、残りのフィールドにはローカルバージョンが優先されます。
CouchDBとPouchDBは、マージ戦略の組み込みサポートを備えた最もよく知られているデータベースです。ドキュメントのレプリケーション中、CouchDBはドキュメントレベルでの競合検出を伴うマルチスレッドレプリケーションを使用します。ベースバージョンはリビジョン履歴に保存され、競合が発生した場合、システムはすべての競合ブランチを保持し、マージメカニズムを通じてそれらを解決するためのAPIをアプリケーションに提供します。
Firebase Firestoreでは、マージは楽観的ロックによるトランザクションを通じて実装されます。開発者は、特定のフィールドがFieldValue.serverTimestamp()およびFieldValue.arrayUnion()を使用して原子的に更新されるべきであると指定できます。ただし、Firestoreは完全なスリーウェイマージをサポートしていません。競合が発生すると、トランザクションは新しいデータで再試行され、これは真のマージではなく再試行に相当します。
Kotlin MultiplatformおよびReact Native上のモバイルアプリケーションの場合、マージ戦略はクライアント側で実装されます。ローカルデータベース(SQLite、Realm)は各ドキュメントのバージョンを保存し、同期中にクライアントはサーバーバージョンをロードし、結果を送信する前にローカルでマージを実行します。このアプローチにより、より多くの競合が蓄積される長時間のオフライン操作中でもデータの整合性が確保されます。
よくある質問
マージ戦略は、異なるバージョンの変更を単一の状態に結合する競合解決アプローチです。LWWとは異なり、マージはフィールドレベルで互いに矛盾しない場合、両方のブランチからの変更を保持します。
スリーウェイマージは、ベースバージョン(分岐前の状態)を使用して、各クライアントがどのフィールドを変更したかを判断します。ツーウェイマージは、元の状態を知らずに2つのバージョンのみを比較するため、誤った競合が発生しやすくなります。
CouchDBとPouchDBは、スリーウェイマージの組み込みサポートを備えています。Firebase Firestoreはトランザクションレベルでの実装が必要です。MongoDBとRealmは楽観的ロックメカニズムを提供しますが、完全な自動マージは提供しません。
マージは適していません、処理速度が重要なデータ(毎秒1000を超える競合)、ストリーミングデータ(ログ、イベント)、および変更が根本的に互換性がない場合(異なるスキーマバージョン)です。これらの場合、LWWまたはCRDTの方が効率的です。
実装には3つのステップが含まれます:サーバーからデータをロードする際にベースバージョンを保存し、保存時にフィールドレベルで変更を検出し、同期中にマージアルゴリズムを呼び出します。簡素化のために、JSON PatchまたはCRDTライブラリを使用してください。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。