404 Not Found

404 Not Found


nginx

Banco de Dados e Sistema de Migrações do Laravel

Migrações são o "controle de versão do banco de dados" do Laravel—elas gerenciam estruturas de banco de dados assim como o Git gerencia código, para que as equipes não precisem mais se preocupar com estruturas de tabela dessincronizadas.

1. O Que Você Vai Aprender


2. Uma História Real de um DBA

(1) Problema: Executar SQL manualmente causa incidentes em produção

Charlie executou manualmente uma consulta ALTER TABLE orders ADD COLUMN discount DECIMAL(8,2) no ambiente de produção, mas esqueceu de incluir o valor padrão—como resultado, a coluna "discount" para todos os 2 milhões de registros foi definida como NULL, causando a queda do módulo de relatórios por duas horas. Para piorar, Bob adicionou um campo localmente sem avisar Charlie, e quando o código foi implantado, imediatamente acionou um erro SQL.

(2) Soluções para Migrações do Laravel

As migrações do Laravel usam código PHP para descrever alterações no banco de dados; as equipes compartilham arquivos de migração, e a ordem de execução é rastreada automaticamente—após todos executarem php artisan migrate, as estruturas do banco de dados ficam completamente idênticas.

BASH
php artisan make:migration add_discount_to_orders_table
# Cria um arquivo de migração com timestamp
php artisan migrate
# Executa todas as migrações pendentes em ordem

(3) Resultado

Depois que Charlie substituiu o SQL manual por migração, novos campos devem ter valores padrão especificados (que podem ser detectados durante revisões de código). Quando Bob envia suas alterações locais para o Git, Charlie as sincroniza com um único comando, resultando em zero incidentes.


3. Fundamentos de Migração

(1) Criar um arquivo de migração

BASH
# Criar uma migração
php artisan make:migration create_shops_table

# Com dica de nome de tabela
php artisan make:migration create_shops_table --create=shops

# Adicionar colunas a uma tabela existente
php artisan make:migration add_status_to_shops_table --table=shops

(2) Estrutura do Arquivo de Migração

PHP
// database/migrations/2024_01_15_000000_create_shops_table.php
return new class extends Migration
{
    public function up(): void
    {
        Schema::create('shops', function (Blueprint $table) {
            $table->id();
            $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->string('slug')->unique();
            $table->text('description')->nullable();
            $table->enum('status', ['active', 'suspended', 'closed'])->default('active');
            $table->decimal('revenue', 12, 2)->default(0);
            $table->timestamps();
            $table->softDeletes();
        });
    }

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

(3) Comandos de Migração

Comando Função
migrate Executar a migração que não foi executada
migrate:rollback Reverter a migração anterior
migrate:refresh Reverter Tudo + Re-executar
migrate:fresh Excluir banco de dados + Re-executar
migrate:status Ver Status da Migração
migrate:reset Reverter todas as migrações

(1) ▶ Exemplo: Criando e Executando uma Migração

BASH
# Criar migração
php artisan make:migration create_shops_table

# Executar migrações pendentes
php artisan migrate
# 2024_01_15_000000_create_shops_table .............. done

# Verificar status da migração
php artisan migrate:status
# Ran?   Migration
# Yes    0001_01_01_000000_create_users_table
# Yes    2024_01_15_000000_create_shops_table
# No     2024_01_16_000000_create_products_table

Saída:

TEXT
# Comando executado com sucesso

4. Schema Builder

(1) Tipos de Coluna Comuns

Método Tipo no Banco Descrição
id() BIGINT UNSIGNED AUTO_INCREMENT Chave Primária
foreignId('x') BIGINT UNSIGNED Chave Estrangeira
string('name', 255) VARCHAR String
text('content') TEXT Texto Longo
integer('count') INT Inteiro
decimal('price', 8, 2) DECIMAL(8,2) Decimal Exato
boolean('active') TINYINT(1) Booleano
enum('status', [...]) ENUM Enumeração
json('metadata') JSON Dados JSON
timestamp('published_at') TIMESTAMP Timestamp
softDeletes() TIMESTAMP NULL Soft Delete

(2) Índices e Restrições

PHP
Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $table->foreignId('shop_id')->constrained()->cascadeOnDelete();
    $table->string('order_number')->unique();
    $table->decimal('total', 12, 2);

    // Índices
    $table->index('shop_id');          // Índice simples
    $table->index(['shop_id', 'status']); // Índice composto
    $table->unique(['tenant_id', 'order_number']); // Composto único

    // Restrições de chave estrangeira
    $table->foreignId('user_id')
        ->constrained('users')         // Nome de tabela personalizado
        ->cascadeOnDelete()            // Excluir relacionados ao excluir pai
        ->cascadeOnUpdate();           // Atualizar relacionados ao atualizar pai

    $table->timestamps();
});
Método de Restrição Função
unique() Índice Único
index() Índice Regular
constrained() Inferir Automaticamente Relação de Chave Estrangeira
cascadeOnDelete() Exclusão em Cascata
restrictOnDelete() Exclusão restrita (erro ocorre se há registros filhos)
nullOnDelete() Definir como NULL ao excluir registro pai

