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:freshvemigrate:refreshproduction'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