Treci la conținut

Primii pași

Acest ghid vă prezintă instalarea, conectarea la o bază de date, capturarea instantaneelor, calcularea diferențelor și efectuarea unei îmbinări în trei direcții.

Instalare

Shell
composer require merql/merql

merql necesită PHP 8.2+ cu ext-mbstring și ext-zlib (ambele necesare pentru pitmaster/pitmaster). Aveți nevoie și de extensia pentru adaptorul folosit: ext-pdo cu ext-pdo_sqlite sau ext-pdo_mysql pentru adaptorul PDO, ori ext-mysqli pentru adaptorul mysqli. Composer doar le sugerează, așadar verificați dacă extensia necesară este instalată.

Pachetul instalează CLI ca vendor/bin/merql. Folosiți merql 0.4.1 sau o versiune ulterioară pentru CLI; în versiunile anterioare, binarul nu putea găsi autoloaderul când merql era instalat ca dependență.

Conectarea la o bază de date

merql funcționează printr-o interfață simplă de adaptor pentru conexiuni. Sunt incluse adaptoare pentru PDO și mysqli, precum și drivere de dialect pentru MySQL și SQLite.

PHP
use Merql\Connection;

// SQLite (in-memory or file-based, through PDO).
$connection = Connection::sqlite(':memory:');
$connection = Connection::sqlite('/path/to/database.sqlite');

// MySQL through PDO.
$connection = Connection::mysql('127.0.0.1', 'my_database', 'root', 'password');

// MySQL through mysqli.
$connection = Connection::mysqli('127.0.0.1', 'my_database', 'root', 'password');

// Any MySQL or SQLite PDO DSN.
$connection = Connection::fromDsn('mysql:host=127.0.0.1;dbname=my_database;charset=utf8mb4', 'root', 'password');

fromDsn() deschide orice DSN acceptat de PDO, însă merql include drivere de dialect doar pentru MySQL și SQLite. Cu un alt motor, precum pgsql:, conexiunea se deschide, dar Merql::init() aruncă Unsupported database driver: pgsql până când înregistrați un driver pentru acesta. Consultați Drivere de baze de date.

Capturarea unui instantaneu

Un instantaneu înregistrează fiecare rând din fiecare tabel (sau dintr-un subset de tabele) sub formă de amprente și date. Amprentele permit detectarea rapidă a modificărilor; datele complete ale rândurilor sunt disponibile când este nevoie de o comparație.

PHP
use Merql\Merql;

Merql::init($connection);

// Snapshot all tables.
$snapshot = Merql::snapshot('baseline');

// Snapshot specific tables.
$snapshot = Merql::snapshot('baseline', ['posts', 'comments', 'users']);

Instantaneele sunt salvate implicit ca fișiere JSON în .merql/snapshots/. Puteți schimba această locație:

PHP
use Merql\Snapshot\SnapshotStore;

SnapshotStore::setDirectory('/var/data/merql-snapshots');

Calcularea unei comparații

O comparație analizează două instantanee și returnează un set de modificări cu inserări, actualizări și ștergeri. Ambele instantanee trebuie să existe deja; aici current este un al doilea instantaneu, capturat după modificarea bazei de date.

PHP
$changeset = Merql::diff('baseline', 'current');

echo "Inserts: " . count($changeset->inserts()) . "\n";
echo "Updates: " . count($changeset->updates()) . "\n";
echo "Deletes: " . count($changeset->deletes()) . "\n";

// Inspect changes for a single table.
$postChanges = $changeset->forTable('posts');

// Check per-column diffs on updates.
foreach ($changeset->updates() as $update) {
    echo "Row {$update->rowKey} in {$update->table}:\n";
    foreach ($update->columnDiffs as $diff) {
        echo "  {$diff->column}: {$diff->oldValue} -> {$diff->newValue}\n";
    }
}

Îmbinare în trei direcții

Operația principală. Preia trei instantanee cu nume (bază, versiunea noastră, versiunea celorlalți) și îmbină cele două seturi de modificări.

Baza este starea de la care au pornit ambele copii. Versiunile noastră și a celorlalți sunt cele două copii după ce au fost modificate independent, așadar fiecare trebuie capturată din baza de date care o conține. De exemplu, când un mediu sandbox a fost copiat dintr-o bază de date activă:

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');      // the sandbox with our edits
Merql::init($live);
Merql::snapshot('theirs');    // live with its own changes

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

echo "Operations: {$result->operationCount()}\n";
echo "Conflicts:  {$result->conflictCount()}\n";
echo "Clean:      " . ($result->isClean() ? 'yes' : 'no') . "\n";

Nu capturați toate cele trei instantanee succesiv dintr-o singură bază de date. În acest caz, versiunea celorlalți ar conține deja modificările noastre, iar fiecare rând inserat de noi ar apărea ca un conflict insert_insert.

