Přejít k obsahu

Začínáme

Tento průvodce vás provede instalací, připojením k databázi, pořizováním snímků, výpočtem rozdílů a provedením třícestného sloučení.

Instalace

Shell
composer require merql/merql

merql vyžaduje PHP 8.2+ s ext-mbstring a ext-zlib (obě vyžaduje závislost pitmaster/pitmaster). Potřebujete také rozšíření pro použitý adaptér: ext-pdo s ext-pdo_sqlite nebo ext-pdo_mysql pro adaptér PDO, případně ext-mysqli pro adaptér mysqli. Composer tato rozšíření pouze doporučuje, proto ověřte, zda je potřebné rozšíření nainstalováno.

Balíček nainstaluje CLI pod názvem vendor/bin/merql. Pro CLI používejte merql 0.4.1 nebo novější; ve starších verzích binární soubor při instalaci merql jako závislosti nedokázal najít automatické načítání tříd.

Připojení k databázi

merql pracuje přes jednoduché rozhraní adaptéru připojení. Obsahuje vestavěné adaptéry pro PDO a mysqli a vestavěné ovladače dialektů pro MySQL a 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() otevírá libovolné databáze podporované PDO, merql však obsahuje pouze ovladače dialektů pro MySQL a SQLite. Při použití jiného stroje, například pgsql:, se připojení otevře, ale Merql::init() vyvolá Unsupported database driver: pgsql, dokud pro něj nezaregistrujete ovladač. Viz Ovladače databází.

Pořízení snímku

Snímek zaznamenává každý řádek každé tabulky (nebo vybraných tabulek) jako otisk a data. Otisky umožňují rychle zjistit změny; úplná data řádků jsou k dispozici při výpočtu rozdílu.

PHP
use Merql\Merql;

Merql::init($connection);

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

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

Snímky se ve výchozím nastavení ukládají jako soubory JSON do .merql/snapshots/. Toto umístění můžete změnit:

PHP
use Merql\Snapshot\SnapshotStore;

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

Výpočet rozdílu

Rozdíl porovnává dva snímky a vrací sadu změn obsahující vložení, aktualizace a odstranění. Oba snímky už musí existovat; zde je current druhý snímek pořízený po změně databáze.

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";
    }
}

Třícestné sloučení

Základní operace. Přijímá tři pojmenované snímky (base, ours, theirs) a slučuje dvě sady změn.

Base je stav, ze kterého vycházely obě kopie. Ours a theirs jsou dvě kopie po samostatných změnách, takže každou je třeba zachytit z databáze, ve které se nachází. Například když byla izolovaná kopie vytvořena z ostré databáze:

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";

Nezachycujte všechny tři snímky postupně z jedné databáze. Theirs by už obsahoval změny z ours a každý řádek vložený v ours by se projevil jako konflikt insert_insert.

Merql::merge() slučuje na úrovni sloupců. Chcete-li slučovat také obsah hodnot ve sloupcích TEXT a JSON, použijte ThreeWayMerge s CellMergeConfig; viz Sloučení na úrovni buněk.

Řešení konfliktů

Pokud obě strany změní tentýž sloupec na různé hodnoty, merql vyvolá konflikt. Konflikty můžete vyřešit automaticky nebo je zkontrolovat ručně.

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";
    }
}

Použití sloučení

Jakmile máte čistý výsledek sloučení (bez nevyřešených konfliktů), použijte jej v databázi zadané pro Merql::init().

Operace ve výsledku sloučení převedou základní stav na sloučený stav a zahrnují změny obou stran. Merql::apply() provede všechny operace, takže je lze bezpečně použít v databázi, která stále obsahuje základní stav. Pokud je použijete v databázi, která už obsahuje změny jedné strany, každé vložení z této strany narazí na existující řádek, selže a vrátí zpět celé sloučení. Chcete-li přenést změny jedné strany do databáze, která už obsahuje změny druhé strany (například úpravy z izolované kopie do ostré databáze), použijte pouze chybějící operace pomocí Podmíněného použití změn.

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

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

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

Nástroj pro použití změn obalí všechny operace do transakce. Pokud některý příkaz selže, celé sloučení se vrátí zpět a chyba se vrátí v errors(). V takovém případě rowsAffected() stále udává počet příkazů spuštěných před selháním, i když byly vráceny zpět (#4); nejprve zkontrolujte hasErrors().

Zkušební běh

Zobrazí náhled příkazů SQL, které by sloučení vygenerovalo, aniž by je provedlo.

PHP
use Merql\Apply\DryRun;

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

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

Dvoucestná oprava

Pokud máte pouze základní stav a sadu změn (bez souběžných úprav v „ours“), použijte patch(). Odpovídá to sloučení, při kterém se „ours“ od základního stavu nezměnilo.

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

Použití CLI

Všechny tyto operace jsou dostupné také z příkazového řádku. Podrobnosti najdete v referenci CLI.

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

Při třícestném sloučení pořiďte každý snímek z databáze obsahující příslušný stav a poté proveďte sloučení:

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

Filtrování tabulek a sloupců

Ze snímků lze vynechat tabulky (například tabulky mezipaměti nebo relací) a ignorovat proměnlivé sloupce (například časové značky 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']),
]);

Další kroky