受保护应用
仅当目标数据库仍与预期的实时快照相符时,GuardedApplier 才会应用无冲突的 MergeResult。如果合并计划较早前已审核,而实时行可能已发生变化,这项功能就很有用。
典型用法是将沙盒中的编辑内容提升到实时数据库。base 是复制沙盒时的实时数据库,ours 是经过编辑的沙盒,theirs 是当前的实时数据库。目标是实时数据库,因此预期的实时快照是 theirs,而要写入的操作是实时数据库尚未包含的操作(参见选择操作)。
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 的值(参见合并计划)。
选择性应用
受保护应用适用于通过合并计划生成的所选合并结果,包括按变更组进行的选择。
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() 检查受保护语句,但不执行它们。
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() 在没有前置条件的情况下执行相同操作。此时,无论计划生成后实时行是否发生变化,更新和删除都会成功;如果插入的行已存在,则会触发数据库的重复键错误。对于你确定仍处于基准状态的目标,使用普通应用;如果自创建快照以来其他写入方可能修改过目标,则使用带保护条件的应用。