Okul platformumuzda bir ebeveyn silme talebi gönderdiğinde, bir yönetici bunu onaylar ve sunucumuzda anonymiseContact($contactId) fonksiyonu çalıştırılır. Bu fonksiyonun doğru bir şekilde çalışması beş deneme gerektirdi.
Bu yazı, GDPR’nın 17. Maddesi (silme hakkı) uygulamasının neden bu kadar zor olduğunu, nihayetinde karara vardığımız karar ağacını ve yol boyunca karşılaştığımız spesifik hata modlarını anlatmaktadır. Kod Laravel ve Postgres ile yazılmıştır ancak kararlar evrenseldir.
Naif Uygulama
Naif Uygulama
“Bir kullanıcının kişisel verilerini sil” için ilk akla gelen taslak, tek bir SQL ifadesidir:
RosterContact::where('id', $contactId)->delete();
Bu yaklaşımın başarısız olmasının üç ayrı sebebi vardır. Her biri farklı bir tasarım kararına işaret eder.
Problem 1: NULL Olmayan Yabancı Anahtar Tuzağı
Problem 1: NULL Olmayan Yabancı Anahtar Tuzağı
Öğrenci tablosunun aşağıdaki sütunu var:
$table->unsignedBigInteger('roster_contact_id'); // NULL OLMAYAN
Student::where('roster_contact_id', $contactId)->update(['roster_contact_id' => null]) çağrısı, aşağıdaki hatayı üretir:
SQLSTATE[23502]: Not null violation: 7 ERROR: null value in column
"roster_contact_id" of relation "students" violates not-null constraint
İletişim bilgilerini referans alan her tablo aynı sorunu taşır: issues, leave_requests, consent_responses, meeting_bookings, students. Naif bir cascade silme işlemi bunların hepsini yok edecektir ki bu yanlıştır, çünkü okul bu kayıtların mülkiyetine sahiptir ve bunları saklamakla yasal olarak yükümlüdür (devamsızlık geçmişi, şikayet kayıtları, onay izleme).
Çözüm, roster_contact_id‘yi her akıştaki tabloda nullable yapmak:
Schema::table('students', function (Blueprint $table) {
$table->unsignedBigInteger()->nullable()->change();
});
// ... aynı şekilde leave_requests, consent_responses, meeting_bookings için de
Sadece böylece bağlantıyı null yapabiliriz ve satırı koruyabiliriz.
Problem 2: Dört Yönlü Karar
Problem 2: Dört Yönlü Karar
FK’ler nullable olduğunda, kullanıcının dokunduğu her tablo için kasıtlı bir karar verilmesi gerekir. Dört olası sonuç vardır, iki değil:
| Sonuç | Ne zaman kullanılmalı | Örnek |
|---|---|---|
| Hard delete | Veri silindikten sonra hiçbir değeri yok ve sadece kullanıcıya ait | Erişim kodları, push bildirim jetonları |
| Anonymise (satırı tut, PII’yi temizle) | Verinin operasyonel veya denetim değeri var ancak kullanıcıyı tanımlamamalıdır. | Kişi kaydı, kullanıcı tarafından yazılan mesajlar |
| Detach (FK’yi null yap, satırı tut) | Veri başka bir tarafa aittir (okul, platform) ama kullanıcıyla bağlantılıdır | Issue’lar, leave requests, consent responses |
| Retain (dokunulmaz) | Veri doğrudan tanımlayıcı değildi | CSAT puanları, aggregate metrikler |
Platformumuz için karar matrisimiz aşağıdaki gibidir:
Access codes → hard delete
Push tokens, device ID → null out (PII)
Contact record → anonymise (name → "Deleted Contact", PII nulled)
Issue messages → anonymise (null author_id, replace meta.actor_name)
Activity log entries → anonymise (scrub contact_name in JSON payload)
Issues, leaves,
consents, bookings → detach (null roster_contact_id, keep row)
Students → detach (school owns the student record)
CSAT responses → retain (never carried direct PII)
Bu matris, kullanıcılarınız için yayımlamaya değerdir. 15. Madde (erişim hakkı) ve 30. Madde (işlem kayıtları) çerçevesinde, bir veri sahibi silme işlemi yapıldığında ne olduğunu sorma hakkına sahiptir. “Verilerinizi siliyoruz” gibi belirsiz bir cevap yeterli değildir.
Uygulama
Uygulama
İşte anonymiseContact() fonksiyonunun basitleştirilmiş hali. İşlem sırasına ve işlem sınırına dikkat edin:
private function anonymiseContact(int $contactId): void
{
$tenantId = tenant();
$contact = RosterContact::where(, $contactId)->first();
if (! $contact) {
return;
}
$actor = auth()->user(); // onaylayan admin
$school = School::where(, $tenantId)->first();
// 1. Satırları silmeden önce ek dosya referanslarını toplayın
$attachmentFiles = IssueAttachment::whereHas(,
fn ($q) => $q->where(, $contactId)
)->get([,])->all();
// 2. Açık sorunları otomatik olarak kapatın + atamaları kaldırın (bakım – aşağıya bakın)
$this->autoCloseOpenIssues($contactId, $actor);
// 3. Gelecek toplantı rezervasyonlarını iptal edin
$this->cancelFutureBookings($contactId, $actor);
// 4. Temel anonimleştirme, bir işlem içinde
DB::transaction(function () use ($tenantId, $contactId, $contact) {
// Hard delete erişim kodları (silme sonrası hiçbir değeri yok)
AccessCode::where(, $contactId)->delete();
// Bu ileti tarafından yazılan mesajları anonimleştir
IssueMessage::where( RosterContact::class)
->where( $contactId)
->each(function(IssueMessage $m) {
$meta = $m->meta ??[];
if (isset($meta[])) {
$meta[] =;
}
$m->update([
null,
null,
$meta,
]);
});
// Faaliyet kaydı paylaşımlarında iletişim adını temizle
IssueActivity::whereIn(, Issue::where(
, $contactId
)->pluck())->each(function(IssueActivity $a) {
$data = $a->data ??[];
if (isset($data[])) {
$data[] =;
}
$a->update([=> $data]);
});
// Ayrışma — aşağıdaki kayıtlardaki FK'yi null yapın
Issue::where(, $contactId)
->update([=> null]);
LeaveRequest::where(, $contactId)
->update([=> null]);
ConsentResponse::where(, $contactId)
->update([=> null]);
MeetingBooking::where(, $contactId)
->update([=> null]);
Student::where(, $contactId)
->update([=> null]);
// Nihayetinde, iletişimi kendisini anonimleştir
$contact->update([
=> ,
=> null,
=> null,
=> null,
=> null,
=> null,
null,
=> now(),
=> ,
]);
});
// 5. DB işlemi gerçekleştikten sonra ek dosyaları diskten silin
foreach ($attachmentFiles as $f)
{
try {
Storage::disk($f[])->delete($f[]);
} catch (\Throwable $e) {
// Yetim dosya, hayalet DB kaydından daha güvenli. Loglayın ve devam edin.
Log::warning(, [...]);
}
}
}
Bu kodda gözle görülmeyen dört karar bulunmaktadır.
Dosya temizleme işlem dışında gerçekleşir
Dosya temizleme işlem dışında gerçekleşir
Diskten dosya silmek işlemsel değildir. Eğer DB işlemi başarılı olur ve Storage::delete() çağrısı başarısız olursa, yetim bir dosya oluşur — bu can sıkıcı ama kurtarılabilir. Eğer DB hala işlem aşamasındaysa ve dosya silme işlemi başarılı olur ancak DB geri alınırsa, mevcut olmayan bir dosyayı işaret eden hayalet bir DB satırı oluşur — kullanıcıya görünür hatalar yaratır ve temizlenmesi daha zor olur.
İlk hata modunu tercih ediyoruz. Yetim dosyalar dışarıdan temizlenebilir; hayalet satırlar UI’yi kırar.
Ek dosya yolları işlemden önce toplanır
Ek dosya yolları işlemden önce toplanır
İşlem içinde issue_attachments’de DELETE işlemini gerçekleştiririz. Bu, sorgulamamız gereken kayıtları siler. Yolları işlemden önce toplamak, veritabanı kısmı başarılı olduğunda hâlâ listeye sahip olmamızı sağlar.
Otomatik bakım anonimleştirmeden ayrıdır
Otomatik bakım anonimleştirmeden ayrıdır
Bir iletişim silindiğinde, açık sorunların kapanması, gelecek toplantı rezervasyonlarının iptal edilmesi ve personel atamalarının kaldırılması gerekir. Bunu anonimleştirme işleminden önce yapıyoruz ve bunun yanına onaylayan adminin adını yazıyoruz, böylece her otomatik kapatma, net bir aktör ve close_reason: contact_erased bayrağı ile kendi aktivite kayıt girişini üretir.
Bunu anonimleştirmenin içinde yaparsak, bu kapanmaların aktörü null ya da "Deleted Contact" olarak düşer ki bu da “Bir soruna bir şey oldu ama bunu yapan kimse yok.” anlamına gelir. Bu, denetim kanıtı için istemediğiniz bir durumdur.
Tekrar deneme semantiği önemlidir
Tekrar deneme semantiği önemlidir
İlk anonymiseContact() dağıtımımız, daha önce açıkladığımız NULL OLAMAYAN FK nedeniyle çökmüştü. DB işlemi temiz bir şekilde geri alındı, ancak DataPrivacyRequest satırı failed olarak işaretlendi ve hata mesajı saklandı.
Admin UI’ye iki bayrak ekledik:
- Başarısız talepler “Retry & execute” butonu gösterirken “Approve & execute” butonu göstermez.
- Onay işlemi, hem
pendinghem defaileddurumlarını kabul eder.
Şemayı düzeltmek için (FK’leri nullable yaparak), admin “Retry” butonuna tıkladı ve talep başarıyla tamamlandı. Bu yeniden deneme seçeneği olmadan her düzeltme, manuel DB düzenlemeleri ya da ebeveynden yeni bir talep göndermesini istemek gerektiriyordu – bu, bir işlemin tam olarak hissettirilmesi gereken bir durumda oldukça kötü bir kullanıcı deneyimidir.
Hangi veriler sonsuza dek kalır
Hangi veriler sonsuza dek kalır
GDPR silme özelliği açısından çelişkili bir konudur: Silme işleminin kaydı kendisi saklanmalıdır.
Üç tür kaydı sonsuza kadar saklarız:
data_privacy_requestssatırı – talep anında iletişim bilgilerinin saklandığı bu, uyum kanıtıdır.- Bir
privacy_erasureaktivite kaydı – neyin silindiğinin özeti (sorunlar kapatıldı, rezervasyonlar iptal edildi) onaylayan admin adına atfedilir. - Silinmeden önceki yedek anlık görüntüler – bu olaylar tanımlayıcı PII içeren ancak kendiliğinden düzgün döngülerle 90 günde bir yaşlanan kayıtlardır. Bu, DPA’nızda açıklanmalıdır.
Makale 33 kapsamında denetim otoriteleri, taleplerin ve bunların nasıl ele alındığının kaydını görmeyi bekler. Bir kullanıcı “Beni ne zaman sildiniz ve kim onayladı?” diye soruyorsa, bu geçerli bir sorudur ve “bu tarihte, bu admin tarafından, işte kanıt” şeklinde geçerli bir cevabı vardır.
Karar Ağacının Yayınlanması
Karar Ağacının Yayınlanması
Yukarıdaki matrisin her sözcüğü artık kullanıcıya açık Gizlilik Politikasında, “Silme talebiniz onaylandığında ne olur” başlığı altında bulunmaktadır. Yayınlamanın üç amacı vardır:
- Kullanıcılar, haklarını kullanırken gerçek bir beklentiye sahip olurlar.
- Politikayı okuyan denetçiler mühendisliğin gerçek bir dayanağı olduğunu görürler.
- Bu, içsel netlik sağladığı için — yayımlanacakları şeyleri karar vermediğiniz sürece açıklayamazsınız.
17. Maddeyi uygulamaya başlamadan önce, kodla başlamayın. Önce karar matrisini yazın, bunu kullanıcı dostu bir dilde yazın ve kod ardından gelsin. Beş denememiz büyük ölçüde doğru sırayı takip edememekten kaynaklandı.
Özet
Özet
GDPR silme işlemi, veri türüne göre dört sonuç üreten mühendislik kararıdır, ikili değil. Uygulama, nullable yabancı anahtarlar, kasıtlı bir işlem sırası, DB ve depolamanın düzgün bir şekilde ayrılmasını gerektirir, operasyonel yan etkiler için ayrı bir bakım, hata kurtarma için yeniden deneme imkânı ve silme işleminin kendisine dair sonsuz bir denetim izi gerektirir. Naif bir DELETE FROM users yasal, operasyonel ve denetim sonuçları bakımından yanlış sonuca ulaşır.
Önce matrisi yazın. Sonra kodu yazın.
Kaynak: Orijinal Makale
- Naif Uygulama
- Problem 1: NULL Olmayan Yabancı Anahtar Tuzağı
- Problem 2: Dört Yönlü Karar
- Uygulama
- Dosya temizleme işlem dışında gerçekleşir
- Ek dosya yolları işlemden önce toplanır
- Otomatik bakım anonimleştirmeden ayrıdır
- Tekrar deneme semantiği önemlidir
- Hangi veriler sonsuza dek kalır
- Karar Ağacının Yayınlanması
- Özet


