Migrations

Migrasi untuk proyek Anda menjaga semua perubahan basis data yang terlibat dalam proyek Anda.

byjg/php-migration adalah pustaka inti yang sangat membantu untuk memulai.

Menginstal

Pustaka PHP

Jika Anda ingin menggunakan hanya Pustaka PHP di proyek Anda:

composer require "byjg/migration"

Antarmuka Baris Perintah

Antarmuka baris perintah berdiri sendiri dan tidak memerlukan Anda menginstalnya bersama proyek Anda.

Anda dapat menginstalnya secara global dan membuat tautan simbolis

composer require "byjg/migration-cli"

Silakan kunjungi byjg/migration-cli untuk mendapatkan lebih banyak informasi tentang Migration CLI.

Basis data yang didukung

Basis Data Driver String Koneksi
Sqlite pdo_sqlite sqlite:///path/to/file
MySql/MariaDb pdo_mysql mysql://username:password@hostname:port/database
Postgres pdo_pgsql pgsql://username:password@hostname:port/database
Sql Server pdo_dblib, pdo_sysbase Linux dblib://username:password@hostname:port/database
Sql Server pdo_sqlsrv Windows sqlsrv://username:password@hostname:port/database

Bagaimana Ini Bekerja?

Migrasi Basis Data menggunakan SQL MURNI untuk mengelola versi basis data.

Untuk dapat berfungsi, Anda perlu:

  • Membuat Skrip SQL
  • Mengelola menggunakan Baris Perintah atau API.

Skrip SQL

Skrip dibagi menjadi tiga set skrip:

  • Skrip BASIS berisi SEMUA perintah SQL untuk membuat basis data yang baru;
  • Skrip UP berisi semua perintah migrasi SQL untuk "naik" versi basis data;
  • Skrip DOWN berisi semua perintah migrasi SQL untuk "turun" atau mengembalikan versi basis data;

Direktori skrip adalah:

 <root dir>
     |
     +-- base.sql
     |
     +-- /migrations
              |
              +-- /up
                   |
                   +-- 00001.sql
                   +-- 00002.sql
              +-- /down
                   |
                   +-- 00000.sql
                   +-- 00001.sql
  • "base.sql" adalah skrip dasar
  • Folder "up" berisi skrip untuk migrasi naik versi. Sebagai contoh: 00002.sql adalah skrip untuk mengubah basis data dari versi '1' ke '2'.
  • Folder "down" berisi skrip untuk migrasi turun versi. Sebagai contoh: 00001.sql adalah skrip untuk mengubah basis data dari versi '2' ke '1'. Folder "down" adalah opsional.

Lingkungan Pengembangan Multi

Jika Anda bekerja dengan beberapa pengembang dan beberapa cabang, sulit untuk menentukan nomor berikutnya.

Dalam kasus itu, Anda mempunyai akhiran "-dev" setelah nomor versi.

Lihat skenarionya:

  • Pengembang 1 membuat cabang dan versi terbaru misalnya 42.
  • Pengembang 2 membuat cabang pada saat yang sama dan memiliki nomor versi basis data yang sama.

Dalam kedua kasus, para pengembang akan membuat file bernama 43-dev.sql. Kedua pengembang akan bermigrasi NAIK dan TURUN tanpa masalah dan versi lokal Anda akan menjadi 43.

Namun pengembang 1 menggabungkan perubahan Anda dan membuat versi akhir 43.sql (git mv 43-dev.sql 43.sql). Jika pengembang 2 memperbarui cabang lokal Anda, dia akan memiliki file 43.sql (dari dev 1) dan file Anda 43-dev.sql.

Jika dia mencoba untuk bermigrasi NAIK atau TURUN, skrip migrasi akan turun dan memberi tahu bahwa terdapat DUA versi 43. Dalam kasus ini, pengembang 2 harus memperbarui file-nya menjadi 44-dev.sql dan melanjutkan bekerja hingga menggabungkan perubahan Anda dan menghasilkan versi akhir.

Menggunakan API PHP dan Mengintegrasikannya ke dalam Proyek Anda

Penggunaan dasar adalah

  • Membuat koneksi objek ConnectionManagement. Untuk informasi lebih lanjut, lihat komponen "byjg/anydataset".
  • Membuat objek Migrasi dengan koneksi ini dan folder tempat skrip SQL berada.
  • Gunakan perintah yang sesuai untuk "reset", "up", atau "down" skrip migrasi.

Lihat contohnya:

<?php
// Membuat URI Koneksi
// Lihat lebih lanjut: https://github.com/byjg/anydataset#connection-based-on-uri
$connectionUri = new \ByJG\Util\Uri('mysql://migrateuser:migratepwd@localhost/migratedatabase');