(1) ▶ Exemplo: Design de Chave Estrangeira Multi-Tenant do ShopMetrics

PHP
// database/migrations/create_tenants_table.php
Schema::create('tenants', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->string('domain')->unique();
    $table->enum('status', ['active', 'suspended', 'cancelled'])->default('active');
    $table->timestamps();
    $table->softDeletes();
});

// database/migrations/create_subscriptions_table.php
Schema::create('subscriptions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $table->foreignId('plan_id')->constrained()->cascadeOnDelete();
    $table->enum('status', ['active', 'past_due', 'cancelled'])->default('active');
    $table->timestamp('trial_ends_at')->nullable();
    $table->timestamp('ends_at')->nullable();
    $table->timestamps();

    $table->index(['tenant_id', 'status']);
});

Saída:

TEXT
// Execução bem-sucedida

5. Design de Tabelas Multi-tenant

(1) Tabelas Principais do ShopMetrics

100%
timeline
    title Linha do Tempo de Migrações do ShopMetrics
    Create tenants : tabela tenants
    Create plans : tabela plans
    Create users : tabela users com tenant_id
    Create subscriptions : tabela subscriptions
    Create shops : tabela shops com tenant_id
    Create products : tabela products com shop_id
    Create categories : tabela categories
    Create category_product : tabela pivô
    Create orders : tabela orders com tenant_id + shop_id
    Create order_items : tabela order_items com order_id + product_id

(2) Sequência Completa de Migrações

Ordem Nome da Tabela Campos Principais
1 tenants id, name, slug, domain, status
2 plans id, name, price, features (JSON)
3 users id, tenant_id (FK), name, email, role
4 subscriptions id, tenant_id (FK), plan_id (FK), status
5 shops id, tenant_id (FK), name, slug, revenue
6 products id, shop_id (FK), name, price, sku
7 categories id, name, slug
8 category_product category_id (FK), product_id (FK)
9 orders id, tenant_id (FK), shop_id (FK), total, status
10 order_items id, order_id (FK), product_id (FK), qty, price

(1) ▶ Exemplo: Migração Completa da Tabela de Usuários do ShopMetrics

PHP
// database/migrations/create_users_table.php
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->enum('role', ['super_admin', 'tenant_owner', 'analyst', 'viewer'])
        ->default('viewer');
    $table->rememberToken();
    $table->timestamps();
    $table->softDeletes();

    $table->index(['tenant_id', 'role']);
    $table->index('email');
});

Saída:

TEXT
// Execução bem-sucedida

6. Estratégia de Migração em Produção

(1) Princípios de Migração Segura

Princípio Descrição Consequências da Violação
Novas entradas devem ter valor padrão ->default(0) ou ->nullable() Erro para registros existentes
Não exclua a coluna; exclua o código primeiro Pare de usar a coluna primeiro, depois exclua na próxima versão Código referencia coluna que não existe
Adicionar colunas à tabela usando after() Reduzir reconstrução de tabela especificando posições de coluna Duração do lock da tabela muito longa
Migração dentro de uma transação Propriedade withinTransaction Parcialmente bem-sucedido, parcialmente falho

