Appearance
Migration
Mục tiêu
Migration là mã nguồn của schema database. Mỗi migration phải có thể chạy được trên SQLite và PostgreSQL, dễ kiểm tra, và không phụ thuộc vào behavior thay đổi của application code.
Các nguyên tắc mặc định:
- Dùng
SchemavàBlueprintcho DDL; chỉ dùng raw SQL khi schema builder không thể biểu diễn thay đổi một cách an toàn. - Dùng query builder cho backfill dữ liệu; không gọi Eloquent model trong migration.
- Mọi migration phải khai báo
up()vàdown(). - Ưu tiên thay đổi additive, tách các thay đổi rủi ro cao thành nhiều migration.
Tạo migration
bash
php artisan make:migration create_orders_table
php artisan make:migration add_timezone_to_users_table --table=users
php artisan make:migration create_role_user_table --create=role_userMigration tạo bảng mới nên chứa đầy đủ schema trong một file:
php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('orders', static function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')
->constrained()
->cascadeOnUpdate()
->cascadeOnDelete();
$table->string('status', 30)->default('pending');
$table->timestamps();
$table->index(['user_id', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('orders');
}
};Thay đổi bảng hiện tại
Khi thêm cột vào bảng đã tồn tại, guard từng cột để tránh lỗi khi môi trường đã được hotfix hoặc deploy một phần. Cột mới phải nullable() hoặc có default an toàn cho các row hiện có:
php
if (! Schema::hasColumn('users', 'timezone')) {
Schema::table('users', static function (Blueprint $table): void {
$table->string('timezone', 64)->nullable();
});
}Không dùng change() để siết constraint trước khi xử lý dữ liệu cũ. Backfill trước, sau đó mới thay đổi nullability hoặc type:
php
DB::table('stock_production_lots')
->whereNull('lot_number')
->update(['lot_number' => 'pending']);
Schema::table('stock_production_lots', static function (Blueprint $table): void {
$table->string('lot_number')->default('pending')->nullable(false)->change();
});Khi dùng change(), khai báo lại mọi modifier cần giữ lại như default, comment, unsigned và index. Nếu thao tác không tương thích giữa SQLite và PostgreSQL, branch rõ ràng theo driver hoặc chọn một rollout additive an toàn hơn. Không dùng raw ALTER TABLE cho thay đổi type hoặc nullability nếu Laravel schema builder đã hỗ trợ.
Foreign key và index
Ưu tiên foreignId()->constrained() thay vì tự ghép kiểu cột và constraint. Với foreign key nullable, gọi nullable() trước constrained():
php
$table->foreignId('manager_id')
->nullable()
->constrained('users')
->nullOnDelete();Tách thay đổi index hoặc constraint khỏi thay đổi cột không liên quan khi việc đó giúp rollback và production rollout rõ ràng hơn. Khi cần idempotency, guard index bằng API phù hợp với phiên bản Laravel đang dùng:
php
if (! Schema::hasIndex('users', 'users_email_verified_at_index')) {
Schema::table('users', static function (Blueprint $table): void {
$table->index(
['email', 'email_verified_at'],
'users_email_verified_at_index',
);
});
}Nếu SQLite không hỗ trợ trực tiếp thao tác drop/recreate constraint, branch theo driver và kiểm thử cả hai database. Không trộn sửa foreign key với migration feature lớn nếu chúng có thể triển khai độc lập.
Backfill và rollout nhiều phase
Backfill đơn giản nên dùng query builder:
php
DB::table('users')
->whereNull('timezone')
->update(['timezone' => 'UTC']);Với bảng lớn, đọc và cập nhật theo batch để giảm lock time:
php
DB::table('orders')
->select(['id', 'status'])
->orderBy('id')
->chunkById(500, static function ($orders): void {
foreach ($orders as $order) {
DB::table('orders')
->where('id', $order->id)
->update([
'status_label' => strtoupper($order->status),
]);
}
});Rollout production nên theo thứ tự: thêm cột nullable hoặc có default, deploy code đọc được cả schema cũ và mới, backfill dữ liệu, rồi mới siết constraint hoặc xóa artifact cũ trong migration tiếp theo. Heavy backfill nên tách thành một bước riêng để dễ quan sát và retry.
down() và rollback
Luôn định nghĩa down(). Với migration tạo bảng mới, dùng Schema::dropIfExists(...). Với migration additive đơn giản, có thể xóa artifact vừa thêm nếu việc đảo ngược an toàn.
Với migration destructive, data-moving hoặc có nhiều nhánh theo driver, không phát minh rollback nguy hiểm. Mặc định dùng no-op có ghi chú và tạo một forward migration mới khi cần khôi phục:
php
public function down(): void
{
fwrite(STDERR, static::class . ': down() not implemented.' . PHP_EOL);
}Kiểm thử và kiểm tra trước khi chạy
Trước và sau thay đổi rủi ro, kiểm tra trạng thái và SQL được sinh ra:
bash
php artisan migrate:status
php artisan migrate --pretend
php artisan migrate --path=database/migrations/2026_01_01_000000_add_timezone_to_users_table.phpSau mỗi migration mới hoặc chỉnh sửa migration, bắt buộc chạy:
bash
php artisan test --filter DatabaseMigrationCompatibilityTestCompatibility test phải chứng minh toàn bộ migration chạy sạch trên SQLite in-memory và PostgreSQL, bao gồm guarded additive changes và các nhánh theo driver. Thêm assertion riêng cho raw SQL, change(), foreign key hoặc index repair khi migration có các phần này.
Production
Trước khi deploy, xác nhận migration chưa chạy, xem output --pretend, và kiểm tra mọi cột NOT NULL mới có default hoặc kế hoạch backfill hoàn tất. Chạy migration production bằng:
bash
php artisan migrate --force --isolated--isolated chỉ ngăn nhiều process chạy migration đồng thời; nó không làm cho một migration không an toàn trở nên an toàn. Nếu migration production thất bại một phần, ưu tiên viết migration sửa tiếp theo thay vì phụ thuộc vào rollback đoán trước.
Troubleshooting nhanh
- Table already exists: kiểm tra
migrate:status; nếu table đã đúng schema, tạo migration modify thay vì làmcreateidempotent. - Column already exists: giữ guard cho additive change nhưng kiểm tra type, default và nullability của cột hiện tại để phát hiện drift.
- Foreign key mismatch: so sánh kiểu hai cột, bảng được reference và index; ưu tiên
foreignId()->constrained(). - SQLite operation không hỗ trợ: đổi sang rollout additive hoặc branch rõ theo driver.
- Guard che giấu drift: kiểm tra schema thực tế và viết repair migration có mục tiêu, không chồng thêm guard.
Checklist
- [ ] Migration tương thích SQLite và PostgreSQL.
- [ ] Cột mới trên bảng hiện tại có guard và default/nullability an toàn.
- [ ] Foreign key và index có tên, kiểu và behavior phù hợp.
- [ ] Backfill không dùng Eloquent model và có batch nếu bảng lớn.
- [ ]
up()vàdown()phản ánh đúng mức độ rủi ro. - [ ] Đã chạy
migrate --pretendvàDatabaseMigrationCompatibilityTest. - [ ] Production rollout có kế hoạch lock, backfill và forward repair.