跳转到正文

受保护应用

仅当目标数据库仍与预期的实时快照相符时,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会为生成的语句添加实时行前置条件:

  • 更新要求目标行标识存在,且非标识列的值与预期实时行相符,
  • 删除要求目标行标识存在,且非标识列的值与预期实时行相符,
  • 插入要求目标标识尚不存在。

如果任何语句影响零行,merql 会将计划视为已过期。事务会回滚,返回的 ApplyResult 中会包含错误 Optimistic precondition failed for guarded merge statement.

在 MySQL 中,如果更新后的值已经与实时行相同,该更新也会影响零行,因为 MySQL 统计的是已更改的行,而不是匹配的行。因此,即使行匹配,该更新仍会因计划过期而失败(#2)。SQLite 统计匹配的行,因此会接受这种情况。如果两侧对某行作出相同更改,就可能发生这种情况,此时会进行 merged 更新。

选择操作

合并结果包含两侧的操作,因为合并会将 base 转换为合并后的状态。将其应用于已经包含 theirs 的目标时,来自 theirs 的操作表示目标已有的变更,因此这些操作的前置条件会失败:

操作 包含 theirs 的目标中的状态 选择
ours 插入、更新或删除 缺失 是
merged 更新(两侧都更改了该行) 已有 theirs 的列,缺少 ours 的列 是
theirs 插入 行已存在,因此受保护的插入会影响零行 否
merged 删除(两侧都删除了该行) 行已删除,因此受保护的删除会影响零行 否
theirs 更新或删除 已应用 否

因此,将 Merql::merge() 的完整结果传给 applyGuarded(),只要 theirs 插入了某行或两侧都删除了某行,就会失败。先按上述来源选择操作,或使用基于审查ChangeGroupSelection构建的选择UI。排除 $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() 在没有前置条件的情况下执行相同操作。此时,无论计划生成后实时行是否发生变化,更新和删除都会成功;如果插入的行已存在,则会触发数据库的重复键错误。对于你确定仍处于基准状态的目标,使用普通应用;如果自创建快照以来其他写入方可能修改过目标,则使用带保护条件的应用。