Identitatea rândului
Identitatea rândurilor determină modul în care merql potrivește rândurile între instantanee. Când compară baza cu ours, trebuie să știe că rândul X din bază este același rând logic ca rândul Y din ours, chiar dacă unele valori ale coloanelor s-au schimbat. Regula de identitate stabilește ce coloane formează cheia rândului.
use Merql\Identity\PrimaryKeyIdentity;
$identity = new PrimaryKeyIdentity(['id']);
$key = $identity->key(['id' => 42, 'title' => 'Hello']);
// "42"
Interfața RowIdentity
Toate strategiile de identitate implementează două metode:
interface RowIdentity
{
public function key(array $row): string;
public function columns(): array;
}
key() construiește un identificator unic sub formă de șir din datele unui rând. columns() returnează numele coloanelor folosite pentru identitate.
PrimaryKeyIdentity
Strategia implicită. Potrivește rândurile după coloanele cheii primare.
use Merql\Identity\PrimaryKeyIdentity;
// Single-column PK.
$identity = new PrimaryKeyIdentity(['id']);
$key = $identity->key(['id' => 42, 'title' => 'Hello']);
// "42"
// Composite PK.
$identity = new PrimaryKeyIdentity(['tenant_id', 'user_id']);
$key = $identity->key(['tenant_id' => 1, 'user_id' => 99, 'name' => 'Alice']);
// "1\x1F99" (unit separator between parts)
Aceasta este strategia folosită implicit de merql atunci când schema tabelului definește o cheie primară.
NaturalKeyIdentity
Pentru tabelele fără cheie primară cu incrementare automată, potrivește rândurile după coloanele cu constrângeri unice. Este utilă când PK-ul este un UUID sau când tabelul folosește o constrângere unică compozită drept identitate logică.
use Merql\Identity\NaturalKeyIdentity;
$identity = new NaturalKeyIdentity(['email']);
$key = $identity->key(['id' => 1, 'email' => 'alice@example.com', 'name' => 'Alice']);
// "alice@example.com"
$identity = new NaturalKeyIdentity(['country', 'postal_code']);
$key = $identity->key(['country' => 'US', 'postal_code' => '90210', 'city' => 'Beverly Hills']);
// "US\x1F90210"
ContentHashIdentity
Soluție de rezervă pentru tabelele fără cheie primară și fără constrângeri unice. Calculează un hash SHA-256 al valorilor tuturor coloanelor. Două rânduri sunt identice dacă toate coloanele corespund.
use Merql\Identity\ContentHashIdentity;
$identity = new ContentHashIdentity(['name', 'value']);
$key = $identity->key(['name' => 'color', 'value' => 'blue']);
// "a3f2..." (SHA-256 hash)
Identitatea bazată pe hash-ul de conținut are o limitare: dacă se schimbă conținutul unui rând, se schimbă și hash-ul său, astfel încât rândul apare ca ștergere a hash-ului vechi și inserare a hash-ului nou, nu ca actualizare. Acesta este un compromis inerent atunci când nu există coloane de identitate stabile.
IdentityRule
IdentityRule este configurația de identitate serializabilă folosită de capturarea instantaneelor
și de planurile de îmbinare.
use Merql\Identity\IdentityRule;
$rule = IdentityRule::natural(['option_name']);
$key = $rule->key(['option_name' => 'siteurl']);
// "siteurl"
Tipuri de reguli:
| Tip | Semnificație |
|---|---|
primary |
Coloanele cheii primare din baza de date |
natural |
Coloane de afaceri unice și stabile |
composite |
Coloane de identitate logică compozită |
content |
Soluție de rezervă bazată pe hash-ul de conținut |
IdentityRule::forSchema($schema) rezolvă regula implicită:
- Dacă tabelul are o cheie primară, folosiți coloanele cheii primare.
- Dacă tabelul are chei unice, folosiți coloanele primei chei unice.
- Dacă nu există niciuna, folosiți toate coloanele (soluție de rezervă bazată pe hash-ul de conținut).
IdentityRuleSet
Folosiți IdentityRuleSet atunci când o aplicație gazdă cunoaște o regulă de identitate mai potrivită decât
valoarea implicită din schemă.
use Merql\Identity\IdentityRule;
use Merql\Identity\IdentityRuleSet;
use Merql\Snapshot\Snapshotter;
$rules = new IdentityRuleSet([
'wp_options' => IdentityRule::natural(['option_name']),
'wp_term_relationships' => IdentityRule::composite(['object_id', 'term_taxonomy_id']),
]);
$snapshotter = new Snapshotter($connection, identityRules: $rules);
$snapshot = $snapshotter->capture('live', ['wp_options', 'wp_term_relationships']);
Snapshotter rezolvă o IdentityRule pentru fiecare tabel capturat. Coloanele
alese sunt stocate în TableSnapshot::identityColumns și folosite pentru toate operațiunile ulterioare de diff, merge, plan, SQL și rollback.
Merql::init() poate primi același set de reguli când se folosește interfața statică:
use Merql\Merql;
Merql::init($connection, identityRules: $rules);
Merql::snapshot('live', ['wp_options']);
Seturile de reguli au o serializare stabilă de tip tablou:
$payload = $rules->toArray();
$rules = IdentityRuleSet::fromArray($payload);
Identitate ambiguă
Dacă două rânduri capturate produc aceeași cheie de identitate, capturarea eșuează în loc să suprascrie un rând cu altul.
use Merql\Exceptions\SnapshotException;
try {
$snapshotter->capture('live', ['wp_postmeta']);
} catch (SnapshotException $e) {
// The configured identity rule matched multiple rows.
}
Aplicațiile gazdă pot verifica în prealabil rândurile brute cu IdentityRuleSet::conflictsFor()
când vor să semnaleze ambiguitatea înainte de capturare:
$conflicts = $rules->conflictsFor(
'wp_postmeta',
IdentityRule::natural(['post_id', 'meta_key']),
$rows,
);
Pentru tabelele meta WordPress, post_id + meta_key este de obicei util pentru grupare
și afișare, dar nu este sigur ca identitate de rând, deoarece sunt permise chei meta duplicate.
Folosiți cheia primară a tabelului pentru identitatea rândului, cu excepția cazului în care aplicația gazdă poate
demonstra că cheia naturală este unică.
Codificarea cheii rândului
Cheile rândurilor sunt șiruri codificate care combină mai multe valori de coloană folosind un separator de unitate (\x1F). Valorile care conțin separatorul sau semnul procentului sunt escapate pentru a preveni coliziunile:
use Merql\Snapshot\Snapshotter;
// Encode.
$key = Snapshotter::buildRowKey(
['tenant_id' => '1', 'user_id' => '99'],
['tenant_id', 'user_id'],
);
// "1\x1F99"
// Decode.
$parts = Snapshotter::decodeRowKey($key);
// ['1', '99']
Reguli de escapare:
| Original | Escapat |
|---|---|
% |
%25 |
\x1F (separator de unitate) |
%1F |
Astfel, o valoare care conține \x1F nu se împarte în mai multe părți ale cheii. Codificarea este deterministă și reversibilă.
Divergența valorilor cu incrementare automată
Când ambele părți inserează rânduri noi, ID-urile cu incrementare automată vor diferi între ramuri. Rândul #100 din ours ar putea fi o înregistrare complet diferită de rândul #100 din theirs.
merql gestionează acest lucru potrivind rândurile după cheia primară. Dacă ambele părți au inserat un rând cu același ID cu incrementare automată, acesta este marcat drept conflict insert_insert. Pentru a evita situația, folosiți chei naturale (coloane unice și stabile între ramuri) pentru tabelele în care sunt așteptate inserări concurente.
// A natural key on (slug) avoids auto-increment collisions.
$identity = new NaturalKeyIdentity(['slug']);
Identitatea în instantanee
Coloanele de identitate alese în timpul capturării sunt stocate împreună cu fiecare TableSnapshot:
$tableSnapshot = $snapshot->getTable('posts');
$tableSnapshot->identityColumns;
// ['id']
SqlGenerator folosește aceste coloane pentru a construi clauze WHERE pentru instrucțiunile UPDATE și DELETE, iar planurile de îmbinare le folosesc pentru a expune valorile de identitate decodificate în vederea examinării, selectării, aplicării cu verificări și revenirii.