Moder yazılım mimarisinde, veri bütünlüğü ve izlenebilirlik vazgeçilmez gereksinimlerdir. Birçok geliştirici denetim günlüklerini bir düşünce ürünü olarak görüp değişiklik olduğunda yalnızca ayrı bir `audit_log` tablosuna satır eklerken, daha sağlam bir yaklaşım Olay Kaynağı (Event Sourcing)'dır. Durum değişikliklerini değişmez olaylar olarak ele alarak, uygulamanızın evriminin tam ve yeniden oynatılabilir bir geçmişine sahip olursunuz.
Bu yazı, geleneksel ilişkisel veritabanı sistemlerinde (PostgreSQL veya MySQL gibi) denetim izleri için özel olarak tasarlanmış bir Olay Kaynağı desenini nasıl uygulayacağınızı, veri değişmezliği ve performansını sağlamak için SQL özelliklerinden yararlanarak incelemektedir.
Temel Kavram: Olaylar vs. Durum
Geleneksel olarak, ilişkisel veritabanları varlıkların mevcut durumunu saklar. Bir kullanıcının e-posta adresi değiştiğinde, önceki e-posta adresi üzerine yazılır. Olay Kaynağı ile durumları saklamaz; bu durumu yaratan olayları saklarız. Denetim amaçlı olarak bu, her oluşturma, güncelleme ve silme işleminin ayrı, yalnızca ekleme yapılabilir bir kayıt olarak kaydedildiği anlamına gelir.
Bu yaklaşım birkaç avantaj sağlar:
- Tam Hesap Verebilirlik: Ne değiştiğini, kimin değiştirdiğini ve ne zaman değiştirildiğini tam olarak bilirsiniz.
- Yeniden Oynatılabilirlik: Herhangi bir kaydı herhangi bir anda yeniden oluşturabilirsiniz.
- Uyumluluk: Veri kökeni için sıkı düzenleyici gereksinimleri karşılar (örneğin, GDPR, HIPAA).
Değişmez Olaylar İçin Şema Tasarımı
Bu desenin temeli, özel bir audit_events tablosudur. Standart tablolardan farklı olarak, bu tablonun değişmezliği sağlaması gerekir. Modern PostgreSQL'de bunu WITHOUT OVERWRITE kullanarak veya DELETE ve UPDATE izinlerini tamamen kısıtlayarak zarif bir şekilde gerçekleştirebiliriz.
İşte sağlam bir şema tanımı:
CREATE TABLE audit_events (
event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aggregate_id UUID NOT NULL, -- Değiştirilen varlığın kimliği (örn. user_id)
event_type VARCHAR(50) NOT NULL, -- Örn. 'USER_CREATED', 'EMAIL_UPDATED'
payload JSONB NOT NULL, -- Veri değişiklikleri
metadata JSONB, -- IP adresi, kullanıcı aracısı gibi ek bağlam
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_audit_no_delete CHECK (true) -- İzinler aracılığıyla uygulanır
);
-- Hızlı erişim için dizinler
CREATE INDEX idx_audit_aggregate ON audit_events(aggregate_id, created_at DESC);
CREATE INDEX idx_audit_type ON audit_events(event_type);
Veritabanı İzinleri ile Değişmezliğin Sağlanması
Kod düzeyindeki kontroller atlanabilir. Gerçek değişmezliği sağlamak için audit_events tablosundaki doğrudan DELETE ve UPDATE işlemlerini veritabanı düzeyinde kısıtlamalıyız.
-- Uygulama kullanıcılarından doğrudan yazma izinlerini kaldırın
REVOKE DELETE, UPDATE ON audit_events FROM app_user;
-- Olayları güvenli bir şekilde eklemek için bir işlev oluşturun
CREATE OR REPLACE FUNCTION insert_audit_event(
p_aggregate_id UUID,
p_event_type VARCHAR,
p_payload JSONB,
p_metadata JSONB
) RETURNS VOID AS $$
BEGIN
INSERT INTO audit_events (aggregate_id, event_type, payload, metadata)
VALUES (p_aggregate_id, p_event_type, p_payload, p_metadata);
END;
$$ LANGUAGE plpgsql;
Durumu Yeniden Oluşturma: Okuma Modeli
Olay Kaynağı'nın zorluklarından biri veriyi okumaktır. Sadece olayları sakladığımız için mevcut durumu hesaplamamız gerekir. Bu, uygulama kodunda yapılabilse de SQL, pencere işlevlerini kullanarak bunu verimli bir şekilde halledebilir.
Bir kullanıcının profilinin en son sürümünü almak için, her bir birleşim için en son olayı almak üzere Ortak Tablo İfadesi (CTE) kullanabilirsiniz:
WITH latest_events AS (
SELECT
aggregate_id,
event_type,
payload,
created_at,
ROW_NUMBER() OVER (PARTITION BY aggregate_id ORDER BY created_at DESC) as rn
FROM audit_events
WHERE aggregate_id = 'user-123-uuid'
)
SELECT payload, created_at
FROM latest_events
WHERE rn = 1;
Uygulama Katmanında Pratik Uygulama
Uygulama kodunuzda, verileri değiştiren iş mantığını bir işlem (transaction) içinde sarmalısınız. Bir kullanıcı profilini güncellediğinde, yalnızca users tablosunu güncellemek yerine, insert_audit_event saklı yordamını da çağırmanız gerekir.
Örneğin, bir Python veya Node.js hizmetinde:
- İşlemi Başlat
- İş Mantığını Yürüt (
userstablosunu güncelle) - Olayı Kalıcı Hale Getir (Saklı yordamı çağır)
- İşlemi Onayla
Bu, denetim izi ile gerçek veri değişikliğinin birlikte gerçekleşmesini sağlar. İş mantığı başarısız olursa, denetim kaydı geri alınır ve yetim olayların oluşması önlenir.
Sonuç
İlişkisel veritabanlarında denetim izleri için Olay Kaynağı uygulamak, veri bütünlüğü ve uyumluluğu korumak için güçlü bir mekanizma sağlar. Olayların saklanmasını mevcut durumdan ayırarak, yalnızca daha şeffaf değil, aynı zamanda doğası gereği daha sağlam bir sistem oluşturursunuz. Bu, okuma yolunda karmaşıklık eklese de, değişmez ve yeniden oynatılabilir bir geçmişin faydaları, kritik sistemler için ek yükün çok ötesindedir. Basit bir audit_events tablosuyla başlayın, değişmezliği veritabanı düzeyinde uygulayın ve ihtiyaçlarınız büyüdükçe yeniden oynatma yeteneklerinizi kademeli olarak genişletin.