(2) Diferenças entre MySQL e PostgreSQL

Funcionalidade MySQL PostgreSQL
Valor Padrão de Coluna Adição instantânea Requer Sobrescrita de Tabela
Coluna JSON json() json() + jsonb()
Enumeração enum() Sugestão string + CHECK
Índice de Texto Completo fullText() fullText() + GIN
Verificação de Chave Estrangeira Pode Ser Temporariamente Desativada Verificação Estrita

(1) ▶ Exemplo: Adicionando Seguramente uma Coluna a uma Tabela Grande em Produção

PHP
// Seguro: Adicionar coluna com valor padrão
Schema::table('orders', function (Blueprint $table) {
    $table->decimal('discount', 8, 2)
        ->default(0)
        ->after('total');
});

// Seguro: Tornar a coluna nullable primeiro
Schema::table('shops', function (Blueprint $table) {
    $table->string('phone')->nullable()->after('email');
});

// PERIGOSO: Remover coluna — faça em duas etapas
// Etapa 1: Esta versão — pare de usar a coluna no código
// Schema::table('shops', function (Blueprint $table) {
//     $table->dropColumn('legacy_field');
// });

Saída:

TEXT
// Execução bem-sucedida

7. Modificando Estruturas de Tabelas

(1) Modificar Coluna

BASH
composer require doctrine/dbal
# Necessário para modificar colunas existentes
PHP
Schema::table('shops', function (Blueprint $table) {
    $table->string('name', 100)->change();     // Alterar comprimento
    $table->renameColumn('desc', 'description'); // Renomear coluna
    $table->dropColumn('legacy_field');          // Excluir coluna
});

(2) Modificar o índice

PHP
Schema::table('orders', function (Blueprint $table) {
    $table->dropUnique('orders_order_number_unique');
    $table->unique(['tenant_id', 'order_number'], 'orders_tenant_order_unique');
});

(1) ▶ Exemplo: Guia Prático de Migração e Personalização do ShopMetrics

PHP
// database/migrations/2024_02_01_add_stripe_to_subscriptions.php
return new class extends Migration
{
    public function up(): void
    {
        Schema::table('subscriptions', function (Blueprint $table) {
            $table->string('stripe_id')->nullable()->unique()->after('id');
            $table->string('stripe_status')->nullable()->after('status');
            $table->timestamp('current_period_start')->nullable()->after('trial_ends_at');
            $table->timestamp('current_period_end')->nullable()->after('current_period_start');
        });
    }

    public function down(): void
    {
        Schema::table('subscriptions', function (Blueprint $table) {
            $table->dropColumn([
                'stripe_id', 'stripe_status',
                'current_period_start', 'current_period_end',
            ]);
        });
    }
};

Saída:

TEXT
// Execução bem-sucedida

8. Exemplo Completo: O Conjunto Completo de Migrações do ShopMetrics

PHP
// ============================================
// Completo: Migrações Principais do ShopMetrics
// Abrange: tabelas, chaves estrangeiras, índices, polimórficas
// ============================================

// Migração 1: Criar tabela plans
Schema::create('plans', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->decimal('price', 8, 2);
    $table->integer('shop_limit')->default(5);
    $table->integer('order_limit')->default(1000);
    $table->json('features')->nullable();
    $table->boolean('is_active')->default(true);
    $table->timestamps();
});

// Migração 2: Criar tabela tenants
Schema::create('tenants', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->string('domain')->unique();
    $table->foreignId('plan_id')->nullable()->constrained()->nullOnDelete();
    $table->enum('status', ['active', 'suspended', 'cancelled'])->default('active');
    $table->timestamps();
    $table->softDeletes();
    $table->index(['status', 'created_at']);
});

// Migração 3: Criar tabela shops
Schema::create('shops', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $table->string('name');
    $table->string('slug');
    $table->text('description')->nullable();
    $table->decimal('revenue', 12, 2)->default(0);
    $table->enum('status', ['active', 'suspended', 'closed'])->default('active');
    $table->timestamps();
    $table->softDeletes();
    $table->unique(['tenant_id', 'slug']);
    $table->index(['tenant_id', 'status']);
});

