TB
← Tüm yazılar

Soft delete ve audit

Veriyi fiziksel olarak silmek yerine soft delete uygulamak ve tüm değişiklikleri audit log'a kaydetmek, regülasyon uyumluluğu ve veri kurtarma için kritiktir.

Üretim veritabanlarında bir kaydı DELETE ile kalıcı olarak silmek, çoğu zaman geri dönüşü zor veya imkânsız bir karar verir. Yanlışlıkla silinen müşteri siparişi, muhasebe kaydı veya KVKK kapsamındaki kişisel veri; hem iş sürekliliğini hem de yasal uyumluluğu tehdit eder. Soft delete (mantıksal silme), satırı fiziksel olarak kaldırmak yerine silindiğini işaretleyen bir bayrak veya zaman damgası ile saklar. Audit (denetim izi) ise kim, ne zaman, hangi değeri değiştirdi sorusuna cevap verir. İkisi birlikte düşünüldüğünde veri bütünlüğü, regülasyon ve operasyonel güvenilirlik aynı mimari çatı altında toplanır.

Soft delete temel modeli

En yaygın uygulama, her iş tablosuna IsDeleted (bit/boolean), DeletedAt (datetimeoffset) ve DeletedBy (nvarchar veya foreign key) kolonları eklemektir. Silme işlemi aslında bir UPDATE'tir:

UPDATE Orders
SET IsDeleted = 1,
    DeletedAt = SYSUTCDATETIME(),
    DeletedBy = @CurrentUserId
WHERE Id = @OrderId AND IsDeleted = 0;

Tüm okuma sorgularına WHERE IsDeleted = 0 filtresi eklenmesi gerekir. Bu filtreyi her sorguda manuel yazmak hataya açıktır; bir geliştiricinin unutması, silinmiş kayıtların tekrar görünmesine yol açar. Entity Framework Core'da global query filter bu sorunu merkezi çözer:

modelBuilder.Entity()
    .HasQueryFilter(o => !o.IsDeleted);

Global filter, LINQ sorgularına otomatik eklenir. IgnoreQueryFilters() yalnızca bilinçli senaryolarda (admin geri yükleme, denetim raporu) kullanılmalıdır.

Benzersiz kısıtlar ve soft delete

Soft delete ile birlikte gelen klasik tuzak: benzersiz indeksler. Örneğin UNIQUE(Email) tanımlı bir Users tablosunda aynı e-posta ile silinmiş bir kayıt varken yeni kullanıcı oluşturulamaz. Çözüm, filtered unique index kullanmaktır:

CREATE UNIQUE INDEX UX_Users_Email_Active
ON Users (Email)
WHERE IsDeleted = 0;

PostgreSQL, SQL Server ve MySQL 8+ bu tür kısmi indeksleri destekler. Tasarım aşamasında tüm unique constraint'ler soft delete ile birlikte gözden geçirilmelidir.

Audit log mimarileri

Audit ihtiyacı tek bir tabloya CreatedDate eklemekten çok daha geniştir. Üç ana yaklaşım vardır:

  1. Tablo içi audit kolonları: CreatedBy, CreatedDate, LastModifiedBy, LastModifiedDate — basit ve hızlı, ancak geçmiş değerleri tutmaz.
  2. Ayrı audit tablosu: Her değişiklik için eski/yeni değer JSON veya satır bazlı kopya — tam geçmiş, daha fazla depolama.
  3. Temporal tables / CDC: Veritabanı motoru seviyesinde otomatik versiyonlama — uygulama kodundan bağımsız, vendor özelliklerine bağlı.

SQL Server system-versioned temporal tables ile geçmiş otomatik History tablosuna yazılır. PostgreSQL'de benzer ihtiyaç için trigger tabanlı audit veya pg_audit extension kullanılır. Seçim kriterleri: sorgu karmaşıklığı, saklama süresi, regülasyon ve operasyon ekibinin yetkinliği.

EF Core SaveChanges interceptor ile audit

Uygulama katmanında değişiklikleri yakalamak için interceptor tercih edilir:

public override InterceptionResult SavingChanges(
    DbContextEventData eventData,
    InterceptionResult result)
{
    foreach (var entry in eventData.Context!.ChangeTracker.Entries())
    {
        if (entry.State == EntityState.Modified)
        {
            entry.Property("LastModifiedDate").CurrentValue = DateTime.UtcNow;
            entry.Property("LastModifiedBy").CurrentValue = _currentUser.Id;
        }
        if (entry.State == EntityState.Deleted)
        {
            entry.State = EntityState.Modified;
            entry.Property("IsDeleted").CurrentValue = true;
        }
    }
    return result;
}

Bu kalıp, geliştiricinin Remove() çağırmasına izin verirken altta soft delete uygular. Ayrı bir AuditLog entity'sine JSON diff yazmak için aynı interceptor genişletilebilir.

İlişkiler ve cascade soft delete

Order silindiğinde OrderLine kayıtları ne olacak? Fiziksel cascade delete artık devre dışıdır. Seçenekler:

  • Alt kayıtları da soft delete ile işaretlemek (domain event veya trigger ile)
  • Üst kayıt silinmişken alt kayıtları global filter ile gizlemek
  • Soft delete'i yalnızca aggregate root seviyesinde uygulamak

Aggregate root modelinde yalnızca Order.IsDeleted kontrol edilir; OrderLine'lar fiziksel olarak kalır ancak join üzerinden filtrelenir. Denormalize raporlama tablolarında soft delete tutarlılığı ayrı batch job ile sağlanabilir.

Regülasyon, KVKK ve veri saklama

