Bài 8 — Migration, Schema và rollback: bắt đầu tầng dữ liệu của TaskFlow sau form preview ở bài 7. Bài này tạo schema, chưa biến preview thành thao tác lưu. Chúng ta kiểm tra ràng buộc bằng database thật trong bộ nhớ, không chỉ nhìn file migration có vẻ đúng.
1. Chốt quan hệ và chính sách xóa trước
users 1 ── n projects (owner_id)
projects 1 ── n tasks (project_id)
users 1 ── n tasks (assignee_id, có thể null)
Mỗi project có một owner; task thuộc một project, có thể chưa được giao cho ai. Khi xóa user đang sở hữu project, database từ chối. Khi xóa project còn task, database cũng từ chối. Khi xóa user chỉ được giao task, assignee_id trở thành null. Đây là quyết định bảo toàn dữ liệu của bài học, không phải mặc định phù hợp với mọi sản phẩm.
Khóa ngoại chỉ xác nhận record tham chiếu tồn tại; nó không chứng minh assignee có quyền tham gia project. Quy tắc thành viên và authorization sẽ nằm ở phần nghiệp vụ sau. Không nhầm ràng buộc dữ liệu với quyền truy cập.
2. Migration projects phải chạy trước tasks
Tạo file database/migrations/2026_09_22_000100_create_projects_table.php:
<?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('projects', function (Blueprint $table) {
$table->id();
$table->foreignId('owner_id')->constrained('users')->restrictOnDelete();
$table->string('name', 120);
$table->string('slug', 140);
$table->timestamps();
$table->unique(['owner_id', 'slug']);
});
}
public function down(): void
{
Schema::dropIfExists('projects');
}
};Unique(owner_id, slug) cho phép hai owner dùng cùng slug, nhưng một owner không có hai project trùng slug. Nếu URL tương lai chỉ chứa slug mà không có owner, contract đó sẽ không đủ định danh: hãy dùng ID hoặc thêm scope owner. Đừng đặt unique toàn cục chỉ vì thuận tiện nếu yêu cầu sản phẩm khác.
3. Schema tasks và index theo truy vấn dự kiến
Tạo database/migrations/2026_09_22_000200_create_tasks_table.php:
<?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('tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained()->restrictOnDelete();
$table->foreignId('assignee_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('title', 120);
$table->text('description')->nullable();
$table->string('status', 20)->default('todo');
$table->string('priority', 10)->default('normal');
$table->timestamp('due_at')->nullable();
$table->timestamps();
$table->index(['project_id', 'status', 'id']);
$table->index(['assignee_id', 'due_at']);
});
}
public function down(): void
{
Schema::dropIfExists('tasks');
}
};Index (project_id, status, id) hỗ trợ hướng truy vấn task theo project và status, có thứ tự ID ổn định. Index (assignee_id, due_at) phục vụ danh sách việc được giao theo hạn. Đây là giả thuyết thiết kế, chưa phải bằng chứng truy vấn nhanh; đến bài hiệu năng cần xem execution plan và dữ liệu thực tế.
Status và priority ở đây là cột string có default, không có CHECK giới hạn tập giá trị. Validation của ứng dụng vẫn cần kiểm soát chúng; một câu SQL trực tiếp có thể ghi giá trị khác. SQLite không áp giới hạn độ dài VARCHAR giống mọi database khác, nên không bỏ validation title 120 ký tự chỉ vì schema ghi string(..., 120).
4. Chạy migration có kiểm soát
Trước lệnh ghi, xác nhận project, connection và database đích. Với TaskFlow local mới và database có thể phục hồi:
php artisan migrate:status
php artisan migrate --pretend
php artisan migrate
php artisan migrate:status
--pretend giúp xem SQL dự kiến nhưng không phải sandbox cho mọi tác dụng phụ PHP tự viết trong migration. Các migration ở bài này chỉ chứa thao tác schema. Khi migration đã được chia sẻ hoặc chạy production, tạo migration mới để đổi schema thay vì sửa lịch sử rồi hy vọng migrate chạy lại.
Theo tài liệu Laravel migrations, rollback mặc định xử lý batch gần nhất; --step giới hạn số migration. down() xóa bảng không phục hồi dữ liệu từng có trong bảng. Migrate lại chỉ tạo schema rỗng.
5. Thử rollback trong database dùng một lần
Đoạn test độc lập dưới đây đặt SQLite :memory: và bỏ DB_URL trước khi mở connection. Tạo tests/Feature/SchemaRollbackTest.php:
<?php
namespace Tests\Feature;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class SchemaRollbackTest extends TestCase
{
public function test_task_migration_round_trip_in_memory(): void
{
config([
'database.default' => 'sqlite',
'database.connections.sqlite.database' => ':memory:',
'database.connections.sqlite.url' => null,
'database.connections.sqlite.foreign_key_constraints' => true,
]);
DB::purge('sqlite');
$this->artisan('migrate', ['--force' => true])->assertSuccessful();
$this->assertTrue(Schema::hasTable('tasks'));
$this->artisan('migrate:rollback', ['--step' => 1, '--force' => true])->assertSuccessful();
$this->assertFalse(Schema::hasTable('tasks'));
$this->assertTrue(Schema::hasTable('projects'));
$this->artisan('migrate', ['--force' => true])->assertSuccessful();
$this->assertTrue(Schema::hasTable('tasks'));
}
}Ở mốc bài 8, tasks là migration cuối nên rollback một bước xóa tasks và giữ projects. Khi thêm migration mới ở các bài sau, không tiếp tục giả định migration cuối luôn là tasks; test round-trip cần khóa vào phạm vi migration của mốc này hoặc điều chỉnh theo lịch sử mới.
Bản thực hành TaskFlow hiện có test tương đương cùng ba kiểm tra: task không được tham chiếu project thiếu, slug không được trùng trong cùng owner, và project còn task không được xóa. Toàn suite pass 18 test, 52 assertions. Các thao tác rollback được thực hiện trong SQLite bộ nhớ; không xóa bảng ở database local trên đĩa. Chưa có kiểm chứng MySQL/PostgreSQL hoặc lock DDL production.
6. Rollback kỹ thuật không thay thế kế hoạch phục hồi
Trước thay đổi production, cần backup có thử restore, thời gian bảo trì nếu cần, kiểm tra dữ liệu vi phạm constraint và khả năng app cũ/mới cùng đọc schema. Với thay đổi lớn, cân nhắc thêm cột mới, chuyển dữ liệu theo lô, đổi code rồi mới bỏ cột cũ ở lần deploy sau. Không chạy migrate:fresh, refresh hoặc reset trên dữ liệu cần giữ.
Nếu thêm cột bắt buộc vào bảng có dữ liệu, phải xác định giá trị cho record cũ. Nếu thêm unique, xử lý trùng trước. Nếu thêm index lớn, đánh giá lock và tài nguyên trên đúng database engine. Một test SQLite xanh không trả lời các câu hỏi vận hành đó.
7. Bài tập
Giải thích tại sao down của tasks phải chạy trước projects. Thêm test cho hai owner khác nhau cùng slug và test xóa assignee làm null thay vì xóa task. Ghi rõ dữ liệu nào mất nếu rollback tasks. Chỉ tiếp tục khi bạn phân biệt được tạo lại bảng với khôi phục dữ liệu.
Điều hướng: Bài 7 · Lộ trình. Bài tiếp theo dùng factory và seeder để tạo dữ liệu thực hành có quan hệ.




Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.