시작하기
이 가이드에서는 설치, 데이터베이스 연결, 스냅샷 캡처, 차이 계산 및 3방향 병합을 안내합니다.
설치
composer require merql/merql
merql에는 PHP 8.2 이상과 ext-mbstring 및 ext-zlib가 필요합니다(둘 다 pitmaster/pitmaster 종속성이 요구). 사용하는 어댑터에 맞는 확장도 필요합니다. PDO 어댑터에는 ext-pdo_sqlite가 포함된 ext-pdo 또는 ext-pdo_mysql가 필요하며, mysqli 어댑터에는 ext-mysqli가 필요합니다. Composer은 이러한 확장을 권장할 뿐이므로 필요한 확장이 설치되어 있는지 확인하세요.
패키지는 CLI을 vendor/bin/merql로 설치합니다. CLI을 사용하려면 merql 0.4.1 이상을 사용하세요. 이전 릴리스에서는 merql이 종속성으로 설치된 경우 바이너리가 자동 로더를 찾지 못했습니다.
데이터베이스 연결
merql은 간단한 연결 어댑터 인터페이스를 사용합니다. PDO와 mysqli용 기본 제공 어댑터 및 MySQL과 SQLite용 기본 제공 방언 드라이버가 있습니다.
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()은 PDO가 지원하는 모든 DSN을 열 수 있지만, merql에는 MySQL 및 SQLite용 방언 드라이버만 포함되어 있습니다. pgsql: 같은 다른 엔진을 사용해도 연결은 열리지만, 드라이버를 등록할 때까지 Merql::init()에서 Unsupported database driver: pgsql를 발생시킵니다. 데이터베이스 드라이버를 참고하세요.
스냅샷 캡처
스냅샷에는 모든 테이블(또는 일부 테이블)의 모든 행이 지문과 데이터로 기록됩니다. 지문을 사용하면 변경 사항을 빠르게 감지할 수 있으며, 차이를 계산할 때 전체 행 데이터를 사용할 수 있습니다.
use Merql\Merql;
Merql::init($connection);
// Snapshot all tables.
$snapshot = Merql::snapshot('baseline');
// Snapshot specific tables.
$snapshot = Merql::snapshot('baseline', ['posts', 'comments', 'users']);
기본적으로 스냅샷은 .merql/snapshots/에 JSON 파일로 저장됩니다. 이 위치는 변경할 수 있습니다.
use Merql\Snapshot\SnapshotStore;
SnapshotStore::setDirectory('/var/data/merql-snapshots');
차이 계산
차이는 두 스냅샷을 비교해 삽입, 업데이트, 삭제로 이루어진 변경 세트를 반환합니다. 두 스냅샷이 모두 이미 있어야 합니다. 여기서 current은 데이터베이스 변경 후 캡처한 두 번째 스냅샷입니다.
$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";
}
}
3방향 병합
핵심 작업입니다. 이름이 지정된 세 스냅샷(base, ours, theirs)을 받아 두 변경 세트를 병합합니다.
Base는 두 복사본이 함께 시작한 상태입니다. Ours와 theirs는 각각 독립적으로 변경된 뒤의 두 복사본이므로 각 스냅샷은 해당 상태가 있는 데이터베이스에서 캡처해야 합니다. 예를 들어 샌드박스를 운영 데이터베이스에서 복사한 경우는 다음과 같습니다.
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";
세 스냅샷을 한 데이터베이스에서 차례로 만들지 마세요. 그러면 theirs에 이미 ours의 변경 사항이 포함되어 ours가 삽입한 모든 행이 insert_insert 충돌로 표시됩니다.
Merql::merge()은 열 수준에서 병합합니다. TEXT 및 JSON 값 내부까지 병합하려면 CellMergeConfig와 함께 ThreeWayMerge를 사용하세요. 자세한 내용은 셀 수준 병합을 참고하세요.
충돌 처리
양쪽에서 같은 열을 서로 다른 값으로 변경하면 merql은 충돌을 발생시킵니다. 충돌을 자동으로 해결하거나 직접 확인할 수 있습니다.
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";
}
}
병합 적용
해결되지 않은 충돌이 없는 깨끗한 병합 결과가 준비되면 Merql::init()에 전달한 데이터베이스에 적용합니다.
병합 결과의 작업은 base를 병합 상태로 바꾸며 양쪽의 변경 사항을 모두 포함합니다. Merql::apply()은 모든 작업을 실행하므로 base 상태가 그대로 남아 있는 데이터베이스에는 문제없이 적용됩니다. 한쪽의 변경 사항이 이미 들어 있는 데이터베이스에 적용하면 해당 쪽에서 삽입한 행이 이미 존재해 실패하고 전체 병합이 롤백됩니다. 한쪽의 변경 사항을 다른 쪽이 이미 반영된 데이터베이스(예: 샌드박스의 편집 내용을 운영 환경에 반영)에 적용하려면 보호된 적용을 사용합니다.
$applied = Merql::apply($result);
echo "Rows affected: {$applied->rowsAffected()}\n";
if ($applied->hasErrors()) {
foreach ($applied->errors() as $error) {
echo "Error: {$error}\n";
}
}
적용기는 모든 작업을 트랜잭션으로 묶습니다. 문 하나라도 실패하면 전체 병합을 롤백하고 오류를 errors()에 반환합니다. 이 경우 롤백되더라도 rowsAffected()에는 실패 전에 실행된 문 수가 포함됩니다(#4). 먼저 hasErrors()를 확인하세요.
드라이 런
병합이 생성할 SQL을 실행하지 않고 미리 봅니다.
use Merql\Apply\DryRun;
$statements = DryRun::generate($result);
foreach ($statements as $sql) {
echo "{$sql};\n";
}
양방향 패치
base와 변경 사항만 있고 동시에 발생한 "ours" 수정은 없다면 patch()을 사용합니다. 이는 ours가 base에서 변경되지 않은 병합과 같습니다.
$result = Merql::patch('base', 'changes');
// Always clean: no concurrent edits means no conflicts.
Merql::apply($result);
CLI 사용
이 모든 작업은 명령줄에서도 사용할 수 있습니다. 자세한 내용은 CLI 참조를 확인하세요.
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
3방향 병합에서는 각 스냅샷을 해당 상태가 있는 데이터베이스에서 가져온 다음 병합합니다.
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
테이블 및 열 필터링
스냅샷에서 테이블(예: 캐시 또는 세션 테이블)을 제외하고 변동이 잦은 열(예: updated_at 타임스탬프)을 무시할 수 있습니다.
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']),
]);
다음 단계
- 전체 명령줄 인터페이스는 CLI 참조를 확인하세요.
- 모든 클래스와 메서드의 상세 문서는 PHP API 참조를 확인하세요.