Yanlışlıkla silinen kayıt geri gelir mi? Yumuşak silme, çöp kutusu ve kalıcı silme tasarımı
Cevap, silme anında hangi katmanın devrede olduğuna bağlı ve çoğu ekipte bu katmanlar hiç ayrıştırılmamıştır. Birbirine karışan dört ayrı mekanizma var: arayüzdeki birkaç saniyelik geri alma şeridi, kaydı listelerden kaldıran ama satırı tabloda bırakan yumuşak silme, çöp kutusunu belli bir gün sonra gerçekten boşaltan kalıcı silme işi ve en sonda yedekten dönüş. Bunları baştan ayırmazsanız "yanlışlıkla sildik, geri alır mısınız" talebi geldiğinde elinizdeki tek araç yedek olur. Yedek ise tek bir satır için tasarlanmış bir araç değildir.
Önce hangi kazayı kurtardığınıza karar verin
Dört senaryo var ve dördü farklı mekanizma ister.
Birincisi, kullanıcı yanlış satırı sildi ve üç saniye içinde fark etti. Burada lazım olan şey arayüzde bir geri alma düğmesi; veritabanı tarafında hiçbir şey yapılmasa bile çözülebilir.
İkincisi, üç ay önce silinen müşteri kaydı bir mutabakat sırasında geri lazım oldu. Bu bir arşiv ve denetim izi sorusu, geri alma sorusu değil.
Üçüncüsü, bir toplu işlem yanlış filtreyle çalıştı ve 4.000 satırı sildi. Kimse ertesi güne kadar fark etmedi. Bu, tek tek geri almanın işe yaramadığı, parti kimliğine göre topluca geri döndürme gereken durum.
Dördüncüsü, müşteri "hesabımı ve verimi silin" dedi. Burada istenen şey kurtarmanın tam tersi: verinin gerçekten, her kopyadan gitmesi.
Tek bir deleted_at kolonu bu dördünü çözmez. Ürün kararını tabloyla verin: senaryo, mekanizma, pencere, kimin yetkisi var.
Yumuşak silme: satırı bırakmak, görünürlüğü kaldırmak
En yaygın yaklaşım satırı yerinde bırakıp bir işaret koymak. Kolonu boolean değil, boş bırakılabilir bir zaman damgası yapın (deleted_at). Sebebi basit: saklama penceresini hesaplamanın, "30 günü dolanları boşalt" diyebilmenin tek yolu silinme anını bilmektir. Kimin sildiği de kaydın içinde değil, denetim izinde durmalı.
Kazanç gerçek. Faturaya bağlı ürün satırı, kapanmış bir destek kaydındaki temsilci, geçmiş siparişteki adres, hepsi yerinde kalır. Kaydı gerçekten silseniz bu ilişkilerin yarısı ya hata verir ya da boş isimle görünür.
Fatura da gerçek ve dört kalemde geliyor.
Fatura 1: her sorguya filtre borcu
Yumuşak silme uygulandığı andan itibaren sistemdeki her okuma "silinmişleri dışarıda bırak" sorumluluğunu üstlenir. Bu filtre bir yerde unutulur ve o yer genellikle en görünür yerdir: aylık rapor, Excel dışa aktarımı, yönetici paneli, arama sonuçları.
ORM'ler yardım ediyor ama sınırlarını bilmek gerekiyor. Hibernate'te @Where 6.3 sürümünde kullanımdan kaldırıldı, yerine aynı işi yapan @SQLRestriction geldi; belgeler bu kısıtların her zaman uygulandığını, kapatılamadığını ve parametre alamadığını açıkça söylüyor. 6.4 ile gelen @SoftDelete ise hâlâ incubating işaretli ve dönüştürücüyle zaman damgası birleşimi desteklenmiyor.
Rails tarafında daha ince bir tuzak var. default_scope belgelerine göre kapsam yeni kayıt oluştururken uygulanır (Article.new.published true döner), ancak güncelleme ve silme sorgularında varsayılan olarak uygulanmaz; bunu istiyorsanız all_queries: true demeniz gerekir. Yani yumuşak silme kapsamınız okumada çalışırken, toplu bir update_all silinmiş satırlara da dokunabilir.
Pratik kural: filtreyi uygulama kodunun iyi niyetine bırakmayın. Raporlama ve dışa aktarma katmanını ham tabloya değil, silinmişleri zaten dışarıda bırakan bir görünüme bağlayın. Ham tabloya erişimi sayılı yere verin.
Fatura 2: eşsizlik kısıtı artık yalan söylüyor
Klasik sahne: kullanıcı hesabını sildi, bir ay sonra aynı e-postayla geri dönmek istedi, sistem "bu e-posta kullanımda" dedi. Kayıt silinmiş görünüyor ama satır tabloda ve eşsiz indeks onu hâlâ sayıyor.
PostgreSQL'de çözüm kısmi eşsiz indeks. Belgeler bunu açıkça anlatıyor: indeks predikatını sağlayan satırlar arasında eşsizliği zorlar, sağlamayanları kısıtlamaz. Yani CREATE UNIQUE INDEX ... ON users (email) WHERE deleted_at IS NULL dediğinizde yalnızca canlı satırlar yarışır.
Aynı belgenin ikinci yarısı daha az biliniyor ve asıl sürpriz orada: planlayıcı, sorgunun WHERE koşulu indeks predikatını matematiksel olarak ima etmiyorsa bu indeksi kullanamaz. Sorgularınızda deleted_at IS NULL koşulunu açıkça taşımazsanız indeks ortada durur ama devreye girmez. Üstelik eşleşme çalışma anında değil planlama anında yapılıyor, bu yüzden parametreli koşullarla bu ima kurulamaz.
MySQL'de kısmi indeks yok. Oradaki yaygın çözüm, canlı satırlarda sabit bir değer, silinmiş satırlarda silinme zamanını taşıyan üretilmiş bir kolon açıp eşsiz indeksi o kolona kurmak. Daha çirkin, ama aynı işi görür.
Fatura 3: kaskad artık sizin işiniz
ON DELETE CASCADE yumuşak silmede çalışmaz. Müşteriyi silersiniz, siparişleri, adresleri, kullanıcı hesapları yerli yerinde durur ve bir sonraki listede karşınıza çıkar.
İki yol var. Birincisi açık kaskad: tek bir işlem içinde bağlı satırları da işaretlersiniz. İkincisi türetilmiş görünürlük: alt kayıt kendi bayrağını taşımaz, üst kayda bakılır. İkincisi her sorguya bir birleştirme ekler ve büyük tablolarda pahalıdır; birincisi hızlıdır ama geri almayı zorlaştırır, çünkü "bu kayıtlar hangi silme işlemiyle gitti" bilgisini kaybedersiniz.
Çözüm ikisinin ortası: açık kaskad yapın, ama her silme işlemine bir parti kimliği verin ve etkilenen satırlara onu yazın. Yanlış filtreyle çalışan toplu silmeyi geri almak, o kimlikle tek bir güncellemeye iner. Bu alan yokken aynı iş, zaman damgası aralığına bakıp tahmin yürütmek demektir.
Fatura 4: tablo şişer, kimse fark etmez
Silinen satır gitmediği için tablo ve indeksler büyümeye devam eder. Altı ay sonra en çok sorgulanan tablonun üçte biri kimsenin görmediği satırlardan oluşabilir. Kısmi indeksler (WHERE deleted_at IS NULL) canlı satırlara odaklanarak bunun bir kısmını kurtarır. Geri kalanı için belli bir yaştan sonra arşiv tablosuna taşıma işi kurun; veri modelini kurarken bu taşımanın mümkün olup olmadığını kontrol edin.
Çöp kutusu bir ürün özelliğidir, bir kolon değil
Yumuşak silme teknik bir detay, çöp kutusu ise kullanıcının gördüğü bir şey. Kullanıcı silinenleri görebilmeli, süreyi bilmeli ve geri almak için destek talebi açmak zorunda kalmamalı.
Pencereyi seçerken büyük ürünlerin ne yaptığına bakmak işe yarıyor. Google, 13 Ekim 2020'den itibaren Drive çöp kutusundaki dosyaları 30 gün sonra otomatik siliyor; yöneticilerin kullanıcının çöp kutusundan kaldırılan dosyaları geri getirebileceği ek bir pencere de 25 gün. Microsoft'ta silinen bir Entra ID kullanıcısı 30 gün askıda kalıyor ve tüm özellikleriyle geri alınabiliyor; süre dolduğunda kalıcı silme kendiliğinden başlıyor ve durdurulamıyor. GitHub'ın Enterprise Server belgeleri silinen deponun 90 gün diskte kaldığını söylüyor.
Aynı GitHub belgesinin devamı daha öğretici: geri yüklenen depo fork ağından kopuk döner, sürüm ekleri ve takım izinleri geri gelmez, üstelik silmeden sonra bir saate kadar geri yükleme mümkün olmayabilir. Buradan çıkan kural şu: geri yükleme nadiren birebir aynı nesneyi döndürür. Pencereyi yazdığınız yere, neyin geri gelmediğini de yazın. Kullanıcı bunu kurtarma anında öğrenirse ikinci bir sorun daha çıkar.
Üç saniyelik geri alma, onay penceresinden iyidir
"Emin misiniz?" diyaloğu kullanıcının okumayı bıraktığı ilk şeydir. Silmeyi engellemez, yalnızca bir tık ekler.
Daha iyi çalışan yaklaşım: işlemi hemen yapın, ekranda beş on saniye duran bir geri alma şeridi gösterin, asıl işi o süre dolduğunda kesinleştirin. Kullanıcı bunu "silmeden önce düşün" davranışına zorlamaz, çünkü düşünmesi gereken anı yanlış yere koymaz.
Geri alınabilirlik azaldıkça sürtünme artmalı. Tek satırı silmek tek tık olabilir; bir hesabı veya 4.000 satırı silmek için adın elle yazılması, ikinci bir kişinin onayı ya da gecikmeli bir uygulama makul. Nesne depolarında da benzer bir ayrım var: S3'te MFA delete gibi ayarlar tam da geri dönüşü olmayan işlemleri yavaşlatmak için duruyor. Kimin hangi kapsamda silebileceği ise yetkilendirme modelinin parçasıdır, ayrı bir bayrağın değil.
Silinen kaydın kimliğini yeniden kullanmayın
Microsoft'un kendi belgeleri bu tuzağı anlatıyor: silinen kullanıcıyı geri yüklemek yerine aynı adresle yeni bir kullanıcı açarsanız nesne kimliği değişir ve eski kimliği tutan sistemler erişim hatası vermeye başlar.
Sizin sisteminizde aynı şey daha sessiz olur. Silinen kaydın kimliği dışarıda yaşıyordur: entegrasyonun eşleme tablosunda, muhasebe yazılımında, geçen hafta gönderilmiş bir e-postadaki bağlantıda. O kimliği yeni bir kayda vermek, sessiz bir veri karışması üretir.
Doğrusu kaydı mezar taşına çevirmek. Satır durur, durumu silinmiştir, eski kimlikle gelen istek 404 yerine açık bir "bu kayıt kaldırıldı" cevabı alır ya da yerine geçen kayda yönlendirilir. Mükerrer kayıtları birleştirirken kullanılan mantığın aynısı.
Kalıcı silme tek bir DELETE değil, bir zincir
Çöp kutusu kendiliğinden boşalmaz. Zamanlanmış bir iş gerekir: parti parti çalışan, yarıda kesilirse baştan alınabilen, ne sildiğini kaydeden bir iş. Tasarımı arka plan işleri için geçerli olan kuralların aynısına tabi.
Asıl zorluk zincirin uzunluğu. Bir kaydı gerçekten silmek demek, ona bağlı satırlar, nesne depolarındaki dosyalar, arama indeksi, önbellek, veri ambarına gitmiş kopyalar, e-posta ve destek araçlarındaki izler demek. Zincirin ortasında duran bir silme en kötü durumdur: arama sonuçlarında görünen ama açıldığında bulunamayan kayıt.
Nesne deposu tarafında bir sürpriz var. AWS belgelerine göre sürümlemenin açık olduğu bir kovada versiyon belirtmeden yapılan DELETE nesneyi silmez, üzerine bir silme işaretçisi koyar; nesne sürüm olarak durmaya devam eder, GET isteği 404 ile birlikte x-amz-delete-marker: true başlığı döner. Gerçekten silmek için versiyon kimliğiyle silmek gerekir. Bu davranış bir yandan kurtarıcıdır, diğer yandan "sildik" sandığınız dosyaların hâlâ durduğu anlamına gelir.
Yedek bir satır kurtarma aracı değildir, son çaredir
Yönetilen veritabanlarında zaman noktasına dönüş iyi çalışıyor. Amazon RDS'te otomatik yedek saklama süresi 1 ile 35 gün arasında, işlem günlükleri beş dakikada bir S3'e gidiyor ve saklama penceresi içinde saniye hassasiyetinde geri dönüş yapılabiliyor (son beş dakika civarına kadar).
Buradaki tuzak teknik değil, prosedürel. Tek bir satır için üretim veritabanını geriye sarmazsınız, çünkü o esnada yazılan her şeyi de geri sarmış olursunuz. Yapılması gereken, yedekten ayrı bir örnek açıp satırı oradan çekmek. Bunun kaç dakika sürdüğünü olay günü öğrenmek pahalıya patlar; bir kez prova edin ve süreyi yazın.
Bir de yedek olduğunu sanma meselesi var. GitLab'ın 31 Ocak 2017 olay raporu bu konudaki en açık belge: bir mühendis replikasyonu onarırken yanlış sunucuda veri dizinini sildi, yaklaşık 300 GB'ın 4,5 GB'ı kaldı. Sonra sırayla beş kurtarma yolu denendi. LVM anlık görüntüleri 24 saatte bir alınıyordu, düzenli yedekler yalnızca birkaç byte'lık dosyalar üretiyordu, pg_dump yanlış sürüm ikilisiyle çalıştığı için sessizce başarısız oluyordu, Azure disk anlık görüntüleri veritabanı sunucularında açık değildi, S3 kovası boştu. Veriyi kurtaran şey, bir mühendisin başka bir iş için tesadüfen aldığı altı saatlik bir kopya oldu; o altı saat içinde oluşan veri (yaklaşık 5.000 proje, 5.000 yorum, 700 kullanıcı hesabı) geri gelmedi. Yedekleme ve felaket kurtarma tarafındaki temel kural burada da aynı: test edilmemiş yedek, yedek değildir.
Silinemeyen kayıtlar ve geri yüklemenin yan etkisi
Bazı kayıtlar silinemez. Kesilmiş fatura, muhasebe hareketi, ödeme kaydı yerinde kalmak zorundadır. Bu durumda yapılan iş silme değil anonimleştirmedir: kişisel alanlar sabit bir değerle değiştirilir, tutarlar ve ilişkiler korunur. Geri eşlenebilir bir dönüşüm kullanmayın; küçük bir değer uzayında hash, silme değil gizlemedir. Silme taleplerinin hukuki çerçevesi ayrı bir konu ve o tarafı hukuk danışmanınızla konuşun; mühendislik tarafında cevaplamanız gereken soru şudur: hangi tablo silinir, hangisi anonimleştirilir, hangi alan olduğu gibi kalır.
Bir de kimsenin aklına gelmeyen yan etki var. Yedekten geri yükleme, o yedeğin alındığı tarihten sonra silinmiş kayıtları geri getirir. Geri yükleme prosedürünüzde "silme kuyruğunu yeniden çalıştır" adımı yoksa, bir felaket kurtarma tatbikatı sessizce silinmiş verileri diriltir.
Test edilmeyen geri alma yoktur
Dört test yazın ve bunları her sürümde çalıştırın. Silinen kayıt listede, raporda, aramada ve dışa aktarımda görünmüyor mu? Geri alınan kayıt bağlı satırlarıyla birlikte dönüyor mu? Silinmiş bir kullanıcının e-postasıyla yeniden kayıt olunabiliyor mu? Kalıcı silme işi canlıya benzer bir ortamda sonuna kadar çalışıyor mu?
Bunlar arasında en sık atlanan, ikincisi. Çoğu ekip yumuşak silmeyi yazar, geri alma yolunu hiç çalıştırmaz ve ilk gerçek geri alma talebinde kodun eksik olduğunu fark eder.
Bu haftaya sığan ilk adım
Üç sorgu ve bir tablo. Birinci sorgu: deleted_at IS NOT NULL olan satır sayısı ve en eski silinme tarihi. Tarih bir yıl öncesini gösteriyorsa çöp kutunuz hiç boşalmıyor demektir ve kalıcı silme işi yok demektir. İkincisi: en büyük tablonuzda silinmiş satırların toplama oranı. Üçüncüsü: son bir ayda yapılan silme işlemlerinin denetim izindeki kaydı, önceki değerleriyle birlikte duruyor mu?
Tablo ise dört satırdan oluşuyor: yazının başındaki dört senaryo, her birinin karşısında hangi mekanizmanın devrede olduğu, penceresinin kaç gün olduğu ve kimin yetkili olduğu. Yarım günlük bir iş. Bittiğinde "yanlışlıkla sildik" telefonuna verdiğiniz cevap tahminden çıkar, prosedüre döner.
Bu konuda yardıma mı ihtiyacınız var?