// Daftarkan Database atau Basis Data yang dapat menangani URI tersebut:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);

// Membuat instance Migrasi
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');

// Tambahkan fungsi progres callback untuk menerima info dari eksekusi
$migration->addCallbackProgress(function ($action, $currentVersion, $fileInfo) {
    echo "$action, $currentVersion, ${fileInfo['description']}\n";
});

// Mengembalikan basis data menggunakan skrip "base.sql"
// dan menjalankan SEMUA skrip yang ada untuk menaikkan versi basis data ke versi terbaru
$migration->reset();

// Jalankan SEMUA skrip yang ada untuk naik atau turun versi basis data
// dari versi sekarang hingga nomor $version;
// Jika nomor versi tidak ditentukan, migrasi hingga versi basis data terakhir
$migration->update($version = null);

Objek Migrasi mengontrol versi basis data.

Membuat kontrol versi di proyek Anda

<?php
// Daftarkan Database atau Basis Data yang dapat menangani URI tersebut:
\ByJG\DbMigration\Migration::registerDatabase(\ByJG\DbMigration\Database\MySqlDatabase::class);

// Membuat instance Migrasi
$migration = new \ByJG\DbMigration\Migration($connectionUri, '.');

// Perintah ini akan membuat tabel versi di basis data Anda
$migration->createVersion();

Mendapatkan versi saat ini

<?php
$migration->getCurrentVersion();

Menambahkan Callback untuk mengontrol progres

<?php
$migration->addCallbackProgress(function ($command, $version, $fileInfo) {
    echo "Melakukan Perintah: $command di versi $version - ${fileInfo['description']}, ${fileInfo['exists']}, ${fileInfo['file']}, ${fileInfo['checksum']}\n";
});

Mendapatkan instance Driver Db

<?php
$migration->getDbDriver();

Untuk menggunakannya, silakan kunjungi: https://github.com/byjg/anydataset-db

Menghindari Migrasi Parsial (tidak tersedia untuk MySQL)

Migrasi parsial adalah ketika skrip migrasi terhenti di tengah proses karena kesalahan atau penghentian manual.

Tabel migrasi akan memiliki status partial up atau partial down dan perlu diperbaiki secara manual sebelum dapat bermigrasi lagi.

Untuk menghindari situasi ini, Anda dapat menentukan migrasi akan dijalankan dalam konteks transaksional.

Jika skrip migrasi gagal, transaksi akan dibatalkan dan tabel migrasi akan ditandai sebagai complete dan versi akan menjadi versi sebelumnya yang segera sebelum skrip yang menyebabkan kesalahan.

Untuk mengaktifkan fitur ini, Anda perlu memanggil metode withTransactionEnabled dengan melewatkan true sebagai parameter:

<?php
$migration->withTransactionEnabled(true);

CATATAN: Fitur ini tidak tersedia untuk MySQL karena tidak mendukung perintah DDL di dalam transaksi.

Jika Anda menggunakan metode ini dengan MySQL, Migrasi akan mengabaikannya tanpa pemberitahuan.

Info lebih lanjut: https://dev.mysql.com/doc/refman/8.0/en/cannot-roll-back.html

Tips dalam menulis migrasi SQL untuk Postgres

Saat membuat trigger dan fungsi SQL

-- Lakukan
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- Periksa bahwa empname dan salary diberikan
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname tidak boleh null'; -- tidak masalah apakah komentar ini kosong atau tidak
        END IF; --
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% tidak dapat memiliki salary null', NEW.empname; --
        END IF; --

        -- Siapa yang bekerja untuk kita ketika mereka harus membayar untuk itu?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% tidak dapat memiliki salary negatif', NEW.empname; --
        END IF; --

        -- Ingat siapa yang mengubah gaji ketika
        NEW.last_date := current_timestamp; --
        NEW.last_user := current_user; --
        RETURN NEW; --
    END; --
$emp_stamp$ LANGUAGE plpgsql;

-- JANGAN
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- Periksa bahwa empname dan salary diberikan
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname tidak boleh null';
        END IF;
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% tidak dapat memiliki salary null', NEW.empname;
        END IF;

        -- Siapa yang bekerja untuk kita ketika mereka harus membayar untuk itu?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% tidak dapat memiliki salary negatif', NEW.empname;
        END IF;

        -- Ingat siapa yang mengubah gaji ketika
        NEW.last_date := current_timestamp;
        NEW.last_user := current_user;
        RETURN NEW;
    END;
$emp_stamp$ LANGUAGE plpgsql;

Karena lapisan abstraksi basis data PDO tidak dapat menjalankan kelompok pernyataan SQL,

