Mobil uygulamada veri kaydetmek, bilgisayardaki kadar rahat değil. Kullanıcı asansörde, metroda ya da çekimin zayıf olduğu bir kafede olabilir. Bağlantı bir anlığına kopup geri gelebilir.
Tek bir şey kaydediyorsanız sorun yok. Kaydedilemezse tekrar denersiniz, biter. Asıl dert, birbirine bağlı birkaç kaydı arka arkaya yazmanız gerektiğinde başlar.
Sorun: Orphan (Sahipsiz) Veri
Bir örnekle gidelim. Bir sınav uygulaması düşünün. Kullanıcı sınavı bitirince iki şey kaydedilmeli:
- Session ("Ahmet bu sınava girdi")
- Answers / cevaplar ("Ahmet şu soruya şu cevabı verdi")
Bu ikisi arasında bir parent–child (ebeveyn–çocuk) ilişkisi var: session parent, cevaplar ise ona bağlı child kayıtlar. Cevaplar olmadan session'ın hiçbir anlamı yok.
Şimdi şöyle bir şey olduğunu düşünün: Parent kayıt başarılı oldu (session açıldı), ama tam o sırada bağlantı koptu ve child kayıtlar (cevaplar) yazılamadı. Geriye ne kaldı? Boş bir session. Ahmet sınava girmiş görünüyor ama tek bir cevabı bile yok.
İşte buna orphan data (sahipsiz / yetim veri) diyoruz: zincirin bir ucu yazılmış, diğer ucu kopmuş. Sistem hata bile vermez; sessizce yanlış bir veri tutar. En tehlikeli kısmı da bu: fark etmesi zor.
Aynı durumun daha kötüsü e-ticarette olur:
| Olan | Olmayan | Sonuç | | --- | --- | --- | | Ödeme alındı | Order (sipariş) oluşturulmadı | Müşteri parayı ödedi, ama sistemde siparişi yok. |
Yani sadece bir veri hatası değil; doğrudan para meselesi.
Yaygın ama Yanlış Çözüm: Client-Side Orchestration
İlk akla gelen şey şudur: "İki insert'i client tarafında sırayla yaparım — ilkini bekler, sonra ikincisini yazarım." Bunun adı client-side orchestration (kaydetme sırasını client'ın yönetmesi).
// ⚠️ Bu kod mobilde tehlikeli — açıklayacağım
const { data: session } = await supabase
.from("exam_sessions")
.insert({ exam_id })
.select()
.single();
// Tam burada bağlantı koparsa: session var, cevaplar yok.
await supabase
.from("answers")
.insert(answers.map((a) => ({ ...a, session_id: session.id })));
Buradaki sorun şu: iki insert'in arası açık. İlki sunucuya gidip döndü, ikincisi daha yola çıkmadı. Tam o boşlukta client tünele girerse, session kalıcı olarak yazılmış ama cevaplar hiç gönderilmemiş olur. Elinizde bir orphan kalır.
"Hata olursa ilk kaydı silerim" demek de işe yaramaz. Çünkü silmek için yine bir network isteği gerekir. O an bağlantı zaten yoksa, silme işlemi de yapılamaz. Sorunu çözmüş olmazsınız, bir kat aşağı itersiniz.
Kısacası: bağlantının garanti olmadığı bir ortamda "büyük ihtimalle çalışır" yeterli değildir.
Çözüm: Atomicity ve RPC
İhtiyacımız olan kavramın adı atomicity (atomiklik). Tek cümleyle:
Ya hepsi kaydedilsin, ya hiçbiri. Arada "yarım" diye bir durum olmasın.
Veritabanı dünyasında bunun karşılığı transaction'dır. Bir transaction içindeki işlemler tek bir bütün gibi davranır: hepsi başarılı olursa commit edilir, biri bile patlarsa hepsi birden geri alınır (rollback).
Bunu nasıl uyguluyoruz? İki ayrı insert'i tek bir işleme dönüştürerek. Client "şunu yaz, sonra bunu yaz" demek yerine sadece "bu session'ı kaydet" der. Gerisini server halleder — ya tamamını yapar ya da bir şey ters giderse yaptığı kısmı da geri alır. Client'ın tünele girmesi, kapanması, bağlantının gitmesi artık fark etmez; çünkü client tek bir istek gönderdi, o kadar.
Supabase'de bunun aracı RPC (Remote Procedure Call). Server'da bir PL/pgSQL fonksiyonu yazarsınız; bu fonksiyon kendi içinde tek bir transaction olarak çalışır.
Nasıl Yapılıyor?
1. Server Tarafı (PL/pgSQL Fonksiyonu)
Bu fonksiyon cevapları alır, önce session'ı açar, sonra cevapları o session'a bağlar. Hepsi tek transaction içinde:
create or replace function public.submit_session(p_answers jsonb)
returns uuid
language plpgsql
as $$
declare
v_session_id uuid;
begin
-- 1) Önce parent kaydı (session) aç.
-- Kullanıcının kim olduğunu client'tan değil, server'dan okuyoruz (auth.uid).
-- Zamanı da server'dan alıyoruz (now), client'ın saati yanlış olabilir.
insert into exam_sessions (user_id, started_at)
values (auth.uid(), now())
returning id into v_session_id;
-- 2) Sonra child kayıtları (cevapları) bu session'a bağla.
-- Client'tan gelen cevap listesini tek tek satırlara açıyoruz.
insert into answers (session_id, question_id, selected_option)
select
v_session_id,
(elem ->> 'question_id')::uuid,
(elem ->> 'selected_option')::text
from jsonb_array_elements(p_answers) as elem;
-- 3) Buraya kadar geldiyse her şey yazıldı; transaction commit olur.
-- Yukarıda bir şey patlarsa hiçbiri yazılmaz; rollback olur. Orphan imkansız.
return v_session_id;
end;
$$;
Burada üç önemli nokta var:
| Ne kullandık | Ne işe yarıyor |
| --- | --- |
| auth.uid() | Kullanıcının kim olduğunu server'dan okur. Client yalan söyleyemez. |
| now() | Zamanı server'dan alır. Client'ın saati yanlış olsa bile kayıt doğru olur. |
| jsonb_array_elements() | Client'tan gelen cevap listesini (jsonb) tek tek satırlara açar. |
2. Client Tarafı (TypeScript)
Client'ta artık tek bir çağrı yeterli. Sıralama yok, bekleme yok:
async function submitSession(answers) {
// Tek istek. Ya hepsi yazılır ya hiçbiri.
const { data: sessionId, error } = await supabase.rpc("submit_session", {
p_answers: answers,
});
if (error) {
// Buraya düştüyse hiçbir şey yazılmadı (rollback).
// Rahatça retry edebilirsiniz, orphan riski yok.
throw error;
}
return sessionId;
}
İki yaklaşımı yan yana koyalım:
| | Client-Side Orchestration | RPC + Transaction | | --- | --- | --- | | Kaç network isteği | Her insert için ayrı | Tek sefer | | Orphan riski | Yüksek | Yok | | Retry güvenli mi | Hayır (kısmi yazım olabilir) | Evet |
Özet: Ne Zaman Hangisi?
-
Tek bir insert yapıyorsanız (profil güncellemek, yorum eklemek gibi), standart
.insert()gayet yeterli. RPC'ye uğraşmaya gerek yok. -
Birbirine bağlı birden fazla insert yapıyorsanız ve biri olmazsa diğeri anlamsız kalıyorsa, iş mantığını server'a (RPC'ye) taşıyın.
Karar vermek için kendinize tek bir soru sorun:
"Bunlardan biri yazılır, diğeri yazılmazsa veri yanlış mı olur?"
Cevabınız "evet" ise, o işi client'a bırakmayın. Bir transaction'ın içinde, server'da yapın.
Mobilde bağlantı her zaman vardır diye düşünemezsiniz. O yüzden bütünlüğü kontrol edemediğiniz client'a değil, güvenebileceğiniz server'a emanet edin.