// Migração 4: Criar tabela products
Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->foreignId('shop_id')->constrained()->cascadeOnDelete();
    $table->string('name');
    $table->string('sku')->unique();
    $table->decimal('price', 10, 2);
    $table->integer('stock')->default(0);
    $table->boolean('is_active')->default(true);
    $table->timestamps();
    $table->softDeletes();
    $table->index(['shop_id', 'is_active']);
});

// Migração 5: Criar tabela orders
Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $table->foreignId('shop_id')->constrained()->cascadeOnDelete();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('order_number')->unique();
    $table->decimal('subtotal', 12, 2);
    $table->decimal('discount', 8, 2)->default(0);
    $table->decimal('total', 12, 2);
    $table->enum('status', ['pending', 'processing', 'completed', 'cancelled'])->default('pending');
    $table->json('metadata')->nullable();
    $table->timestamps();
    $table->index(['tenant_id', 'status']);
    $table->index(['shop_id', 'created_at']);
});

❓ Perguntas Frequentes

P Qual é a diferença entre migrate:fresh e migrate:refresh?
R fresh exclui o banco de dados e depois o reconstrói; é mais rápido mas resulta em perda completa de dados. refresh primeiro reverte as migrações e depois as aplica, executando down() seguido de up() em sequência. Use fresh no ambiente de desenvolvimento para melhor desempenho, mas nunca use nenhum desses comandos em ambiente de produção.
P Restrições de chave estrangeira afetam o desempenho?
R Sim. Restrições de chave estrangeira devem ser verificadas durante cada operação INSERT, UPDATE ou DELETE. Em cenários de alta concorrência, você pode considerar omitir restrições de chave estrangeira e, em vez disso, garantir a consistência dos dados na camada de aplicação.
P Como posso realizar uma migração com segurança em ambiente de produção?
R Primeiro, teste a migração no ambiente de staging; novas colunas devem ter valor padrão ou ser nullable; exclusão de colunas deve ser feita em duas etapas (primeiro remover referências de código, depois excluir a coluna na próxima versão); migre tabelas grandes fora dos horários de pico.
P Arquivos de migração podem ser modificados?
R Migrações que não foram executadas (ou seja, ainda não migradas) podem ser modificadas livremente; não modifique migrações que já foram executadas. Em vez disso, crie uma nova migração para implementar as alterações. Caso contrário, o comando migrate em outros ambinhos retornará um erro.
P Quais problemas de compatibilidade surgem ao migrar de SQLite para MySQL?
R O SQLite não suporta certas operações ALTER TABLE (como alterar tipos de coluna ou excluir colunas), então usar SQLite durante o desenvolvimento pode levar a limitações durante a migração. Recomendamos usar MySQL ou PostgreSQL em ambientes de produção.
P Como posso adicionar um índice a uma tabela grande existente sem bloquear a tabela?
R MySQL usa ALGORITHM=INPLACE LOCK=NONE (Laravel não suporta isso diretamente; você precisa usar DB::statement); PostgreSQL usa CONCURRENTLY ($table->index('col')->concurrently() é suportado no Laravel 11+).

📖 Resumo


📝 Exercícios

  1. Exercício Básico (⭐): Crie arquivos de migração para as três tabelas—tenants, plans e subscriptions—para o ShopMetrics, incluindo chaves estrangeiras e índices apropriados, e execute migrate para verificar se tudo está correto.

  2. Exercício Avançado (⭐⭐): Baseado na tabela orders existente, crie uma migração para adicionar uma coluna discount (valor padrão 0) e um índice composto (tenant_id + status), e escreva o método down() correspondente para garantir que a migração seja reversível.

  3. Desafio (⭐⭐⭐): Projetar uma migração de associação polimórfica para o ShopMetrics—permitir que tanto lojas quanto produtos tenham endereços (usando morphTo para a tabela addresses)—e implemente um design de chave estrangeira usando morphable_type e morphable_id.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%