HardТеория8 min

Миграции (Migrations)

Создание миграций, типы колонок, модификаторы, индексы, внешние ключи, переименование/удаление колонок, запуск миграций, squashing

Миграции -- это система версионирования схемы базы данных. Каждая миграция -- это класс с методами up() (применить) и down() (откатить). Миграции выполняются в хронологическом порядке и позволяют всей команде работать с одинаковой структурой БД.

Создание миграций

# Create a migration
php artisan make:migration create_flights_table

# Create a table migration (generates up/down boilerplate)
php artisan make:migration create_flights_table --create=flights

# Create an alter table migration
php artisan make:migration add_destination_to_flights_table --table=flights

# Create a model with migration
php artisan make:model Flight -m
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('flights', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('airline');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('flights');
    }
};

Типы колонок

Числовые типы

Schema::create('products', function (Blueprint $table) {
    $table->id();                          // BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
    $table->bigIncrements('id');           // Same as id()
    $table->bigInteger('votes');           // BIGINT
    $table->decimal('amount', 8, 2);       // DECIMAL(8,2)
    $table->double('amount');              // DOUBLE
    $table->float('amount', precision: 53); // FLOAT
    $table->increments('id');              // INT UNSIGNED AUTO_INCREMENT
    $table->integer('votes');              // INT
    $table->mediumIncrements('id');        // MEDIUMINT UNSIGNED AUTO_INCREMENT
    $table->mediumInteger('votes');        // MEDIUMINT
    $table->smallIncrements('id');         // SMALLINT UNSIGNED AUTO_INCREMENT
    $table->smallInteger('votes');         // SMALLINT
    $table->tinyIncrements('id');          // TINYINT UNSIGNED AUTO_INCREMENT
    $table->tinyInteger('votes');          // TINYINT
    $table->unsignedBigInteger('votes');   // UNSIGNED BIGINT
    $table->unsignedInteger('votes');      // UNSIGNED INT
    $table->unsignedMediumInteger('votes');
    $table->unsignedSmallInteger('votes');
    $table->unsignedTinyInteger('votes');
});

Строковые и текстовые типы

Schema::create('posts', function (Blueprint $table) {
    $table->char('code', 4);              // CHAR(4)
    $table->string('name', 255);           // VARCHAR(255)
    $table->tinyText('notes');             // TINYTEXT
    $table->text('description');           // TEXT
    $table->mediumText('content');         // MEDIUMTEXT
    $table->longText('body');              // LONGTEXT
});

Дата и время

Schema::create('events', function (Blueprint $table) {
    $table->date('birthday');              // DATE
    $table->dateTime('starts_at', precision: 0); // DATETIME
    $table->dateTimeTz('starts_at');       // DATETIME with timezone
    $table->time('sunrise', precision: 0); // TIME
    $table->timeTz('sunrise');             // TIME with timezone
    $table->timestamp('added_at', precision: 0); // TIMESTAMP
    $table->timestampTz('added_at');       // TIMESTAMP with timezone
    $table->timestamps(precision: 0);      // created_at, updated_at TIMESTAMP
    $table->timestampsTz(precision: 0);    // With timezone
    $table->softDeletes(precision: 0);     // deleted_at TIMESTAMP nullable
    $table->softDeletesTz(precision: 0);   // With timezone
    $table->year('birth_year');            // YEAR
});

Специальные типы

Schema::create('misc', function (Blueprint $table) {
    $table->binary('data');                // BLOB
    $table->boolean('confirmed');          // BOOLEAN (TINYINT(1))
    $table->enum('difficulty', ['easy', 'hard']); // ENUM
    $table->set('flavors', ['strawberry', 'vanilla']); // SET
    $table->geometry('positions');          // GEOMETRY
    $table->ipAddress('visitor');           // VARCHAR (for IP)
    $table->json('options');               // JSON
    $table->jsonb('options');              // JSONB (PostgreSQL)
    $table->macAddress('device');           // VARCHAR (for MAC)
    $table->morphs('taggable');            // taggable_type VARCHAR, taggable_id BIGINT UNSIGNED
    $table->nullableMorphs('taggable');    // Same, but nullable
    $table->ulidMorphs('taggable');        // taggable_type VARCHAR, taggable_id CHAR(26)
    $table->uuidMorphs('taggable');        // taggable_type VARCHAR, taggable_id CHAR(36)
    $table->rememberToken();               // remember_token VARCHAR(100) nullable
    $table->ulid('id');                    // CHAR(26)
    $table->uuid('id');                    // CHAR(36) or UUID type
    $table->foreignId('user_id');          // BIGINT UNSIGNED (for FK)
    $table->foreignIdFor(User::class);     // Same, inferred name
    $table->foreignUlid('user_id');        // CHAR(26) (for FK)
    $table->foreignUuid('user_id');        // CHAR(36) (for FK)
});

