Ders 11 / 12
Veritabanı Erişimi
Bir kukla sürücüyle beş sütunluk bir sorgu koşturulur: sürücünün dar tip kümesinden gelen değerlerin üçü, hedef değişkenin kendi Scan yöntemiyle özel bir Go tipine geri kurulur, ikisi zaten sürücünün doğal tipiyle örtüşür. Sınırlayıcı ölçüm, taramanın hedefin tipine baktığını ve uyumsuz bir hedefin çalışma zamanında hata verdiğini, bağlantı havuzu ölçümü ise kapatılmayan bir satır kümesinin havuzu tıkadığını koşturarak gösteriyor.
İçindekiler
Önceki ders bir günlük satırından çıkarılan bir alanın tipinin metin olarak kaldığını,
geri kurmanın okuyan tarafın kendi tarifine bağlı olduğunu gösterdi. Bu konunun son dersi
aynı soruyu bir veritabanı sürücüsünün elinden geçen bir satır üzerinde soruyor — ama bu kez
tarif metinde değil, kodun kendisinde: çağıranın Scan çağrısına verdiği hedef değişkenin
tipinde duruyor.
Veritabanı satırı, konu boyunca ölçülen sınırların en katmanlısı. Dosya ve arabellek dersinde
sınır tek bir arayüzdü (Read/Write); burada üç katman var — sürücünün kendi dar tip
kümesi, database/sqlin bu kümeyi taşıyan genel mekanizması, ve çağıranın Scane verdiği
hedef tip. Her katman kendi payına düşen dönüşümü yapıyor, ve bu ders üç katmanın da nerede
durduğunu ayrı ayrı gösteriyor.
Gerçek kaynak kullanılmaz. Bu derste hiçbir gerçek veritabanı çalıştırılmıyor; standart
kitaplığın database/sql/driver paketindeki sürücü arayüzü, bu dersin kendi yazdığı bir
kukla ile gerçekleştiriliyor. Kukla sürücü sabit, elde önceden hazırlanmış iki satırlık
bir veri döndürüyor; hiçbir ağ bağlantısı, dosya ya da dış süreç yok.
Kukla Sürücü
Go’nun standart kitaplığı database/sql paketiyle genel bir veritabanı arayüzü sunuyor;
gerçek sürücüler (belirli bir veritabanı ürününe özgü bağlantı mantığı) her zaman üçüncü
taraf paketler olarak dağıtılıyor. Bu ders o ayrımı ciddiye alıyor: database/sql/driver
paketindeki arayüzleri (Driver, Conn, Rows) kendi yazdığı basit bir tiple
gerçekleştiriyor, hiçbir üçüncü taraf sürücü içe aktarmıyor.
// kukla.go — database/sql/driver arayuzu kendi kuklasiyla gerceklestiriliyor, gercek veritabani yok
package main
import (
"database/sql"
"database/sql/driver"
"fmt"
"io"
"time"
)
// KullaniciID, satirin ilk sutununu tasiyan tip: surucu bunu int64 olarak tasir.
type KullaniciID int64
func (k *KullaniciID) Scan(kaynak any) error {
v, ok := kaynak.(int64)
if !ok {
return fmt.Errorf("KullaniciID.Scan: beklenmeyen kaynak tipi %T", kaynak)
}
*k = KullaniciID(v)
return nil
}
// Tutar, kurus cinsinden bir tutari tasir; surucu bunu da int64 olarak tasir.
type Tutar int64
func (t *Tutar) Scan(kaynak any) error {
v, ok := kaynak.(int64)
if !ok {
return fmt.Errorf("Tutar.Scan: beklenmeyen kaynak tipi %T", kaynak)
}
*t = Tutar(v)
return nil
}
// ZamanDamgasi, surucunun dizgi olarak tasidigi bir zamani time.Time'a cevirir.
type ZamanDamgasi struct{ time.Time }
func (z *ZamanDamgasi) Scan(kaynak any) error {
s, ok := kaynak.(string)
if !ok {
return fmt.Errorf("ZamanDamgasi.Scan: beklenmeyen kaynak tipi %T", kaynak)
}
ayristirilan, err := time.Parse(time.RFC3339, s)
if err != nil {
return err
}
z.Time = ayristirilan
return nil
}
var satirVerisi = [][]driver.Value{
{int64(1), "kalem", int64(1250), true, "2026-08-23T10:00:00Z"},
{int64(2), "defter", int64(3990), false, "2026-08-20T09:30:00Z"},
}
type kuklaSurucu struct{}
func (kuklaSurucu) Open(ad string) (driver.Conn, error) { return &kuklaBaglanti{}, nil }
type kuklaBaglanti struct{}
func (kuklaBaglanti) Prepare(sorgu string) (driver.Stmt, error) {
return nil, fmt.Errorf("kukla surucu: hazirlanmis ifade desteklenmiyor")
}
func (kuklaBaglanti) Close() error { return nil }
func (kuklaBaglanti) Begin() (driver.Tx, error) {
return nil, fmt.Errorf("kukla surucu: islem desteklenmiyor")
}
func (kuklaBaglanti) Query(sorgu string, args []driver.Value) (driver.Rows, error) {
return &kuklaSatirlar{veri: satirVerisi, konum: 0}, nil
}
type kuklaSatirlar struct {
veri [][]driver.Value
konum int
}
func (kuklaSatirlar) Columns() []string {
return []string{"id", "ad", "fiyat_kurus", "aktif", "eklenme"}
}
func (kuklaSatirlar) Close() error { return nil }
func (r *kuklaSatirlar) Next(dest []driver.Value) error {
if r.konum >= len(r.veri) {
return io.EOF
}
copy(dest, r.veri[r.konum])
r.konum++
return nil
}
func init() {
sql.Register("kukla", kuklaSurucu{})
}
Bu dosya func main içermediği için diske yazılıyor ama çalıştırılmıyor; aşağıdaki ölçümler
onu kullanıyor. kuklaSatirlar.Next yöntemi, sürücünün gerçekte yaptığı şeyi taklit ediyor:
her satırı driver.Value diliminde döndürüyor, ve driver.Value kasıtlı olarak dar bir
tip: yalnızca int64, float64, bool, []byte, string, time.Time ve nil bu tipe
girebiliyor. Bu ders için önemli olan sonuç: bir sütunun kaynaktaki “gerçek” anlamı ne
olursa olsun (bir kullanıcı kimliği, bir para tutarı, bir zaman damgası), sürücü sınırını
geçerken bu yedi tipten birine indirgeniyor.
Bu darlık kasıtlı bir tasarım kararı. database/sql paketi genel bir sözleşme sunuyor —
her sürücü, hangi veritabanı ürününü konuşursa konuşsun, satırdan gelen değeri bu yedi tipten
birine dönüştürmek zorunda. Çağıran kod, hangi sürücüyle çalıştığını bilmeden aynı Query ve
Scan çağrılarını kullanabiliyor, çünkü sürücüler arasındaki fark tam olarak bu dar arayüzün
arkasında kalıyor. Bu ders, gerçek bir sürücünün yaptığı şeyi en aza indirgenmiş bir biçimde
tekrarlıyor: iki satırlık sabit bir veri, bağlantı açma girişiminde hiçbir gerçek işlem
yapmayan bir Open, ve yalnızca elindeki diziyi sırayla döndüren bir Next.
Beş Sütun, Sürücünün Dar Tip Kümesi
SS17. — Beş sütunluk bir sorgu koşturuluyor; her sütun kendi Go tipine (KullaniciID,
string, Tutar, bool, ZamanDamgasi) taranıyor ve sonucun tipi %T ile okunuyor.
// olcum.go — bes sutun kendi tipine taraniyor, indirgenen ve indirgenmeyen sutunlar ayriliyor
package main
import (
"database/sql"
"fmt"
)
func main() {
db, _ := sql.Open("kukla", "bellek-ici")
defer db.Close()
satirlar, _ := db.Query("select id, ad, fiyat_kurus, aktif, eklenme from urunler")
defer satirlar.Close()
for satirlar.Next() {
var id KullaniciID
var ad string
var fiyat Tutar
var aktif bool
var eklenme ZamanDamgasi
if err := satirlar.Scan(&id, &ad, &fiyat, &aktif, &eklenme); err != nil {
fmt.Println("tarama hatasi:", err)
continue
}
fmt.Printf("id=%d(%T) ad=%s(%T) fiyat=%d(%T) aktif=%t(%T) eklenme=%s(%T)\n",
id, id, ad, ad, fiyat, fiyat, aktif, aktif, eklenme.Format("2006-01-02"), eklenme)
}
}
id=1(main.KullaniciID) ad=kalem(string) fiyat=1250(main.Tutar) aktif=true(bool) eklenme=2026-08-23(main.ZamanDamgasi) id=2(main.KullaniciID) ad=defter(string) fiyat=3990(main.Tutar) aktif=false(bool) eklenme=2026-08-20(main.ZamanDamgasi)
Beş sütunun üçü — id, fiyat_kurus, eklenme — sürücüden geldiğinde sırasıyla int64,
int64, string; hiçbiri hedef tipin adını (KullaniciID, Tutar, ZamanDamgasi) taşımıyor.
Bu üç sütunun her biri, hedef değişkenin kendi Scan yöntemi tarafından geri kuruluyor —
KullaniciID.Scan ham int64’ü alıp kendi tipine dönüştürüyor, ZamanDamgasi.Scan ham dizgiyi
alıp time.Parse ile bir time.Timee çeviriyor. Öteki iki sütun — ad, aktif — sürücüden
zaten hedef tipiyle aynı biçimde geliyor (string, bool); onlar için özel bir Scan
yöntemine gerek yok, doğrudan atama yeterli. Sonuç: beş sütunun üçü indirgeniyor ve taramayla
geri kuruluyor, ikisi zaten sürücünün doğal tipiyle örtüşüyor.
Bu üç Scan yöntemi, konunun ölçü ekseninin en net “yeniden kuruluyor” örneği: sürücü hiçbir
yerde “bu bir kullanıcı kimliği” ya da “bu bir zaman” demiyor, yalnızca ham bir sayı ya da
dizgi veriyor. Geri kurmayı sağlayan tarif, sürücüde değil, çağıranın Scane verdiği hedef
değişkenin kendi tipinde ve kendi yönteminde duruyor.
ad ve aktif sütunlarının hiçbir özel Scan yöntemine ihtiyaç duymaması bir rastlantı
değil: standart kitaplığın kendi dönüştürme mantığı, hedef değişkenin tipi sürücüden gelen
driver.Value tipiyle zaten örtüştüğünde, hiçbir ek adım gerektirmeden doğrudan atama
yapıyor. Özel bir Scan yöntemi yalnızca hedef tip sürücünün tanıdığı yedi tipin dışında
olduğunda devreye giriyor — KullaniciID, Tutar ve ZamanDamgasi bu yüzden kendi
yöntemlerini taşıyor, string ve bool taşımıyor. Bu, sürücünün dar tip kümesinin sınırının
tam olarak nerede çizildiğini gösteriyor: sınır sürücüde sabit, ama sınırın ötesine geçen her
hedef tip kendi geçiş kuralını kendisi yazmak zorunda.
Sınırlayıcı Ölçüm: Tarama Hedefin Tipine Bakar
Yukarıdaki Scan çağrısı, sütun sırasının hedef değişken sırasıyla eşleştiği varsayımına
dayanıyor. Bu eşleşme bozulduğunda — bir sütunun hedefi, o sütunun taşıdığı driver.Value
tipine uymadığında — sonuç bir derleme hatası değil, bir çalışma zamanı hatası.
SS18. — İkinci sütun (ad, bir string) yanlışlıkla KullaniciID tipinde bir hedefe
taranıyor; sorgunun kendisi hiç değişmiyor, yalnızca Scan çağrısındaki değişken sırası
karışıyor.
// uyumsuz-hedef.go — sorgu ayni, yalniz Scan hedefi karisiyor; hata calisma zamaninda geliyor
package main
import (
"database/sql"
"fmt"
)
func main() {
db, _ := sql.Open("kukla", "bellek-ici")
defer db.Close()
satirlar, _ := db.Query("select id, ad, fiyat_kurus, aktif, eklenme from urunler")
defer satirlar.Close()
satirlar.Next()
var id KullaniciID
var yanlisHedef KullaniciID // ikinci sutun (ad, bir dizgi) buraya taranmaya calisiliyor
var fiyat Tutar
var aktif bool
var eklenme ZamanDamgasi
hata := satirlar.Scan(&id, &yanlisHedef, &fiyat, &aktif, &eklenme)
fmt.Println("uyumsuz hedefle tarama hata verdi mi:", hata != nil)
}
uyumsuz hedefle tarama hata verdi mi: true
Sorgu metni ("select id, ad, ...") burada hiçbir şey söylemiyor — sütun adları doğru,
sorgu kukla sürücüden başarıyla dönüyor. Hata, yalnızca Scan çağrısı çalıştığında ortaya
çıkıyor: yanlisHedef bir KullaniciID, ama sürücüden gelen ikinci sütunun değeri bir
string ("kalem"), ve KullaniciID.Scan bunu bir int64e dönüştüremediği için hata
döndürüyor. Bu, konunun ölçü ekseninin bu derse özgü sınırı: tarama hedefin tipine
bakıyor, kaynağın tipine değil — sütunun kendisi hangi tipte olduğunu önceden ilan
etmiyor, yalnızca hedefe uyup uymadığı çalışma zamanında sınanıyor. Bir sorgu yanlış yazılmış
gibi görünmüyor; yalnızca Scan argümanlarının sırası kaymış, ve bu kaymayı derleyici hiç
yakalayamıyor çünkü Scanin imzası ...any — kaç ve hangi tipte argüman verildiği derleme
zamanında denetlenmiyor.
Bu, önceki dersteki günlük alanı okumasıyla aynı biçimde ortaya çıkan bir kusur. Orada da
yanlış bir tarifle (basarili alanını int diye okumak) hata çalışma zamanında geliyordu,
sorgu ya da günlük satırı yazılırken değil. Aradaki fark, sorgunun select listesi ile
Scanin argüman listesi arasındaki bağın konuma dayanması: iki liste arasında sütun
adına bakan hiçbir denetim yok, yalnızca sıra var — ve bu sıranın doğru olup olmadığı, ancak
program fiilen çalıştırıldığında sınanıyor.
Bağlantı Havuzu ve Kapatma Sorumluluğu
sql.DB bir tek bağlantı değil, bir bağlantı havuzu; havuzun büyüklüğü
SetMaxOpenConns ile sınırlanabiliyor. Bir sorgunun sonucu (*sql.Rows) kapatılana kadar,
o sorgunun kullandığı bağlantı havuza geri dönmüyor — kapatmayı unutmak, havuzu tıkıyor.
SS19. — Havuz tek bağlantıyla sınırlanıyor. İlk sorgunun sonucu kasıtlı olarak kapatılmadan, aynı havuzdan ikinci bir sorgu (kısa bir zaman aşımıyla) deneniyor; sonra ilk sonuç kapatılıp üçüncü bir sorgu deneniyor.
// havuz.go — kapatilmayan bir satir kumesi tek baglantili havuzu tikiyor
package main
import (
"context"
"database/sql"
"fmt"
)
func main() {
db, _ := sql.Open("kukla", "bellek-ici")
defer db.Close()
db.SetMaxOpenConns(1)
birinci, _ := db.Query("select id, ad, fiyat_kurus, aktif, eklenme from urunler")
birinci.Next() // baglantiyi acik tutuyor, henuz kapatilmadi
fmt.Println("birinci sorgudan sonra acik baglanti sayisi:", db.Stats().OpenConnections)
baglamKisa, iptal := context.WithTimeout(context.Background(), 0)
defer iptal()
_, hataIkinci := db.QueryContext(baglamKisa, "select id, ad, fiyat_kurus, aktif, eklenme from urunler")
fmt.Println("havuz doluyken ikinci sorgu basarisiz mi:", hataIkinci != nil)
birinci.Close() // baglantiyi havuza geri birakir
ucuncu, hataUcuncu := db.Query("select id, ad, fiyat_kurus, aktif, eklenme from urunler")
fmt.Println("kapatmadan sonra sorgu basarili mi:", hataUcuncu == nil)
if hataUcuncu == nil {
defer ucuncu.Close()
}
}
birinci sorgudan sonra acik baglanti sayisi: 1 havuz doluyken ikinci sorgu basarisiz mi: true kapatmadan sonra sorgu basarili mi: true
Havuz tek bağlantıya sınırlandığında, ilk sorgunun sonucu kapatılmadan durduğu sürece o tek
bağlantı meşgul kalıyor. İkinci sorgu, süresi anında dolan bir bağlamla (WithTimeout
sıfır süreyle) deneniyor — gerçek bir süre ölçülmüyor, yalnızca “havuzda boş bağlantı yok”
durumunun hemen bir hata olarak dönmesi sağlanıyor. birinci.Close() çağrıldığında bağlantı
havuza geri dönüyor ve üçüncü sorgu sorunsuz çalışıyor. Bu, “kapatma sorumluluğu” ifadesinin
somut karşılığı: bir *sql.Rows değeri kapatılmadığı sürece, altındaki bağlantıyı elinde
tutmaya devam ediyor — havuzun büyüklüğü sınırlıysa, bu unutma diğer sorguları
tıkayabiliyor. Sürücünün kendisi (kukla ya da gerçek) burada rol oynamıyor; havuzun yönetimi
tamamen database/sql paketinin kendi sorumluluğunda.
Bu ölçüm havuzu bilerek tek bağlantıya indirdi ki tıkanma bir tek sorguyla görünür olsun;
gerçek boyuttaki bir havuzda aynı unutma birden fazla sorgu birikene kadar fark edilmeyebiliyor.
Kod içinde defer satirlar.Close() yazmak — Query çağrısından hemen sonra, satırların
kullanılmasından önce — bu unutmayı önlemenin standart yolu; bu ders boyunca kullanılan kalıp
da bu. Kapatmanın kendisi ölçülebilir bir sonuç üretmiyor, yalnızca bir sonraki sorgunun
başarılı olup olmamasını belirliyor — tam olarak bu ölçümün gösterdiği şey.
Özet
- Sürücünün taşıdığı tip kümesi (
driver.Value) yalnızca yedi tiple sınırlı; bir sütunun “gerçek” anlamı ne olursa olsun, sürücü sınırını geçerken bu kümeye indirgeniyor. - Beş sütunun üçü (
id,fiyat_kurus,eklenme) hedef tipin kendiScanyöntemiyle geri kuruluyor; ikisi (ad,aktif) zaten sürücünün doğal tipiyle örtüştüğü için doğrudan atanıyor. - Sınırlayıcı ölçüm: tarama hedefin tipine bakıyor, kaynağın tipine değil — uyumsuz bir hedef
sorgu yazılırken değil,
Scançağrıldığında, çalışma zamanında hata veriyor. sql.DBbir bağlantı havuzu yönetiyor; bir sorgu sonucu kapatılmadığı sürece altındaki bağlantıyı elinde tutmaya devam ediyor.- Kapatılmayan bir satır kümesi, sınırlı büyüklükteki bir havuzda sonraki sorguları tıkayabiliyor; kapatmak bağlantıyı havuza geri bırakıyor.
Sonraki Adım
Bu derste tarama, hedefin tipine bakarak sürücünün dar tip kümesinden bir Go değerini geri
kurdu — tarif kodun kendisindeydi, Scane verilen değişkenin tipinde. Konunun beş dersi
boyunca aynı soru beş ayrı yerde soruldu: dosya ve arabellekte, ikili dosyaya gömülen bir
kaynakta, metne dönen bir zaman değerinde, alan adlarını taşıyan bir günlük kaydında, ve
şimdi bir veritabanı satırında. Beşinde de sınırı geçen değer kendi başına yeterli değildi;
geri kurmayı sağlayan şey her seferinde karşı tarafın elindeki bir tarifti.
Tarifin nerede durduğu her derste değişti. Gömülü kaynakta tarif derleme zamanındaydı — hedef
değişkenin kendi tipi. Zaman değerinde tarif çağıranın bildiği bir düzendi. Günlük kaydında
tarif okuyan kodun elle yazdığı bir dönüşümdü. Bu derste tarif, hedef tipin kendi Scan
yöntemiydi — bir sözleşme olarak, standart kitaplığın kendisine bağlanmış. Ortak nokta hep
aynı: sınırın kendisi hiçbir zaman tip bilgisini taşımadı, taşıyan şey her defasında sınırın
bu tarafında duran bir bilgiydi.
Sıradaki ders bu konunun kapanışı ve aynı soruyu programın girişine taşıyor: bir komut satırı argümanı da, tıpkı bir günlük satırı ya da bir veritabanı sütunu gibi, dizgi olarak geliyor. Programın kendi ayrıştırıcısı bu dizgiden hangi tipleri, ne kadarını geri kurabiliyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.