Laravel ile CQRS ve Event Sourcing
Büyüyen projelerde okuma ve yazma işlemlerini aynı model üzerinden yönetmek karmaşıklaşıyor. CQRS bu ikisini ayırarak her birini optimize etmenizi sağlıyor. Event Sourcing ise her değişikliği olay olarak kaydederek veri geçmişini takip etmenizi mümkün kılıyor.
CQRS Nedir?
CQRS (Command Query Responsibility Segregation), yazma (Command) ve okuma (Query) işlemlerini ayrı model ve mekanizmalarda yönetme yaklaşımı.
Neden ayıralım?
- Performans: Okuma ve yazma farklı optimize edilebilir. Okuma için denormalize view'lar, yazma için normalize tablolar.
- Sadelik: Karmaşık sorgular ve iş kuralları ayrışır.
- Esneklik: Okuma için Redis/Elasticsearch, yazma için MySQL kullanabilirsiniz.
Laravel'de CQRS Uygulaması
Command (Yazma İşlemi)
// app/Commands/CreatePostCommand.php
namespace App\Commands;
class CreatePostCommand
{
public function __construct(
public readonly string $title,
public readonly string $content,
public readonly int $authorId
) {}
}
Command Handler
// app/Handlers/CreatePostHandler.php
namespace App\Handlers;
use App\Models\Post;
use App\Commands\CreatePostCommand;
use App\Events\PostCreated;
class CreatePostHandler
{
public function handle(CreatePostCommand $command): Post
{
$post = Post::create([
'title' => $command->title,
'content' => $command->content,
'author_id' => $command->authorId,
]);
event(new PostCreated($post));
return $post;
}
}
Query (Okuma İşlemi)
// app/Queries/GetPostsQuery.php
namespace App\Queries;
use App\Models\Post;
class GetPostsQuery
{
public function latest(int $limit = 10)
{
return Post::with(['author', 'categories'])
->published()
->latest()
->limit($limit)
->get();
}
public function findBySlug(string $slug)
{
return Post::with(['author', 'categories', 'tags'])
->where('slug', $slug)
->firstOrFail();
}
}
Controller'da Kullanım
class PostController extends Controller
{
public function index(GetPostsQuery $query)
{
$posts = $query->latest();
return view('blog.index', compact('posts'));
}
public function store(Request $request, CreatePostHandler $handler)
{
$post = $handler->handle(
new CreatePostCommand(
title: $request->title,
content: $request->content,
authorId: auth()->id()
)
);
return redirect()->route('posts.show', $post);
}
}
Event Sourcing Nedir?
Event Sourcing, veritabanında son durumu saklamak yerine, o duruma ulaşan tüm olayları saklama yöntemi. Banka hesabı düşünün: bakiye saklamak yerine tüm işlemleri (yatırma, çekme) sırayla kaydetmek.
Avantajları
- Tam geçmiş: Her değişiklik kaydedilir, audit trail otomatik.
- Geri dönüş: Olayları tekrar oynatarak herhangi bir ana geri dönülebilir.
- Debug kolaylığı: "Bu veri nasıl bu hale geldi?" sorusunun cevabı olaylar zincirinde.
Laravel'de Event Sourcing
Event Sınıfı
// app/Events/PostCreated.php
namespace App\Events;
use App\Models\Post;
use Illuminate\Queue\SerializesModels;
use Illuminate\Foundation\Events\Dispatchable;
class PostCreated
{
use Dispatchable, SerializesModels;
public function __construct(
public readonly Post $post
) {}
}
Event Listener
// app/Listeners/LogPostCreated.php
namespace App\Listeners;
use App\Events\PostCreated;
use Illuminate\Support\Facades\Log;
class LogPostCreated
{
public function handle(PostCreated $event): void
{
Log::info('Yeni yazı oluşturuldu', [
'post_id' => $event->post->id,
'title' => $event->post->title,
'author_id' => $event->post->author_id,
]);
}
}
Event Store Tablosu
Olayları kalıcı saklamak için:
Schema::create('event_store', function (Blueprint $table) {
$table->id();
$table->string('aggregate_type');
$table->uuid('aggregate_id');
$table->string('event_type');
$table->json('payload');
$table->json('metadata')->nullable();
$table->integer('version');
$table->timestamp('created_at');
$table->index(['aggregate_type', 'aggregate_id']);
});
Event'i Kaydetme
class EventStore
{
public function store(string $aggregateType, string $aggregateId, string $eventType, array $payload): void
{
DB::table('event_store')->insert([
'aggregate_type' => $aggregateType,
'aggregate_id' => $aggregateId,
'event_type' => $eventType,
'payload' => json_encode($payload),
'version' => $this->nextVersion($aggregateType, $aggregateId),
'created_at' => now(),
]);
}
private function nextVersion(string $type, string $id): int
{
return DB::table('event_store')
->where('aggregate_type', $type)
->where('aggregate_id', $id)
->max('version') + 1;
}
}
CQRS + Event Sourcing Birlikte
Bu iki pattern birlikte çok güçlü:
- Command → İş kuralını çalıştırır → Event üretir
- Event → Event Store'a kaydedilir
- Event Listener → Read model'i günceller (denormalize)
- Query → Read model'den hızlıca okur
// Command Handler
class TransferMoneyHandler
{
public function handle(TransferMoneyCommand $command): void
{
// İş kuralı kontrolü
$account = $this->accounts->findById($command->fromAccountId);
$account->withdraw($command->amount);
// Event üret
$this->eventStore->store(
'Account',
$command->fromAccountId,
'MoneyTransferred',
[
'from' => $command->fromAccountId,
'to' => $command->toAccountId,
'amount' => $command->amount,
]
);
}
}
Ne Zaman Kullanmalı?
CQRS uygun:
- Okuma/yazma oranı çok farklı (çok okuma, az yazma)
- Farklı veri kaynakları kullanma ihtiyacı
- Karmaşık raporlama gereksinimleri
Event Sourcing uygun:
- Audit trail zorunlu (finans, sağlık, hukuk)
- Veri geçmişi önemli
- Temporal query ihtiyacı (geçmişteki durumu sorgulama)
Uygun değil:
- Basit CRUD uygulamalar
- Küçük projeler — karmaşıklığa değmez
Sonuç
CQRS ve Event Sourcing güçlü pattern'ler ama her proje için uygun değil. Laravel'in event sistemi, queue yapısı ve Eloquent ile bu pattern'leri pragmatik şekilde uygulayabilirsiniz. Önemli olan ihtiyacı doğru belirlemek — over-engineering'den kaçının.