Laravel 数据库迁移与 Seeder
Laravel 的数据库迁移(Migration)和数据填充(Seeder)系统让你可以像管理代码一样管理数据库结构,确保团队成员之间的数据库 schema 保持一致。本文详细介绍其使用方法。
创建迁移
// 创建 articles 表迁移
php artisan make:migration create_articles_table
// 在指定目录下创建
php artisan make:migration create_articles_table --path=database/migrations
// 创建已有表的迁移(用于修改表结构)
php artisan make:migration add_views_to_articles_table --table=articles
迁移文件编写
// database/migrations/2024_01_01_000001_create_articles_table.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("articles", function (Blueprint $table) {
$table->id(); // 主键自增
$table->foreignId("user_id") // 外键
->constrained("users")
->cascadeOnDelete();
$table->foreignId("category_id")
->nullable()
->constrained("categories")
->nullOnDelete();
$table->string("title", 200);
$table->string("slug", 200)->unique();
$table->text("content")->nullable();
$table->string("excerpt", 500)->nullable();
$table->unsignedInteger("views")->default(0);
$table->boolean("is_published")->default(false);
$table->timestamp("published_at")->nullable();
$table->timestamps(); // created_at + updated_at
$table->softDeletes(); // deleted_at
// 索引
$table->index(["category_id", "created_at"]);
$table->index("slug");
});
}
public function down(): void
{
Schema::dropIfExists("articles");
}
};
修改表结构
// 新增字段
Schema::table("articles", function (Blueprint $table) {
$table->string("cover_image", 500)->after("title")->nullable();
$table->boolean("is_featured")->default(false)->after("is_published");
});
// 修改字段(Laravel 10+)
Schema::table("articles", function (Blueprint $table) {
$table->string("title", 250)->change(); // 改长度
$table->renameColumn("excerpt", "summary"); // 重命名
});
// 删除字段
Schema::table("articles", function (Blueprint $table) {
$table->dropColumn(["cover_image", "is_featured"]);
});
// 重命名表
Schema::rename("old_articles", "articles");
运行迁移
// 运行所有迁移
php artisan migrate
// 只运行最近一批迁移
php artisan migrate:fresh // 删除所有表后重建(危险!)
php artisan migrate:refresh // 回滚所有迁移后再运行
// 回滚
php artisan migrate:rollback // 回滚最后一批
php artisan migrate:rollback --step=3 // 回滚最后 3 批
// 检查状态
php artisan migrate:status
// 强制运行(生产环境通常需要确认)
php artisan migrate --force
Seeder(数据填充)
// database/seeders/DatabaseSeeder.php
namespace Database\Seeders;
use App\Models\User;
use App\Models\Category;
use App\Models\Article;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
CategorySeeder::class,
UserSeeder::class,
ArticleSeeder::class,
]);
}
}
// database/seeders/CategorySeeder.php
namespace Database\Seeders;
use App\Models\Category;
use Illuminate\Database\Seeder;
class CategorySeeder extends Seeder
{
public function run(): void
{
$categories = [
["name" => "PHP", "slug" => "php"],
["name" => "Laravel", "slug" => "laravel"],
["name" => "Vue.js", "slug" => "vuejs"],
["name" => "Docker", "slug" => "docker"],
["name" => "数据库", "slug" => "database"],
];
foreach ($categories as $category) {
Category::create($category);
}
}
}
// database/seeders/ArticleSeeder.php
namespace Database\Seeders;
use App\Models\Article;
use App\Models\User;
use App\Models\Category;
use Illuminate\Database\Seeder;
class ArticleSeeder extends Seeder
{
public function run(): void
{
$users = User::all();
$categories = Category::all();
foreach (range(1, 50) as $i) {
Article::create([
"user_id" => $users->random()->id,
"category_id" => $categories->random()->id,
"title" => "示例文章标题 $i - " . fake()->sentence(),
"slug" => "article-$i-" . fake()->unique()->slug(),
"content" => fake()->paragraphs(5, true),
"is_published" => fake()->boolean(80), // 80% 概率发布
"views" => fake()->numberBetween(0, 10000),
]);
}
}
}
// 运行 seeder
php artisan db:seed
php artisan db:seed --class=CategorySeeder
php artisan db:seed --class=CategorySeeder --force
Factory(工厂)
// database/factories/ArticleFactory.php
namespace Database\Factories;
use App\Models\Article;
use App\Models\User;
use App\Models\Category;
use Illuminate\Database\Eloquent\Factories\Factory;
class ArticleFactory extends Factory
{
protected $model = Article::class;
public function definition(): array
{
return [
"user_id" => User::factory(), // 自动创建关联用户
"category_id" => Category::factory(),
"title" => $this->faker->sentence(6),
"slug" => $this->faker->unique()->slug(),
"content" => $this->faker->paragraphs(5, true),
"excerpt" => $this->faker->sentence(),
"is_published" => true,
"views" => $this->faker->numberBetween(0, 10000),
"published_at" => $this->faker->dateTimeBetween("-1 year"),
];
}
// 状态方法
public function draft()
{
return $this->state(fn (array $attributes) => [
"is_published" => false,
"published_at" => null,
]);
}
public function featured()
{
return $this->state(fn (array $attributes) => [
"views" => 10000,
]);
}
}
// 使用 Factory
use App\Models\Article;
// 创建 1 篇
$article = Article::factory()->create();
// 创建 10 篇
$articles = Article::factory()->count(10)->create();
// 创建 5 篇草稿
$drafts = Article::factory()->count(5)->draft()->create();
// 创建带关联的
$article = Article::factory()->for(User::factory())->create();
常见问题
- Q: migrate:fresh 和 migrate:refresh 有什么区别?
A:migrate:fresh 直接删除所有表后重新创建,不运行 down() 方法;migrate:refresh 运行所有迁移的 down() 再 up()。两个都会丢失数据,生产环境禁用。 - Q: 迁移文件中的 foreignId 和单独定义 index 有什么区别?
A:foreignId() 会自动创建外键约束(BEFORE/AFTER SQL),同时在外键字段上建索引。如果用 $table->unsignedBigInteger("user_id") 则需手动加索引。 - Q: Seeder 和 Factory 哪个更好?
A:Seeder 适合创建固定的初始数据(如管理员账号、默认分类);Factory 适合批量生成随机测试数据。两者配合使用效果最佳。 - Q: 已有数据的表想加索引会影响线上吗?
A:会。ALTER TABLE 加索引会锁表(InnoDB 大表可能锁几秒到几分钟)。生产环境建议使用 pt-online-schema-change 或 gh-ost 工具进行在线 DDL 操作。 - Laravel 数据填充官方文档
- Laravel 数据库事务与并发控制详解