Treci la conținut

Îmbinare în trei direcții

O îmbinare în trei direcții compară două seturi de modificări cu un strămoș comun și le combină într-un singur rezultat. merql aplică acest algoritm rândurilor din baza de date, în locul liniilor din fișiere.

PHP
use Merql\Merge\ThreeWayMerge;

$merge = new ThreeWayMerge();
$result = $merge->merge($base, $ours, $theirs);

echo $result->operationCount(); // resolved operations
echo $result->conflictCount();  // unresolved conflicts

Cele trei stări

Fiecare îmbinare necesită exact trei instantanee:

  • Bază -- starea bazei de date în momentul în care cele două ramuri au divergat.
  • A noastră -- o ramură după modificările sale, de exemplu o copie sandbox care a fost editată.
  • A lor -- cealaltă ramură după modificările sale, de exemplu baza de date activă care a continuat să funcționeze.

Stările noastră și a lor trebuie capturate fiecare din baza de date care conține ramura respectivă. Regulile de îmbinare tratează ambele părți în mod egal; denumirile stabilesc source fiecărei operații, numele tipurilor de conflict (update_delete înseamnă că noi am actualizat, iar ei au șters) și partea favorizată de ConflictPolicy::OursWins și TheirsWins.

Cod
         Base (snapshot at fork time)
        /                              \
   Ours (our changes)                   Theirs (their changes)
        \                              /
         --------- MERGE ----------
                     |
              Merged result
              + conflicts (if any)

Fără o bază comună, nu puteți distinge „adăugat” de „nemodificat”. Comparația în două direcții arată că partea A are o valoare, iar partea B nu, dar nu poate determina dacă A a adăugat-o sau B a șters-o. Baza răspunde la această întrebare.

Cum se calculează seturile de modificări

Differ compară fiecare instantaneu cu baza pentru a produce două seturi de modificări. Fiecare set conține trei tipuri de operații:

Bază Curent Operație
Rândul există Rândul există, amprentă identică Nicio modificare
Rândul există Rândul există, amprentă diferită UPDATE (diferențiere pentru fiecare coloană)
Rândul există Rândul lipsește DELETE
Rândul lipsește Rândul există INSERT

Amprentele (SHA-256 hash-uri ale conținutului normalizat al rândului) oferă o cale rapidă. Dacă două amprente coincid, rândul nu s-a schimbat și nu este necesară compararea la nivel de coloană.

PHP
use Merql\Diff\Differ;

$differ = new Differ();
$oursChangeset = $differ->diff($base, $ours);
$theirsChangeset = $differ->diff($base, $theirs);

Reguli de îmbinare

Cele două seturi de modificări sunt apoi îmbinate operație cu operație. Acest tabel definește rezultatul pentru fiecare combinație:

Ale noastre Ale lor Rezultat
Fără modificări Fără modificări Păstrează baza
Fără modificări Actualizat Acceptă varianta lor
Actualizat Fără modificări Acceptă varianta noastră
Actualizat (aceeași valoare) Actualizat (aceeași valoare) Acceptă (ambele sunt de acord)
Actualizat (valori diferite) Actualizat (valori diferite) Conflict
Fără modificări Șters Acceptă ștergerea
Șters Fără modificări Acceptă ștergerea
Șters Șters Acceptă ștergerea (ambele sunt de acord)
Actualizat Șters Conflict
Șters Actualizat Conflict
Inserat (doar al nostru) -- Acceptă inserarea
-- Inserat (doar al lor) Acceptă inserarea
Inserat (același PK) Inserat (același PK) Conflict

Când ambele părți actualizează același rând, merql nu declară imediat un conflict. Trecerea se face la îmbinarea la nivel de coloană, comparând fiecare coloană separat. Pentru detalii, consultați Îmbinare la nivel de coloană.

Utilizarea facade-ului

Merql facade-ul operează asupra unor instantanee denumite, persistate pe disc. Apelați Merql::init() cu baza de date care conține fiecare stare înainte de a crea instantaneul:

PHP
use Merql\Connection;
use Merql\Merql;

$live    = Connection::sqlite('/data/live.sqlite');
$sandbox = Connection::sqlite('/data/sandbox.sqlite');

Merql::init($live);
Merql::snapshot('base');      // before the sandbox is copied from live

// ... copy live.sqlite to sandbox.sqlite, then both databases change ...

Merql::init($sandbox);
Merql::snapshot('ours');
Merql::init($live);
Merql::snapshot('theirs');

$result = Merql::merge('base', 'ours', 'theirs');

