Захищене застосування
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() виконує ті самі операції без передумов. Тоді оновлення та видалення виконуються незалежно від того, чи змінювався поточний рядок після створення плану, а вставлення вже наявного рядка завершується помилкою дублікатного ключа бази даних. Використовуйте звичайне застосування для цілі, у якій, як ви знаєте, досі міститься base, а захищене — коли інші процеси могли змінити ціль після створення її знімка.