Merql::merge() face îmbinarea la nivel de coloană. Pentru a îmbina și în interiorul valorilor TEXT și JSON, folosiți ThreeWayMerge cu un CellMergeConfig; consultați Îmbinare la nivel de celulă.

Gestionarea conflictelor

Când ambele părți modifică aceeași coloană cu valori diferite, merql semnalează un conflict. Puteți rezolva conflictele automat sau le puteți inspecta manual.

PHP
use Merql\Merge\ConflictPolicy;
use Merql\Merge\ConflictResolver;

if (!$result->isClean()) {
    // Auto-resolve: one side wins.
    $resolved = ConflictResolver::resolve($result, ConflictPolicy::TheirsWins);

    // Or inspect each conflict.
    foreach ($result->conflicts() as $conflict) {
        echo "Conflict on {$conflict->table()} row {$conflict->rowKey()}:\n";
        echo "  Type:   {$conflict->type()}\n";
        echo "  Column: {$conflict->column()}\n";
        echo "  Ours:   " . var_export($conflict->oursValue(), true) . "\n";
        echo "  Theirs: " . var_export($conflict->theirsValue(), true) . "\n";
    }
}

Aplicarea îmbinării

După ce obțineți un rezultat al îmbinării curat (fără conflicte nerezolvate), aplicați-l bazei de date transmise către Merql::init().

Operațiile din rezultatul îmbinării transformă baza în starea îmbinată și includ modificările ambelor părți. Merql::apply() le execută pe toate, așadar aplicarea reușește pe o bază de date care încă păstrează starea de bază. Dacă le aplicați unei baze de date care conține deja modificările uneia dintre părți, orice inserare a acelei părți întâlnește un rând existent, eșuează și anulează întreaga îmbinare. Pentru a transfera modificările unei părți într-o bază de date care o conține deja pe cealaltă parte (de exemplu, modificările din sandbox în baza activă), aplicați doar operațiile care îi lipsesc folosind Aplicare condiționată.

PHP
$applied = Merql::apply($result);

echo "Rows affected: {$applied->rowsAffected()}\n";

if ($applied->hasErrors()) {
    foreach ($applied->errors() as $error) {
        echo "Error: {$error}\n";
    }
}

Executorul încadrează toate operațiile într-o tranzacție. Dacă o instrucțiune eșuează, întreaga îmbinare este anulată, iar eroarea este returnată în errors(). În acest caz, rowsAffected() numără în continuare instrucțiunile executate înainte de eșec, chiar dacă au fost anulate (#4); verificați mai întâi hasErrors().

Simulare

Previzualizați SQL pe care le-ar genera o îmbinare, fără a le executa.

PHP
use Merql\Apply\DryRun;

$statements = DryRun::generate($result);

foreach ($statements as $sql) {
    echo "{$sql};\n";
}

Patch în două direcții

Când aveți doar o bază și un set de modificări (fără modificări concurente din partea noastră), folosiți patch(). Este echivalent cu o îmbinare în care versiunea noastră nu diferă de bază.

PHP
$result = Merql::patch('base', 'changes');
// Always clean: no concurrent edits means no conflicts.
Merql::apply($result);

Folosirea CLI

Toate aceste operații sunt disponibile și din linia de comandă. Consultați CLI Referință pentru detalii complete.

Shell
export MERQL_DB_DSN="sqlite:mydb.sqlite"

vendor/bin/merql snapshot base
# ... change the database ...
vendor/bin/merql snapshot current
vendor/bin/merql diff base current

Pentru o îmbinare în trei direcții, capturați fiecare instantaneu din baza de date care conține starea respectivă, apoi efectuați îmbinarea:

Shell
MERQL_DB_DSN="sqlite:/data/live.sqlite" vendor/bin/merql snapshot base
MERQL_DB_DSN="sqlite:/data/sandbox.sqlite" vendor/bin/merql snapshot ours
MERQL_DB_DSN="sqlite:/data/live.sqlite" vendor/bin/merql snapshot theirs
vendor/bin/merql merge base ours theirs --dry-run

Filtrarea tabelelor și coloanelor

Instantaneele pot exclude tabele (precum cele pentru cache sau sesiuni) și pot ignora coloane volatile (precum marcajele temporale updated_at).

PHP
use Merql\Filter\TableFilter;
use Merql\Filter\ColumnFilter;
use Merql\Snapshot\Snapshotter;

$snapshotter = new Snapshotter($connection);

$snapshot = $snapshotter->capture('baseline', filters: [
    TableFilter::exclude(['sessions', 'cache_*']),
    ColumnFilter::ignore(['updated_at', 'modified_date']),
]);

Pașii următori