Nu creați toate cele trei instantanee pe rând dintr-o singură bază de date. Varianta lor ar conține atunci și modificările noastre, astfel că fiecare rând inserat de noi ar apărea ca inserat de ambele părți și ar fi raportat drept insert_insert conflict.

Merql::merge(), Merql::patch() și Merql::plan() creează un ThreeWayMerge fără CellMergeConfig, așa că îmbină doar la nivel de coloană. Pentru îmbinare la nivel de celulă, încărcați instantaneele și folosiți direct clasa, ca mai jos.

Utilizarea directă a claselor

Pentru mai mult control, instanțiați singuri componentele. Snapshotter::capture() nu persistă nimic, așa că păstrați instantaneele în memorie sau salvați-le cu SnapshotStore::save():

PHP
use Merql\CellMerge\CellMergeConfig;
use Merql\Merge\ThreeWayMerge;
use Merql\Snapshot\Snapshotter;
use Merql\Snapshot\SnapshotStore;

$base   = (new Snapshotter($live))->capture('base');
// ... copy live to the sandbox, then both databases change ...
$ours   = (new Snapshotter($sandbox))->capture('ours');
$theirs = (new Snapshotter($live))->capture('theirs');

$merge = new ThreeWayMerge();
$result = $merge->merge($base, $ours, $theirs);

// Snapshots saved earlier, merged with cell-level merge for TEXT and JSON columns.
$merge = new ThreeWayMerge(CellMergeConfig::auto());
$result = $merge->merge(
    SnapshotStore::load('base'),
    SnapshotStore::load('ours'),
    SnapshotStore::load('theirs'),
);

Scurtătura pentru patch-uri

Când doar o parte are modificări (fără modificări concurente), folosiți patch(). Aceasta este o îmbinare bidirecțională în care varianta noastră este identică cu baza, deci conflictele sunt imposibile:

PHP
$result = $merge->patch($base, $changes);
// Always clean: ours is identical to base.

Prin facade:

PHP
$result = Merql::patch('base', 'changes');

patch() este echivalent cu merge($base, $base, $changes). Deoarece varianta noastră nu s-a abătut niciodată de la bază, toate modificările din varianta lor sunt acceptate fără conflict.

Detectarea nepotrivirilor de schemă

Dacă s-au adăugat sau eliminat coloane dintr-un tabel între instantanee, îmbinarea continuă, dar înregistrează nepotrivirile. Tipurile coloanelor modificate nu sunt comparate, iar o coloană redenumită apare ca una eliminată și una adăugată; consultați Validarea schemei.

PHP
$result = $merge->merge($base, $ours, $theirs);

if ($result->hasSchemaMismatches()) {
    foreach ($result->schemaMismatches() as $mismatch) {
        echo $mismatch->getMessage() . "\n";
    }
}

merql nu migrează automat schemele. Modificările schemei țin de responsabilitatea apelantului. Îmbinarea le raportează, astfel încât să puteți decide cum să procedați.

MergeResult

Obiectul rezultat conține tot ce vă trebuie:

PHP
$result->isClean();            // true if no conflicts
$result->operations();         // list of MergeOperation objects
$result->conflicts();          // list of Conflict objects
$result->operationCount();     // number of resolved operations
$result->conflictCount();      // number of unresolved conflicts
$result->hasSchemaMismatches(); // true if schemas differ
$result->baseSnapshot();       // the base snapshot used for the merge

Fiecare MergeOperation are un tip (insert, update sau delete), un nume de tabel, o cheie de rând, valorile coloanelor și o sursă (ours, theirs sau merged). merged marchează un rând actualizat de ambele părți (combinat coloană cu coloană) sau șters de ambele părți.

Unde se aplică rezultatul unei îmbinări

Rezultatul unei îmbinări descrie cum se transformă baza în starea îmbinată. Operațiile sale conțin modificările ambelor părți: o inserare făcută de noi și una făcută de ei apar ambele ca inserări.

  • Merql::apply() (sau Applier) execută fiecare operație asupra bazei de date conectate. Se aplică fără probleme unei baze de date care încă păstrează baza, de exemplu unei copii proaspete a stării de bază. Dacă baza de date conține deja modificările uneia dintre părți, inserarea unui rând existent eșuează, iar tranzacția este anulată.
  • Pentru a aduce varianta noastră într-o bază de date care conține deja varianta lor, de exemplu pentru a promova modificările din sandbox în mediul live, aplicați doar operațiile care lipsesc din acea bază de date: cele cu sursa ours și actualizările merged. Folosiți un plan de îmbinare pentru a le selecta și Aplicare protejată pentru a le scrie cu precondiții asupra rândurilor live.