Laravel: Sıfırdan İleri Seviye

Ders 6/17 1 saat 30 dk

Veritabanı Tasarımı ve Migration'lar

Veritabanı şeması tasarımı, migration'lar, Schema Builder ve best practices.

Veritabanı Tasarımı ve Migration'lar

Migration'lar, veritabanı şemanızın versiyon kontrolüdür. Ekip çalışmasında herkesin aynı veritabanı yapısına sahip olmasını sağlar.

🎯 Bu ders özellikle önemli! Veritabanı tasarımı projenin temelini oluşturur. İyi düşünülmüş bir şema, ileride birçok problemi önler.

Migration Oluşturma

# Temel migration
php artisan make:migration create_posts_table

# Tablo oluşturma (--create)
php artisan make:migration create_categories_table --create=categories

# Tablo düzenleme (--table)
php artisan make:migration add_avatar_to_users_table --table=users

# Model ile birlikte
php artisan make:model Post -m  # migration ile
php artisan make:model Post -mfs  # migration, factory, seeder ile

Migration Yapısı

id();  // BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
            $table->string('title');
            $table->text('content');
            $table->timestamps();  // created_at ve updated_at
        });
    }

    /**
     * Migration'ı geri al
     */
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Kolon Tipleri

Schema::create('example', function (Blueprint $table) {
    // Primary Key
    $table->id();                          // BIGINT UNSIGNED AUTO_INCREMENT
    $table->uuid('id')->primary();         // UUID primary key
    
    // Sayısal
    $table->integer('count');              // INT
    $table->unsignedInteger('positive');   // UNSIGNED INT
    $table->bigInteger('big_number');      // BIGINT
    $table->tinyInteger('status');         // TINYINT (-128 to 127)
    $table->unsignedTinyInteger('flag');   // UNSIGNED TINYINT (0-255)
    $table->decimal('price', 8, 2);        // DECIMAL(8,2) - 123456.78
    $table->float('rating', 3, 1);         // FLOAT(3,1) - 4.5
    $table->boolean('is_active');          // TINYINT(1)
    
    // String
    $table->string('name');                // VARCHAR(255)
    $table->string('title', 100);          // VARCHAR(100)
    $table->char('code', 4);               // CHAR(4) - Sabit uzunluk
    $table->text('description');           // TEXT (~65KB)
    $table->mediumText('content');         // MEDIUMTEXT (~16MB)
    $table->longText('body');              // LONGTEXT (~4GB)
    
    // Tarih/Zaman
    $table->date('birth_date');            // DATE
    $table->dateTime('published_at');      // DATETIME
    $table->timestamp('verified_at');      // TIMESTAMP
    $table->time('start_time');            // TIME
    $table->year('graduation_year');       // YEAR
    $table->timestamps();                  // created_at ve updated_at
    $table->softDeletes();                 // deleted_at (soft delete için)
    
    // JSON ve Binary
    $table->json('options');               // JSON
    $table->jsonb('settings');             // JSONB (PostgreSQL)
    $table->binary('photo');               // BLOB
    
    // Özel
    $table->enum('status', ['draft', 'published', 'archived']);
    $table->ipAddress('visitor_ip');       // IP adresi
    $table->macAddress('device_mac');      // MAC adresi
    $table->uuid('external_id');           // UUID
    $table->ulid('ulid');                  // ULID
    
    // Morphs (polymorphic ilişkiler için)
    $table->morphs('taggable');            // taggable_type ve taggable_id
    $table->nullableMorphs('imageable');   // Nullable morphs
});

Kolon Modifikatörleri

Schema::create('users', function (Blueprint $table) {
    $table->id();
    
    // Nullable - NULL değer alabilir
    $table->string('middle_name')->nullable();
    
    // Default değer
    $table->string('role')->default('user');
    $table->boolean('is_active')->default(true);
    $table->timestamp('email_verified_at')->nullable();
    
    // Unique constraint
    $table->string('email')->unique();
    $table->string('username')->unique();
    
    // Comment
    $table->integer('status')->comment('0: pasif, 1: aktif, 2: beklemede');
    
    // After (kolon sırası)
    $table->string('avatar')->after('email')->nullable();
    
    // First (ilk sıraya)
    $table->uuid('uuid')->first();
    
    // Unsigned
    $table->integer('count')->unsigned();
    
    // Auto increment
    $table->integer('order')->autoIncrement();
    
    // Virtual/Generated column
    $table->string('full_name')
          ->virtualAs("CONCAT(first_name, ' ', last_name)");
    
    $table->timestamps();
});

Index Oluşturma

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id');
    $table->foreignId('category_id');
    $table->string('title');
    $table->string('slug');
    $table->text('content');
    $table->enum('status', ['draft', 'published', 'archived']);
    $table->timestamp('published_at')->nullable();
    $table->timestamps();
    
    // Tek kolon index
    $table->index('status');
    $table->index('published_at');
    
    // Unique index
    $table->unique('slug');
    
    // Composite index (çoklu kolon)
    $table->index(['status', 'published_at']);
    $table->index(['user_id', 'created_at']);
    
    // Custom index ismi
    $table->index('title', 'posts_title_index');
    
    // Full-text index (MySQL)
    $table->fullText(['title', 'content']);
});

// Index silme
Schema::table('posts', function (Blueprint $table) {
    $table->dropIndex('posts_status_index');
    $table->dropUnique('posts_slug_unique');
});