ketika byjg/migration membaca file migrasi, itu harus memisahkan seluruh isi file SQL pada titik koma, dan menjalankan pernyataan satu per satu. Namun, ada satu jenis pernyataan yang dapat memiliki beberapa titik koma di antara tubuhnya: fungsi.

Agar dapat mem-parsing fungsi dengan benar, byjg/migration 2.1.0 mulai memisahkan file migrasi pada urutan semicolon + EOL bukannya hanya titik koma. Dengan cara ini, jika Anda menambahkan komentar kosong setelah setiap titik koma dalam definisi fungsi, byjg/migration akan dapat mem-parsingnya.

Sayangnya, jika Anda lupa menambahkan salah satu komentar ini, pustaka akan memisahkan pernyataan CREATE FUNCTION menjadi beberapa bagian dan migrasi akan gagal.

Hindari karakter titik dua (:)

-- Lakukan
CREATE TABLE bookings (
  booking_id UUID PRIMARY KEY,
  booked_at  TIMESTAMPTZ NOT NULL CHECK (CAST(booked_at AS DATE) <= check_in),
  check_in   DATE NOT NULL
);

-- JANGAN
CREATE TABLE bookings (
  booking_id UUID PRIMARY KEY,
  booked_at  TIMESTAMPTZ NOT NULL CHECK (booked_at::DATE <= check_in),
  check_in   DATE NOT NULL
);

Karena PDO menggunakan karakter titik dua untuk menjelaskan parameter bernama dalam pernyataan yang sudah disiapkan, penggunaannya akan menyebabkan kesalahan dalam konteks lain.

Misalnya, pernyataan PostgreSQL dapat menggunakan :: untuk mengonversi nilai antar tipe. Di sisi lain, PDO akan membaca ini sebagai parameter bernama yang tidak valid dalam konteks yang tidak valid dan gagal ketika mencoba menjalankannya.

Satu-satunya cara untuk memperbaiki ketidakkonsistenan ini adalah dengan menghindari titik dua sama sekali (dalam hal ini, PostgreSQL juga memiliki sintaks alternatif: CAST(value AS type)).

Gunakan editor SQL

Akhirnya, menulis migrasi SQL manual bisa melelahkan, tetapi jauh lebih mudah jika Anda menggunakan editor yang mampu memahami sintaks SQL, menyediakan autocompletion, mengintrospeksi skema basis data Anda saat ini dan/atau memformat kode Anda secara otomatis.

Menangani berbagai migrasi di dalam satu skema

Jika Anda perlu membuat skrip migrasi yang berbeda dan versi di dalam skema yang sama, itu mungkin

tetapi terlalu berisiko dan saya tidak merekomendasikannya sama sekali.

Untuk melakukan ini, Anda perlu membuat "tabel migrasi" yang berbeda dengan mengoper parameter pada konstruktor.

<?php
$migration = new \ByJG\DbMigration\Migration("db:/uri", "/path", true, "NEW_MIGRATION_TABLE_NAME");

Untuk alasan keamanan, fitur ini tidak tersedia di baris perintah, tetapi Anda dapat menggunakan variabel lingkungan

MIGRATION_VERSION untuk menyimpan namanya.

Kami sangat merekomendasikan untuk tidak menggunakan fitur ini. Rekomendasi adalah satu migrasi untuk satu skema.

Menjalankan Uji Unit

Uji unit dasar dapat dijalankan dengan:

vendor/bin/phpunit

Menjalankan uji basis data

Menjalankan uji integrasi memerlukan Anda untuk memiliki basis data yang aktif dan berjalan. Kami menyediakan docker-compose.yml dasar dan Anda

dapat menggunakannya untuk memulai basis data untuk pengujian.

Menjalankan basis data

docker-compose up -d postgres mysql mssql

Menjalankan uji

vendor/bin/phpunit
vendor/bin/phpunit tests/SqliteDatabase*
vendor/bin/phpunit tests/MysqlDatabase*
vendor/bin/phpunit tests/PostgresDatabase*
vendor/bin/phpunit tests/SqlServerDblibDatabase*
vendor/bin/phpunit tests/SqlServerSqlsrvDatabase*

Opsional Anda dapat mengatur host dan kata sandi yang digunakan oleh uji unit

export MYSQL_TEST_HOST=localhost     # default ke localhost
export MYSQL_PASSWORD=newpassword    # gunakan '.' jika ingin memiliki kata sandi null
export PSQL_TEST_HOST=localhost      # default ke localhost
export PSQL_PASSWORD=newpassword     # gunakan '.' jika ingin memiliki kata sandi null
export MSSQL_TEST_HOST=localhost     # default ke localhost
export MSSQL_PASSWORD=Pa55word
export SQLITE_TEST_HOST=/tmp/test.db      # default ke /tmp/test.db