Модификаторы колонок

Schema::create('users', function (Blueprint $table) {
    $table->id();

    // Nullable
    $table->string('bio')->nullable();

    // Default value
    $table->integer('votes')->default(0);
    $table->boolean('active')->default(true);
    $table->json('settings')->default('{}');
    $table->timestamp('published_at')->nullable()->default(null);

    // Auto-increment starting value
    $table->id()->from(1000);

    // Comment
    $table->string('email')->comment('User email address');

    // After specific column (MySQL only)
    $table->string('phone')->after('email');

    // First column (MySQL only)
    $table->string('ssn')->first();

    // Invisible (MySQL only)
    $table->string('internal_code')->invisible();

    // Unsigned
    $table->integer('quantity')->unsigned();

    // Using expression as default
    $table->timestamp('created_at')->useCurrent();
    $table->timestamp('updated_at')->useCurrentOnUpdate();

    // Virtual/Stored generated columns
    $table->string('full_name')
        ->virtualAs("CONCAT(first_name, ' ', last_name)");
    $table->string('full_name')
        ->storedAs("CONCAT(first_name, ' ', last_name)");

    // Charset and collation (MySQL)
    $table->string('name')->charset('utf8mb4')->collation('utf8mb4_unicode_ci');
});

Индексы

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('email');
    $table->string('name');
    $table->string('phone')->nullable();
    $table->text('bio')->nullable();

    // Unique index
    $table->unique('email');

    // Regular index
    $table->index('name');

    // Composite index
    $table->index(['name', 'email']);

    // Named index
    $table->index('name', 'idx_users_name');

    // Full text index (MySQL/PostgreSQL)
    $table->fullText('bio');

    // Spatial index
    // $table->spatialIndex('location');

    // Inline index definition
    $table->string('email')->unique();
    $table->string('name')->index();
});

Удаление индексов

Schema::table('users', function (Blueprint $table) {
    // Drop by index name
    $table->dropIndex('idx_users_name');

    // Drop by column(s) - Laravel auto-generates the name
    $table->dropIndex(['name']);       // Drops: users_name_index
    $table->dropUnique(['email']);     // Drops: users_email_unique
    $table->dropFullText(['bio']);     // Drops: users_bio_fulltext

    // Drop primary key
    $table->dropPrimary('users_id_primary');
});

Внешние ключи (Foreign Keys)

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');

    // Method 1: Explicit foreign key
    $table->unsignedBigInteger('user_id');
    $table->foreign('user_id')
        ->references('id')
        ->on('users')
        ->onDelete('cascade')
        ->onUpdate('cascade');

    // Method 2: Shorthand with foreignId (recommended)
    $table->foreignId('user_id')->constrained();
    // Automatically: REFERENCES users(id)

    // Method 3: With options
    $table->foreignId('user_id')
        ->constrained()
        ->cascadeOnDelete()
        ->cascadeOnUpdate();

    // Method 4: Custom table/column
    $table->foreignId('author_id')
        ->constrained(table: 'users', column: 'id');

    // Nullable foreign key
    $table->foreignId('category_id')
        ->nullable()
        ->constrained()
        ->nullOnDelete();

    // Foreign key actions
    $table->foreignId('team_id')
        ->constrained()
        ->cascadeOnDelete()    // ON DELETE CASCADE
        ->cascadeOnUpdate();   // ON UPDATE CASCADE

    $table->foreignId('team_id')
        ->constrained()
        ->restrictOnDelete()   // ON DELETE RESTRICT
        ->restrictOnUpdate();  // ON UPDATE RESTRICT

    $table->foreignId('team_id')
        ->constrained()
        ->nullOnDelete();      // ON DELETE SET NULL

    $table->foreignId('team_id')
        ->constrained()
        ->noActionOnDelete();  // ON DELETE NO ACTION

    // UUID foreign key
    $table->foreignUuid('team_id')->constrained();

    // ULID foreign key
    $table->foreignUlid('team_id')->constrained();

    $table->timestamps();
});

Удаление внешних ключей

