İçeriğe geç
academia.sh

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 kuruluyorKullaniciID.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 kendi Scan yö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.DB bir 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat