Foreign Key, Tablolar Arasındaki Bir İşaretçidir
SQLite foreign key, bir tablodaki değeri başka bir tablodaki satırla eşleşmek zorunda olan bir sütundur. İlişkisel veritabanları "şu posts satırı, şu authors satırına ait" demeyi işte böyle ifade eder; yazarın adını ve e-postasını her gönderiye kopyalamak zorunda kalmadan.
Aşağıda olabilecek en sade örnek var: bir parent tablo ile bir child tablo, FK üzerinden birbirine bağlı:
author_id INTEGER REFERENCES authors(id) ifadesi, foreign key tanımının tamamıdır. Şunu söyler: bu kolon, authors tablosundaki bir id değerini tutar. Veritabanı artık iki tablonun ilişkili olduğunu bilir ve — eğer kısıtlama denetimi açıksa — var olmayan bir yazara işaret eden insert'leri reddeder.
SQLite Foreign Key Varsayılan Olarak Kapalıdır
SQLite foreign key konusunda bilmeniz gereken en kritik bilgi bu ve herkesi şaşırtıyor: SQLite, REFERENCES ifadelerini ayrıştırır ama sen açıkça istemediğiniz sürece bunları zorlamaz. Sebebi tarihsel uyumluluk — eski veritabanları, bu özellik var olmadan önce kurulmuştu.
Kısıtlama denetimi kapalıyken neler olduğuna bakalım:
Sahipsiz satır hiçbir uyarı vermeden geçti. Aslında istediğiniz korumayı elde etmek için her bağlantının başında PRAGMA foreign_keys = ON; komutunu çalıştırmanız gerekiyor:
Artık INSERT hata veriyor: FOREIGN KEY constraint failed. Bu pragma bağlantı bazında çalışır, veritabanı bazında değil — yani ayar dosyaya kaydedilmiyor. Her uygulama, her CLI oturumu, her test fixture'ı bu ayarı kendisi yapmak zorunda. Üretim kodunun çoğu, bağlantı açılır açılmaz PRAGMA foreign_keys = ON; komutunu çalıştırır.
REFERENCES İfadesi Aslında Ne Bekliyor?
Referans verdiğiniz kolonun ya PRIMARY KEY olması ya da UNIQUE kısıtına sahip olması şart. SQLite ancak bu sayede aramanın tek bir kayda denk geldiğini garanti edebiliyor. Tipler de uyumlu olmalı — SQLite tip konusunda esnek davransa da, farklı tipleri karıştırmak ileride başınıza iş açar.
SQLite foreign key tanımını iki şekilde yazabilirsiniz. İlki, kolonun yanında satır içi olarak:
Ya da birden fazla sütunu kapsayan foreign key tanımlamak istediğinizde zorunlu olduğu gibi, ayrı bir tablo seviyesinde kısıtlama olarak da yazabilirsiniz:
Her iki kullanım da aynı kısıtı üretir. Tablonuza hangisi daha okunaklı geliyorsa onu tercih edin.
ON DELETE: Çocuk Kayıtların Akıbeti
Bir parent kaydı sildiğinizde, ona bağlı child kayıtlarına ne olacağına SQLite'ın karar vermesi gerekir. Bu davranışı ON DELETE ile siz belirlersiniz:
Ada'yı silmek, ona ait iki gönderiyi de sildi. Seçenekler şöyle:
CASCADE— alt kayıtları da siler. Bir yazara ait gönderiler veya bir siparişe ait kalemler gibi "sahipli" veriler için ideal.SET NULL— FK kolonunuNULLyapar. Alt kayıtlar üst kayıt olmadan da yaşamalıysa uygundur (mesela silinen bir kullanıcının yorumları anonim hale gelsin).SET DEFAULT— FK kolonunu tanımlanmış varsayılan değerine çeker.RESTRICT— alt kayıt varsa silmeyi engeller. İşlem sırasında anında hata verir.NO ACTION— varsayılan davranış. Çoğu durumdaRESTRICTile aynı işi görür (kontrolü commit anına erteler ama sonuç değişmez: ortada sahipsiz alt kayıt bırakamazsınız).
ON UPDATE ise üst tablonun anahtarı değiştiğinde aynı mantıkla çalışır — gerçi primary key güncellemek pek karşılaşılan bir şey değildir.
Foreign Key Constraint Failed Hatası Ne Anlama Geliyor?
Bu hatayla genelde iki durumda karşılaşırsınız. İlki, bir alt kayda eşleşen üst kaydı olmayan bir değer eklemeye veya güncellemeye çalıştığınızda:
sqlite> INSERT INTO posts (title, author_id) VALUES ('Stray', 999);
Runtime error: FOREIGN KEY constraint failed
Ya 999 numaralı yazar tabloda yok ya da sütun tiplerini karıştırmışsınız. Önce parent kaydı ekle, ya da değeri düzelt.
İkincisi, FK RESTRICT veya NO ACTION ile tanımlıyken hâlâ child kaydı olan bir parent'ı silmeye (veya güncellemeye) çalışmak:
sqlite> DELETE FROM authors WHERE id = 1;
Runtime error: FOREIGN KEY constraint failed
Ya önce alt kayıtları silersiniz, ya da gerçekten kademeli (cascade) bir davranış istiyorsanız foreign key'i ON DELETE CASCADE veya SET NULL olarak değiştirirsiniz.
Bunun bir de daha az karşılaşılan akrabası var: FOREIGN KEY mismatch. Bu hata, referans verilen sütun primary key ya da unique değilse veya sütun sayıları birbirini tutmuyorsa karşımıza çıkar. Yani bu bir veri hatası değil, şema hatasıdır.
Var Olan Tablolara Foreign Key Ekleme
SQLite'ta ALTER TABLE oldukça kısıtlıdır — yeni bir sütunu foreign key ile birlikte ekleyebilirsiniz, ama mevcut bir sütuna sonradan foreign key tanımlayamazsınız. Bu durumda standart çözüm, "yeniden adlandır ve yeniden oluştur" yöntemidir:
Bu desen şöyle işliyor: önce kısıtlama denetimini kapatıyorsunuz, istediğiniz constraint'lerle yeni tabloyu oluşturuyorsunuz, veriyi kopyalıyorsunuz, eski tabloyu siliyor ve yenisini onun adıyla yeniden adlandırıyorsunuz. BEGIN/COMMIT sayesinde tüm bu işlem atomik kalıyor. En sonda denetimi tekrar açtığında SQLite, mevcut tüm satırları yeni kısıtlamalara göre doğrulayacaktır — ama dikkat: veri geçersiz olsa bile transaction çoktan commit edilmiş olur. Bu yüzden tedirginsen önceden kontrol et.
Migration sonrası PRAGMA foreign_key_check; çalıştırarak yetim (orphan) satır kalmadığından emin ol.
Gerçekçi Bir Şema Örneği
Şimdi tüm parçaları birleştirelim — ebeveyn-çocuk ilişkileri ve çoka-çok etiketleme için bir join tablosu içeren minik bir blog şeması:
Dikkat etmeniz gereken üç şey var. author_id alanı NOT NULL olarak tanımlı, yani her gönderinin mutlaka bir yazarı olmak zorunda. posts → authors foreign key'i CASCADE ile çalıştığı için bir yazarı sildiğinizde o yazara ait tüm gönderiler de silinir. post_tags ara tablosu ise her iki taraftan da CASCADE yapıyor; yani bir gönderiyi ya da bir etiketi sildiğinizde, ilişki satırları otomatik olarak temizlenir.
İleride Başınızı Ağrıtmayacak Alışkanlıklar
- Her bağlantıda
PRAGMA foreign_keys = ON;komutunu çalıştırın. Bunu aklınızda tutmaya çalışmak yerine, uygulamanızın veritabanı açma rutininin bir parçası haline getirin. - FK sütununa bir index ekleyin. SQLite parent tarafındaki anahtarı otomatik olarak indeksler ama child tarafını indekslemez. Üstelik
ON DELETE CASCADE, parent her silindiğinde child üzerinde arama yapar. ON DELETEdavranışını bilinçli seçin. Varsayılan değer (NO ACTION) güvenlidir ama bu, her temizlik denemenizde "constraint failed" hatasıyla karşılaşacağınız anlamına gelir. Ne olmasını istiyorsanız onu açıkça belirtin.- Migration veya toplu veri aktarımlarından sonra
PRAGMA foreign_key_check;çalıştırarak yetim (orphan) kayıtları, hataya dönüşmeden önce yakalayın.
Sıradaki Konu: INNER JOIN
Foreign key'ler ilişkinin nasıl olduğunu tanımlar; join'ler ise bu ilişki üzerinden gerçekten sorgu yapmanın yoludur. Bir sonraki sayfada INNER JOIN konusuna geçeceğiz: ilişkili tabloların satırlarını birleştirip her tablodan istediğiniz sütunları nasıl alacağınızı göreceğiz.
Sıkça Sorulan Sorular
SQLite'ta foreign key nasıl tanımlanır?
CREATE TABLE içindeki sütun tanımına REFERENCES diger_tablo(sutun) ekliyorsunuz. Örneğin author_id INTEGER REFERENCES authors(id) yazdığınızda author_id, authors tablosundaki bir satıra işaret eder. Referans verilen sütunun PRIMARY KEY ya da UNIQUE olması şart.
SQLite foreign key neden çalışmıyor?
SQLite foreign key tanımlarını okur ama siz açıkça etkinleştirmedikçe zorlamaz. Her bağlantının başında PRAGMA foreign_keys = ON; çalıştırmanız gerekiyor. Bu ayar veritabanına değil, bağlantıya özeldir; yani CLI olsun, kütüphane olsun her yeni bağlantıda tekrar set edilmeli.
SQLite'ta ON DELETE CASCADE ne işe yarar?
ON DELETE CASCADE, parent satır silindiğinde ona bağlı child satırların da otomatik silinmesini sağlar. Diğer seçenekler: RESTRICT (silmeyi engeller), SET NULL (FK sütununu null yapar), SET DEFAULT ve NO ACTION (varsayılan; pratikte RESTRICT ile aynı). Hangisini seçeceğiniz, child satırların parent olmadan anlamlı olup olmadığına bağlı.
'foreign key constraint failed' hatası nasıl çözülür?
Bu hata iki durumda çıkar: ya referans verilen tabloda olmayan bir değerle insert/update yapmaya çalıştınız, ya da hâlâ child satırları olan bir parent'ı silmeye çalıştınız. Önce referans verilen satırın gerçekten var olduğundan emin olun; çocukların otomatik silinmesini istiyorsanız ON DELETE CASCADE tanımlayın.