Foreign Key (Yabancı Anahtar)

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    
    // Yöntem 1: foreignId (Laravel 7+, önerilen)
    $table->foreignId('user_id')->constrained();
    // Otomatik: users tablosuna id kolonuna referans
    
    // Yöntem 2: foreignId ile özel tablo
    $table->foreignId('author_id')->constrained('users');
    
    // Yöntem 3: foreignId ile cascade
    $table->foreignId('category_id')
          ->constrained()
          ->onUpdate('cascade')
          ->onDelete('cascade');
    
    // Yöntem 4: Nullable foreign key
    $table->foreignId('parent_id')
          ->nullable()
          ->constrained('posts')
          ->nullOnDelete();
    
    // Yöntem 5: Manuel tanım (eski yöntem)
    $table->unsignedBigInteger('team_id');
    $table->foreign('team_id')
          ->references('id')
          ->on('teams')
          ->onDelete('cascade');
    
    $table->timestamps();
});

// Foreign key silme
Schema::table('posts', function (Blueprint $table) {
    $table->dropForeign(['user_id']);
    // veya
    $table->dropForeign('posts_user_id_foreign');
});

onDelete Seçenekleri

// CASCADE - Parent silinince child'lar da silinir
$table->foreignId('user_id')
      ->constrained()
      ->onDelete('cascade');

// SET NULL - Parent silinince NULL yapılır (nullable olmalı)
$table->foreignId('category_id')
      ->nullable()
      ->constrained()
      ->nullOnDelete();  // onDelete('set null') ile aynı

// RESTRICT - Child varsa parent silinemez (varsayılan)
$table->foreignId('author_id')
      ->constrained('users')
      ->restrictOnDelete();

// NO ACTION - RESTRICT ile aynı (veritabanına bağlı)
$table->foreignId('team_id')
      ->constrained()
      ->onDelete('no action');

Tablo Düzenleme

// Kolon ekleme
Schema::table('users', function (Blueprint $table) {
    $table->string('avatar')->nullable()->after('email');
    $table->string('phone', 20)->nullable();
});

// Kolon değiştirme (doctrine/dbal gerekir)
// composer require doctrine/dbal
Schema::table('users', function (Blueprint $table) {
    $table->string('name', 100)->change();  // Uzunluk değiştir
    $table->string('email')->nullable()->change();  // Nullable yap
});

// Kolon yeniden adlandırma
Schema::table('users', function (Blueprint $table) {
    $table->renameColumn('name', 'full_name');
});

// Kolon silme
Schema::table('users', function (Blueprint $table) {
    $table->dropColumn('avatar');
    $table->dropColumn(['phone', 'address']);  // Birden fazla
});

// Tablo yeniden adlandırma
Schema::rename('posts', 'articles');

// Tablo silme
Schema::drop('posts');
Schema::dropIfExists('posts');

Migration Komutları

# Migration'ları çalıştır
php artisan migrate

# Durum kontrolü
php artisan migrate:status

# Geri al (son batch)
php artisan migrate:rollback

# Belirli sayıda geri al
php artisan migrate:rollback --step=3

# Tümünü geri al
php artisan migrate:reset

# Geri al ve tekrar çalıştır
php artisan migrate:refresh
php artisan migrate:refresh --seed

# Tümünü sil ve baştan çalıştır (DİKKAT: Tüm veri silinir!)
php artisan migrate:fresh
php artisan migrate:fresh --seed
⚠️ Production Uyarısı:
  • migrate:fresh ve migrate:refresh production'da ASLA kullanmayın!
  • Migration dosyalarını deploy ettikten sonra düzenlemeyin, yeni migration oluşturun.
  • Büyük tablolarda kolon değişiklikleri downtime'a neden olabilir.

Örnek: Blog Veritabanı Şeması

// 1. Categories tablosu
Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->text('description')->nullable();
    $table->foreignId('parent_id')
          ->nullable()
          ->constrained('categories')
          ->nullOnDelete();
    $table->integer('order')->default(0);
    $table->boolean('is_active')->default(true);
    $table->timestamps();
    
    $table->index(['is_active', 'order']);
});

// 2. Posts tablosu
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->foreignId('category_id')->nullable()->constrained()->nullOnDelete();
    $table->string('title');
    $table->string('slug')->unique();
    $table->text('excerpt')->nullable();
    $table->longText('content');
    $table->string('featured_image')->nullable();
    $table->enum('status', ['draft', 'published', 'archived'])->default('draft');
    $table->timestamp('published_at')->nullable();
    $table->unsignedInteger('view_count')->default(0);
    $table->timestamps();
    $table->softDeletes();
    
    $table->index(['status', 'published_at']);
    $table->fullText(['title', 'content']);
});

// 3. Tags tablosu
Schema::create('tags', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->timestamps();
});

// 4. Post-Tag pivot tablosu (Many-to-Many)
Schema::create('post_tag', function (Blueprint $table) {
    $table->id();
    $table->foreignId('post_id')->constrained()->cascadeOnDelete();
    $table->foreignId('tag_id')->constrained()->cascadeOnDelete();
    $table->timestamps();
    
    $table->unique(['post_id', 'tag_id']);
});

// 5. Comments tablosu
Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->foreignId('post_id')->constrained()->cascadeOnDelete();
    $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
    $table->foreignId('parent_id')
          ->nullable()
          ->constrained('comments')
          ->cascadeOnDelete();
    $table->string('author_name')->nullable();
    $table->string('author_email')->nullable();
    $table->text('content');
    $table->boolean('is_approved')->default(false);
    $table->timestamps();
    
    $table->index(['post_id', 'is_approved', 'created_at']);
});

Önemli Noktalar

  • Migration'lar veritabanı şemasının versiyon kontrolüdür
  • Schema Builder ile tablolar programatik oluşturulur
  • Foreign key'ler ilişkileri veritabanı seviyesinde zorlar
  • Index'ler sorgu performansını artırır
  • Soft delete'ler veri kaybını önler