Schema::table('posts', function (Blueprint $table) {
    // Drop by convention name
    $table->dropForeign(['user_id']); // Drops: posts_user_id_foreign

    // Drop by explicit name
    $table->dropForeign('posts_user_id_foreign');

    // Disable/enable foreign key constraints
    Schema::disableForeignKeyConstraints();
    Schema::enableForeignKeyConstraints();

    // Or using withoutForeignKeyConstraints
    Schema::withoutForeignKeyConstraints(function () {
        // Perform operations that might violate FK constraints
    });
});

Изменение колонок

// Rename a column
Schema::table('users', function (Blueprint $table) {
    $table->renameColumn('from', 'to');
});

// Change column type/attributes
Schema::table('users', function (Blueprint $table) {
    $table->string('name', 50)->change();    // Change length
    $table->integer('votes')->unsigned()->default(1)->change(); // Change type & default
    $table->string('bio')->nullable()->change(); // Make nullable
});

// Drop columns
Schema::table('users', function (Blueprint $table) {
    $table->dropColumn('votes');

    // Drop multiple columns
    $table->dropColumn(['votes', 'avatar', 'location']);
});

// Rename table
Schema::rename('users', 'members');

// Drop table
Schema::drop('users');
Schema::dropIfExists('users');

Запуск миграций

# Run all outstanding migrations
php artisan migrate

# Run with seed
php artisan migrate --seed

# Rollback the last batch
php artisan migrate:rollback

# Rollback last N batches
php artisan migrate:rollback --step=3

# Rollback all migrations
php artisan migrate:reset

# Rollback and re-run all migrations
php artisan migrate:refresh
php artisan migrate:refresh --seed

# Drop all tables and re-run all migrations
php artisan migrate:fresh
php artisan migrate:fresh --seed

# Migration status
php artisan migrate:status

# Pretend (show SQL without running)
php artisan migrate --pretend

# Force in production
php artisan migrate --force

# Specific database connection
php artisan migrate --database=mysql

# Specific path
php artisan migrate --path=/database/migrations/2024_01_01

Squashing Migrations

Когда миграций становится слишком много, их можно "сжать" в один SQL-файл.

# Create a squash file (database/schema/mysql-schema.sql)
php artisan schema:dump

# Dump and prune (remove old migration files)
php artisan schema:dump --prune

При следующем migrate:fresh Laravel сначала выполнит schema dump, затем оставшиеся миграции.

Практический пример: полная миграция

<?php

declare(strict_types=1);

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', function (Blueprint $table) {
            // Primary key
            $table->id();

            // Foreign keys
            $table->foreignId('user_id')
                ->constrained()
                ->cascadeOnDelete();

            $table->foreignId('shipping_address_id')
                ->nullable()
                ->constrained('addresses')
                ->nullOnDelete();

            // Order data
            $table->string('order_number', 32)->unique();
            $table->enum('status', ['pending', 'processing', 'shipped', 'delivered', 'cancelled'])
                ->default('pending');
            $table->decimal('subtotal', 10, 2);
            $table->decimal('tax', 10, 2)->default(0);
            $table->decimal('shipping_cost', 10, 2)->default(0);
            $table->decimal('total', 10, 2);
            $table->string('currency', 3)->default('USD');

            // Payment
            $table->string('payment_method')->nullable();
            $table->string('payment_id')->nullable();
            $table->timestamp('paid_at')->nullable();

            // Notes
            $table->text('notes')->nullable();
            $table->json('metadata')->nullable();

            // Timestamps and soft deletes
            $table->timestamps();
            $table->softDeletes();

            // Indexes
            $table->index('status');
            $table->index('created_at');
            $table->index(['user_id', 'status']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('orders');
    }
};

Проверка существования

// Check if table exists
if (Schema::hasTable('users')) {
    // ...
}

// Check if column exists
if (Schema::hasColumn('users', 'email')) {
    // ...
}

// Check multiple columns
if (Schema::hasColumns('users', ['email', 'name'])) {
    // ...
}

// Get column type
$type = Schema::getColumnType('users', 'email'); // 'string'

// Get all column names
$columns = Schema::getColumnListing('users');

// Get all table names
$tables = Schema::getTables();

// Get all indexes
$indexes = Schema::getIndexes('users');

// Get foreign keys
$foreignKeys = Schema::getForeignKeys('orders');

Проверь себя

Для чего используется php artisan schema:dump --prune?

Какой метод создаёт внешний ключ с автоматическим определением таблицы и колонки?

В чём разница между storedAs() и virtualAs() для генерируемых колонок?

Что делает метод nullOnDelete() для внешнего ключа?

Что произойдёт при выполнении php artisan migrate:fresh?