انتقل إلى المحتوى

التطبيق المحمي

لا يطبّق GuardedApplier نتيجة MergeResult نظيفة إلا إذا كانت قاعدة البيانات المستهدفة ما تزال تطابق اللقطة الحية المتوقعة. يفيد ذلك عند مراجعة خطة دمج مسبقًا، إذ قد تكون الصفوف الحية قد تغيرت قبل التطبيق.

في الإعداد المعتاد، تُنقل تعديلات بيئة معزولة إلى قاعدة بيانات حية. يكون الأساس هو قاعدة البيانات الحية عند إنشاء نسخة البيئة المعزولة، وتكون 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 عندما يُجري الجانبان التغيير نفسه على صف.

اختيار العمليات

تحتوي نتيجة الدمج على عمليات كلا الجانبين، إذ إنها تحوّل الأساس إلى الحالة المدمجة. وعند تطبيقها على هدف يحتوي مسبقًا على 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() العمليات نفسها من دون شروط مسبقة. عندها تنجح التحديثات والحذوفات سواء تغير الصف الحي منذ إنشاء الخطة أم لا، ويفشل إدراج صف موجود بسبب خطأ المفتاح المكرر في قاعدة البيانات. استخدم التطبيق العادي لهدف تعرف أنه ما يزال يحتوي على الأساس، والتطبيق المحمي عندما يحتمل أن كتّابًا آخرين قد عدّلوا الهدف منذ أخذ لقطته.