Sipariş kaydedildi ama ürünler kaydedilmedi. Ne yapmalı?
Sipariş kaydedildi ama ürünler kaydedilmedi. Ne yapmalı?
Senaryoyu düşün: checkout’unuz siparişi oluşturur, stoğu azaltır ve ödemeyi kaydeder. Üç işlem, ardışık olarak tümü bir veritabanında gerçekleşiyor.
Bu süreçte bir hata meydana gelebilir. Veritabanı çökebilir, bir doğrulama hatası ortaya çıkabilir ya da ödeme API’si zaman aşımına uğrayabilir.
Sonuç? Sipariş oluşturulmuş, stok azaltilmiş ama ödeme kaydedilmemiş. Ürünleri bedava satmış olursunuz. 🎉
Bu, tutarsız veri kabusu. Çözümü ise basit: transaction.
Görünüşte zararsız olan kod (ama zararlı)
Görünüşte zararsız olan kod (ama zararlı)
Temiz görünen bir controller örneği:
public function store(Request $request)
{
$pedido = Pedido::create($request->only('cliente_id', 'total'));
foreach ($request->itens as $item) {
$pedido->itens()->create($item);
Produto::find($item['produto_id'])->decrement('estoque', $item['qtd']);
}
Pagamento::create([
'pedido_id' => $pedido->id,
'valor' => $pedido->total,
]);
return response()->json($pedido);
}
Temiz görünüyor, değil mi? Ancak sorun şu: her bir satır veritabanına anında kaydediliyor.
Ödeme oluşturulurken bir sorun çıkarsa, sipariş ve ürünler zaten kaydedilmiş durumda. Geri dönüş yok. Veritabanı, yarım bir işlemle dolup taşıyor.
Arkasındaki kavram: atomiklik
Arkasındaki kavram: atomiklik
Bir transaction bu durumu basit bir fikirle çözüyor: her şey ya tamam ya da hiçbiri.
Ya tüm işlemler başarılı olur ve kalıcı olarak kaydedilir (commit), ya da herhangi biri başarısız olursa veritabanı her şeyi, hiç olmamış gibi geri alır (rollback).
Bu, bir banka transferinin aynı ilkesidir: “hesabımdan çıktı ama senin hesabına girmedi” durumu yoktur. Ya tamamen gerçekleşir ya da gerçekleşmez.
Çözüm: DB::transaction()
Çözüm: DB::transaction()
Laravel bunu son derece kolay hale getirir. Kodu bir DB::transaction() içine sararsınız ve hepsi bu kadar:
use Illuminate\Support\Facades\DB;
public function store(Request $request)
{
$pedido = DB::transaction(function () use ($request) {
$pedido = Pedido::create($request->only('cliente_id', 'total'));
foreach ($request->itens as $item) {
$pedido->itens()->create($item);
Produto::find($item['produto_id'])->decrement('estoque', $item['qtd']);
}
Pagamento::create([
'pedido_id' => $pedido->id,
'valor' => $pedido->total,
]);
return $pedido;
});
return response()->json($pedido);
}
Hepsi bu kadar. Eğer closure içindeki bir satır bir hata atarsa, Laravel her şeyi otomatik olarak geri alır. Hiçbir şey kaydedilmez.
Hata olmadan sonuna kadar çalışırsa, commit işlemi gerçekleşir. Ek olarak: closure’ın döndüğü değer (return $pedido) DB::transaction()‘ın dönüşü olur. Son derece kullanışlı.
Gördüğünüz gibi, try/catch, commit veya rollBack kullanmanıza gerek yok. Closure, işin kirli tarafını sizin için halleder.
Pratikte nasıl kullanılır
Pratikte nasıl kullanılır
Gördüğümüz checkout’ta: sipariş, ürünler, stok ve ödeme birbirleriyle birlikte yaşar ve ölür. Artık asla yetim bir sipariş kalmayacak.
Çoklu tablolarla kayıt sürecinde: kullanıcı, profil ve varsayılan ayarları oluşturun. Eğer profil işlemi başarısız olursa, kullanıcı yalnız başına veritabanında kalmaz.
Paketleme işlemlerinde: bir tabloya 500 satır işleyin. Eğer 480. satırda bir hata varsa, 479 kayıt tam olarak istemezsiniz. Ya hepsi girer, ya da dosyayı düzeltip yeniden gelir.
Manuel versiyon (kontrol gerektiğinde)
Manuel versiyon (kontrol gerektiğinde)
Bazen closure yeterli olmaz—hata durumuna bağlı olarak ne zaman commit veya rollback yapacağınızı belirlemeniz gerekir. Bu durumda, manuel versiyonu kullanın:
DB::beginTransaction();
try {
$pedido = Pedido::create($dados);
// ... diğer işlemler ...
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
report($e); // Hatanızı incelemek için kaydeder
throw $e; // Sorunun gizlice kaybolmaması için yeniden atılır
}
Bu yöntemin işleyişi aynı, ancak siz kontrol altındasınız. Dikkat edin: Bu versiyonu kullanırsanız, istediğiniz hatayı gizlemeyin. Geri sar ve sıradan bir şekilde devam etmek, hatayı saklar. Kaydedin ve yeniden fırlatma.
Şüphe durumunda, DB::transaction()‘ı tercih edin. Daha az kod, rollBack unutma ihtimali daha az.
Herkesi yakalayan tuzak
Herkesi yakalayan tuzak
Transaction sadece veritabanı içindir. Sadece bunun için.
Klasik hata, veritabanı olmayan işlemleri transaction içine koymak ve bunun da geri alınacağını düşünmektir:
DB::transaction(function () use ($pedido) {
Pedido::create($pedido);
Mail::to($cliente)->send(new PedidoConfirmado); // ⚠️ tehlike
Storage::put'nota.pdf', $conteudo); // ⚠️ tehlike
});
Eğer transaction işlemi bu işlemlerden sonra geri alınırsa, sipariş veritabanından silinir — ama e-posta zaten gönderilmiştir ve dosya zaten kaydedilmiştir. Geri alma, gerçek dünyada zamanı geri almaz.
Altın kural: transaction içerisinde yalnızca veritabanı işlemleri olsun. E-posta, dosya gönderme, API çağrıları gibi işlemleri commit işleminden sonra yapın.
Laravel, bunu DB::afterCommit() ile destekler (ve ShouldQueue uygulayan işlerinizi bu ayarlandıysanız otomatik olarak after_commit ile uyum sağlar). Bu şekilde, e-posta yalnızca commit başarılı olduğunda gönderilir.
Bonus: Peki şimdi ne olacak?
Bonus: Peki şimdi ne olacak?
Eğer Service Layer kullanıyorsanız, transaction genellikle service metodunun içinde yer alır — controller’da değil. Controller yalnızca düzenleme yapar; service tutarlılığı garanti eder.
Bu noktada retry (DB::transaction()’ın ikinci parametresi, deadlock’lar için faydalıdır) ve savepoint kullanarak iç içe transaction’lara göz atmayı unutmayın. Bir sonraki makalenin konusu. 😉
Sekmeyi kapatmadan önce
Sekmeyi kapatmadan önce
Birden fazla tabloya ardışık kayıt yaptıran bir akışınız varsa ve hala transaction kullanmıyorsanız, bu durumu gözden geçirmeniz için bir işarettir. Bu tür hatalar genellikle mutlu testlerde görünmez, üretimde gece yarısı ortaya çıkar.
Veritabanında yarım veri ile ilgili hiç şaşırdınız mı? Yorumlarda paylaşın — sizi güldürecek hikayeler vardır.
Bu yazı sizi kırık bir checkout’tan kurtardıysa, hala koruma olmadan üç create() yapan o arkadaşa paylaşın. 🙂
Kaynak: Orijinal Makale