Soft delete, veriyi "silindi" olarak işaretler; yasal anlamda silme sayılmayabilir. KVKK kapsamında unutulma hakkı talebi geldiğinde hard delete veya anonimleştirme gerekir. Saklama politikası şöyle tanımlanmalıdır:

  1. Soft delete sonrası X gün boyunca geri yükleme penceresi
  2. Süre dolunca purge job ile fiziksel silme veya PII maskeleme
  3. Audit log'ların ayrı saklama süresi (genellikle daha uzun)

Audit log'da kişisel veri tutuluyorsa maskeleme ve erişim kısıtlaması zorunludur. Denetçi erişimi read-only replica veya ayrı raporlama veritabanı üzerinden sağlanmalıdır.

Performans ve indeksleme

IsDeleted kolonu neredeyse tüm sorgularda filtre olarak kullanılır. Composite indekslerde IsDeleted ilk sütun olmamalı; seçicilik düşüktür. Bunun yerine (CustomerId, OrderDate) indeksine filtered index eklemek daha verimlidir. Silinmiş kayıtların birikmesi tablo boyutunu artırır; düzenli purge veya arşiv tablosuna taşıma planlanmalıdır.

Arşivleme stratejisi

Yüksek hacimli sistemlerde silinmiş kayıtlar ana tablodan archive schema veya soğuk depolama tablosuna taşınır. Uygulama kodu yalnızca aktif tabloyu görür; raporlama veya hukuki talep için arşiv sorgulanır. Bu yaklaşım OLTP performansını korurken audit gereksinimini karşılar.

Geri yükleme ve idempotency

Soft delete geri alma (undelete) işlemi audit'e de yazılmalıdır. Aynı kayıt üzerinde silme-yükleme döngüsü idempotent olmalıdır: ikinci silme DeletedAt'i günceller, eski DeletedBy bilgisi audit tablosunda kalır. Concurrent silme senaryosunda optimistic concurrency (RowVersion) çakışmayı yakalar.

Test ve doğrulama

Entegrasyon testlerinde global filter'ın çalıştığı doğrulanmalıdır: silinen entity varsayılan sorguda görünmemeli, IgnoreQueryFilters ile görünmelidir. Unique constraint testleri silinmiş kayıt varken yeni kayıt oluşturmayı kapsamalıdır. Audit interceptor testlerinde Modified ve Deleted state geçişleri ayrı ayrı assert edilir.

Yaygın hatalar

  • Raw SQL sorgularında IsDeleted filtresini unutmak
  • Global filter'ı navigation property join'lerinde bypass eden Include kullanımı
  • Audit log'a şifre veya token gibi hassas alanları diff olarak yazmak
  • Soft delete ile hard delete'i aynı endpoint'te karıştırmak
  • Silinmiş kayıtları cache'te tutmaya devam etmek

Özet

Soft delete ve audit, modern veritabanı tasarımının ayrılmaz parçalarıdır. Global query filter, filtered unique index, interceptor tabanlı audit ve saklama politikası birlikte kurgulandığında hem geliştirici hatası azalır hem de regülasyon talepleri karşılanır. Hard delete'i tamamen ortadan kaldırmak yerine bilinçli purge süreçleri tanımlamak; sistemin uzun vadede hem performanslı hem de yasal açıdan savunulabilir kalmasını sağlar.

Operasyonel perspektif

Soft delete ve audit konusunda üretim ortamında karşılaşılan senaryolar, geliştirme ortamından farklıdır. Trafik hacmi, eşzamanlı bağlantı sayısı, disk I/O ve replikasyon gecikmesi gibi faktörler tasarım kararlarını doğrudan etkiler. Metrik toplama ve düzenli kapasite gözden geçirmesi, sorunları kullanıcı şikâyetine dönüşmeden yakalamayı sağlar.

Kapasite planlama

Veritabanı katmanında CPU, bellek, disk throughput ve connection pool kullanımı birlikte izlenmelidir. Ani trafik artışlarında autoscaling uygulama katmanında mümkün olsa da veritabanı ölçeklendirmesi genellikle planlı yapılır. Read replica eklemek, connection pool boyutunu artırmak veya sorgu optimizasyonu yapmak gibi seçenekler yük testi sonuçlarına göre sıralanmalıdır.

Güvenlik ve erişim kontrolü

Veritabanı kullanıcıları en az ayrıcalık ilkesine göre tanımlanmalıdır. Uygulama hesabı yalnızca gerekli DML yetkilerine sahip olmalı; DDL ve yönetim işlemleri ayrı role ayrılmalıdır. Bağlantı string'leri secret manager'da tutulmalı, rotation politikası uygulanmalıdır. Audit log erişimi yalnızca denetim ve operasyon rollerine açık olmalıdır.

Ekip disiplini ve dokümantasyon

Mimari kararlar Architecture Decision Record (ADR) ile belgelenmelidir. Onboarding dokümanında bu konuya özel bölüm yer almalı; yeni geliştiriciler global filter, routing kuralı veya yedekleme penceresi gibi kritik detayları atlamamalıdır. Code review checklist'ine ilgili maddeler eklenmesi regresyon riskini azaltır.

Kalite kapıları

  1. Pull request'te ilgili entegrasyon testlerinin çalışması
  2. Staging ortamında gerçekçi veri hacmi ile smoke test
  3. Performans regression eşiği (p95 latency, sorgu sayısı)
  4. Migration rollback planının PR açıklamasında belirtilmesi

Bu disiplinler tek başına mucize yaratmaz; ancak veritabanı katmanındaki hataların maliyeti yüksek olduğundan, erken yakalama yatırımı uzun vadede ödenir. Post-incident review'larda kök neden analizi veritabanı tasarımına geri beslenmelidir.