コンテンツへスキップ

ガード付き適用

GuardedApplierは、対象データベースが想定されるライブスナップショットと一致する場合に限り、競合のないMergeResultを適用します。マージ計画を事前に確認しておき、適用までにライブ行が変更されている可能性がある場合に便利です。

一般的な構成では、サンドボックスで編集した内容をライブデータベースに反映します。baseはサンドボックスのコピー元となった時点のライブ、oursは編集済みサンドボックス、theirsは現在のライブです。対象はライブデータベースなので、想定されるライブスナップショットはtheirsであり、書き込む操作はライブにまだ存在しないものです(操作の選択を参照)。

PHP
use Merql\Connection;
use Merql\Merql;
use Merql\Plan\OperationSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;

Merql::init(Connection::sqlite('/data/live.sqlite'));

$plan = Merql::plan('promote-1', 'base', 'ours', 'theirs');

// Skip rows with unresolved conflicts.
$conflicted = [];
foreach ($plan->conflicts as $conflict) {
    $conflicted[$conflict->table . "\0" . $conflict->rowKey] = true;
}

$ids = [];
foreach ($plan->operations as $operation) {
    $missing = $operation->source === 'ours'
        || ($operation->source === 'merged' && $operation->type === 'update');

    if ($missing && !isset($conflicted[$operation->table . "\0" . $operation->rowKey])) {
        $ids[] = $operation->id;
    }
}

$selected = (new SelectedMergeResultFactory())->fromOperationSelection(
    $plan,
    OperationSelection::fromIds($ids),
    SnapshotStore::load('base'),
);

$applied = Merql::applyGuarded($selected, SnapshotStore::load('theirs'));

if ($applied->hasErrors()) {
    foreach ($applied->errors() as $error) {
        echo $error . "\n";
    }
}

Merql::applyGuarded()はMerql::init()に渡されたデータベースに書き込みます。想定されるライブスナップショットは同じデータベースの状態を表す必要があります。対象から取得し、事前条件が意味を持つ程度に新しいスナップショットを使ってください。

事前条件

ガード付きSQLでは、生成される文にライブ行の事前条件が追加されます:

  • 更新では、対象行の識別情報が存在し、非識別列の値が想定されるライブ行と一致する必要があります。
  • 削除では、対象行の識別情報が存在し、非識別列の値が想定されるライブ行と一致する必要があります。
  • 挿入では、対象の識別情報がまだ存在していない必要があります。

いずれかの文が影響する行数が0の場合、merqlは計画が古くなったと判断します。トランザクションはロールバックされ、返されたApplyResultにはエラーOptimistic precondition failed for guarded merge statement.が含まれます。

MySQLでは、値がすでに現在の行と同じ更新は、変更された行数を数えるMySQLの仕様により、0行に影響します。そのため、行が一致していても古いデータとして失敗します(#2)。SQLiteは一致した行数を数えるため、この更新を受け入れます。両側が同じ行に同じ変更を行った場合、merged更新でこの状況が発生することがあります。

操作の選択

マージ結果には両側の操作が含まれます。baseをマージ後の状態にするためです。すでにtheirsの状態を持つ対象に適用すると、theirs側から生じた操作は対象に反映済みの変更を表すため、その事前条件は失敗します:

操作 theirsの状態を持つ対象での状態 選択
oursの挿入、更新、削除 まだ反映されていない はい
merged更新(両側が行を変更) theirs側の列はあるが、ours側の列はない はい
theirs挿入 行が存在するため、ガード付き挿入は影響行数が0になる いいえ
merged削除(両側が行を削除) 行がすでにないため、ガード付き削除は影響行数が0になる いいえ
theirs更新または削除 すでに適用済み いいえ

したがって、Merql::merge()の結果全体をapplyGuarded()に渡すと、theirsが行を挿入した場合や、両側が行を削除した場合に失敗します。上記のようにソースごとに、または確認UIから作成したChangeGroupSelectionを使って、先に操作を選択してください。$plan->conflictsに記載された行は除外します。選択した結果には競合がなく、競合行の操作では、競合する列の値としてours側の値が使われます(マージ計画を参照)。

選択した結果の適用

ガード付き適用は、変更グループによる選択を含め、マージ計画から作成された選択済みマージ結果に対応します。

PHP
use Merql\Plan\ChangeGroupSelection;
use Merql\Plan\SelectedMergeResultFactory;
use Merql\Snapshot\SnapshotStore;

$selection = ChangeGroupSelection::fromIds([$plan->changeGroups[0]->id]);
$selected = (new SelectedMergeResultFactory())->fromChangeGroupSelection(
    $plan,
    $selection,
    SnapshotStore::load($plan->baseSnapshot),
);

$applied = Merql::applyGuarded($selected, SnapshotStore::load($plan->theirsSnapshot));

これにより、ホストアプリケーションは、計画作成後にライブ行が変化していないことを確認しながら、確認済みグループの一部を適用できます。

SQLプレビュー

SqlGenerator::generateGuarded()を使うと、実行せずにガード付き文を確認できます。

PHP
use Merql\Apply\SqlGenerator;
use Merql\Snapshot\SnapshotStore;

$statements = SqlGenerator::generateGuarded(
    $selected,
    SnapshotStore::load($plan->theirsSnapshot),
);

foreach ($statements as $statement) {
    echo $statement['sql'] . "\n";
}

生成される文は引き続きパラメーター化されています。事前条件は、更新と削除ではWHERE句、挿入ではNOT EXISTSチェックとして追加されます。

ガード付き適用と通常の適用

Merql::apply()は事前条件なしで同じ操作を実行します。その場合、ライブ行が計画の作成後に変更されていても更新と削除は成功し、既存行の挿入はデータベースの重複キーエラーで失敗します。対象が引き続きbaseの状態だと分かっている場合は通常の適用を、スナップショット取得後に他の書き込み処理が対象を変更した可能性がある場合はガード付き適用を使ってください。