PHP'de PDO ile Veritabanı İşlemleri: Bağlantı, Sorgu ve İşlem Yönetimi

PHP'de PDO ile Veritabanı İşlemleri: Bağlantı, Sorgu ve İşlem Yönetimi

6 dakika okuma

PDO, farklı veritabanları için ortak bir arayüz sunar. Sürücü değiştiğinde sorgu metinleri farklılaşsa da bağlantı, hazırlama ve okuma kodu aynı kalır. Bu bölümde bağlantının doğru yapılandırılması, hazır sorguların kullanımı, okuma kipleri ve toplu işlem yönetimi ele alınıyor.

Yapılandırma seçenekleri çoğu projede varsayılan haliyle bırakılır ve bu, sonradan fark edilen sorunların önemli bir bölümünün kaynağıdır. Hatalar sessizce yutulur, sayılar metin olarak döner, karakter kümesi ayarlanmadığı için Türkçe karakterler bozulur.

Bağlantı kurmak

Bağlantı bir kez kurulur ve uygulama boyunca paylaşılır. Bağlantı dizesinde karakter kümesinin belirtilmesi zorunludur; belirtilmediğinde sunucu varsayılanı kullanılır ve Türkçe metinlerde bozulma yaşanır.

PHP8 satır
$dsn = 'mysql:host=127.0.0.1;dbname=uygulama;charset=utf8mb4';

$pdo = new PDO($dsn, $kullanici, $parola, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,       // hatalar istisnaya dönüşür
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,  // yalnızca ad ile erişim
    PDO::ATTR_EMULATE_PREPARES => false,               // gerçek hazır sorgu kullanılır
    PDO::ATTR_STRINGIFY_FETCHES => false,              // sayılar sayı olarak döner
]);

Öykünmüş hazır sorgular kapatıldığında, sorgu metni ile parametreler veritabanı sunucusuna ayrı ayrı gönderilir. Bu, enjeksiyona karşı en güçlü yapısal korumadır ve tip bilgisinin korunmasını da sağlar. Bağlantı bilgileri koda gömülmez; ortam değişkeninden okunur.

Bağlantının gerçekten ihtiyaç duyulduğunda kurulması, sayfa üretimi sırasında veritabanına hiç gidilmeyen isteklerde gereksiz maliyeti ortadan kaldırır. Bunun için bağlantı bir sınıf içinde tembel biçimde oluşturulur.

PHP19 satır
final class Baglanti
{
    private ?PDO $pdo = null;

    public function __construct(
        private readonly string $dsn,
        private readonly string $kullanici,
        private readonly string $parola,
    ) {}

    public function al(): PDO
    {
        return $this->pdo ??= new PDO($this->dsn, $this->kullanici, $this->parola, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
        ]);
    }
}

Hazır sorgular

Kullanıcı verisi sorgu metnine birleştirilmez, parametre olarak bağlanır. İsimli parametreler okunabilirliği artırır ve parametre sırası değiştiğinde hata riskini ortadan kaldırır.

PHP13 satır
$sorgu = $pdo->prepare(
    'SELECT id, baslik, olusturma FROM yazilar
     WHERE kategori_id = :kategori AND durum = :durum
     ORDER BY olusturma DESC
     LIMIT :adet'
);

$sorgu->bindValue(':kategori', $kategoriId, PDO::PARAM_INT);
$sorgu->bindValue(':durum', 'yayinda', PDO::PARAM_STR);
$sorgu->bindValue(':adet', $adet, PDO::PARAM_INT);   // LIMIT için tip belirtilmeli
$sorgu->execute();

$satirlar = $sorgu->fetchAll();

Parametre dizisi doğrudan çalıştırma metoduna da verilebilir. Bu kısa yolda tüm değerler metin olarak bağlanır; sayısal sınır ifadelerinde tip belirtmek gerektiği için orada ayrı bağlama tercih edilir.

PHP5 satır
$sorgu = $pdo->prepare('SELECT * FROM kullanicilar WHERE eposta = ?');
$sorgu->execute([$eposta]);

$kullanici = $sorgu->fetch();          // tek satır ya da false
$sayi = $pdo->query('SELECT COUNT(*) FROM kullanicilar')->fetchColumn();

Tablo ve sütun adları parametre olarak bağlanamaz. Bu değerler kullanıcıdan geliyorsa, izin verilen adların bulunduğu bir listeyle eşleştirilerek doğrulanmalıdır.

PHP7 satır
$izinli = ['olusturma', 'baslik', 'okunma'];

$sutun = in_array($_GET['sirala'] ?? '', $izinli, strict: true)
    ? $_GET['sirala']
    : 'olusturma';

$sorgu = $pdo->prepare("SELECT * FROM yazilar ORDER BY {$sutun} DESC");

Liste parametreleri

Bir küme içinde arama yapan sorgularda parametre sayısı çalışma zamanında belirlenir. Yer tutucular dizinin boyutuna göre üretilir; değerler yine bağlanarak gönderilir.

PHP6 satır
$idler = [4, 9, 17, 23];

$yerTutucu = implode(',', array_fill(0, count($idler), '?'));

$sorgu = $pdo->prepare("SELECT id, baslik FROM yazilar WHERE id IN ({$yerTutucu})");
$sorgu->execute($idler);

Aynı yaklaşım toplu ekleme sorgularında da kullanılır: değer grupları dizinin boyutuna göre üretilir ve tüm değerler tek bir çalıştırmada bağlanır. Kayıt başına ayrı sorgu göndermeye kıyasla ağ gidiş dönüş sayısını belirgin biçimde azaltır.

Dizinin boş olabileceği durumlar ayrıca ele alınmalıdır; boş bir küme ile kurulan sorgu sözdizimi hatası verir. Bu tür sorgularda önce eleman sayısı denetlenir.

Yazma işlemleri

PHP18 satır
$sorgu = $pdo->prepare(
    'INSERT INTO yazilar (baslik, icerik, olusturma)
     VALUES (:baslik, :icerik, :olusturma)'
);

$sorgu->execute([
    'baslik' => $baslik,
    'icerik' => $icerik,
    'olusturma' => (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s'),
]);

$yeniId = (int) $pdo->lastInsertId();

// güncellemede etkilenen satır sayısı
$guncelle = $pdo->prepare('UPDATE yazilar SET durum = ? WHERE id = ?');
$guncelle->execute(['yayinda', $yeniId]);

$etkilenen = $guncelle->rowCount();

Etkilenen satır sayısı her zaman güvenilir bir başarı ölçütü değildir. Aynı değerin yeniden yazıldığı bir güncellemede bazı sürücüler sıfır döndürür. Başarı ölçütü, istisna fırlatılmamış olmasıdır.

Toplu işlemler

Birden fazla yazma işlemi mantıksal olarak tek bir bütün oluşturuyorsa, hepsinin ya birlikte tamamlanması ya da hiçbirinin uygulanmaması gerekir. İşlem bloğu bu garantiyi sağlar.

PHP12 satır
$pdo->beginTransaction();

try {
    $stokDus->execute(['adet' => $adet, 'id' => $urunId]);
    $siparisEkle->execute(['kullanici' => $kullaniciId, 'tutar' => $tutar]);

    $pdo->commit();
} catch (Throwable $e) {
    $pdo->rollBack();

    throw $e;          // hata yutulmaz, yukarı taşınır
}

İşlem blokları iç içe açılmamalıdır; çoğu sürücü bunu desteklemez ve içteki tamamlama, dıştaki bloğu da kapatır. İşlem yönetimi, iş akışını bilen tek bir katmanda toplanır. Uzun süren işlemler de kilit tuttukları için kısa tutulmalıdır; dış servis çağrıları işlem bloğunun içine alınmaz.

Okuma kipleri

Varsayılan kip ile satırlar birleşimli dizi olarak döner. Farklı ihtiyaçlar için ayrı kipler bulunur; doğru kipin seçilmesi hem kodu kısaltır hem de gereksiz dönüşümleri ortadan kaldırır.

PHP10 satır
// tek sütunluk liste
$idler = $pdo->query('SELECT id FROM yazilar')->fetchAll(PDO::FETCH_COLUMN);

// anahtar => değer eşlemesi
$sozluk = $pdo->query('SELECT id, baslik FROM yazilar')->fetchAll(PDO::FETCH_KEY_PAIR);

// doğrudan sınıf örneklerine eşleme
$sorgu = $pdo->prepare('SELECT id, baslik FROM yazilar WHERE durum = ?');
$sorgu->setFetchMode(PDO::FETCH_CLASS, Yazi::class);
$sorgu->execute(['yayinda']);

Büyük sonuç kümelerinde tüm satırların tek seferde belleğe alınması bellek sınırını zorlar. Sorgu nesnesi üzerinde doğrudan dolaşıldığında satırlar teker teker alınır ve bellek kullanımı sabit kalır.

PHP5 satır
$sorgu = $pdo->query('SELECT id, baslik FROM yazilar');

foreach ($sorgu as $satir) {      // tüm sonuç belleğe alınmaz
    isle($satir);
}

Tekrarlayan sorgular ve N+1

Bir liste çekip her satır için ayrı sorgu çalıştırmak, sayfa üretim süresini kayıt sayısıyla doğru orantılı büyütür. Yirmi kayıtlık bir listede bu, tek sorgu yerine yirmi bir sorgu anlamına gelir.

PHP14 satır
// sorunlu: her yazı için ayrı sorgu
foreach ($yazilar as $yazi) {
    $yazar = $pdo->prepare('SELECT ad FROM yazarlar WHERE id = ?');
    $yazar->execute([$yazi['yazar_id']]);
}

// çözüm: tek sorguda birleştirmek
$sorgu = $pdo->prepare(
    'SELECT y.id, y.baslik, z.ad AS yazar
     FROM yazilar y
     INNER JOIN yazarlar z ON z.id = y.yazar_id
     WHERE y.durum = ?'
);
$sorgu->execute(['yayinda']);

Aynı sorgunun döngü içinde tekrar tekrar hazırlanması da gereksiz maliyettir. Hazırlama bir kez yapılır, çalıştırma her kayıt için tekrarlanır. Toplu yazma işlemlerinde bu düzen, tek bir işlem bloğuyla birleştirildiğinde belirgin hızlanma sağlar.

PHP9 satır
$ekle = $pdo->prepare('INSERT INTO kayitlar (ad, deger) VALUES (?, ?)');

$pdo->beginTransaction();

foreach ($satirlar as $satir) {
    $ekle->execute([$satir['ad'], $satir['deger']]);   // hazırlama tekrarlanmaz
}

$pdo->commit();

Birleştirmenin uygun olmadığı durumlarda ikinci sorgu tek seferde çalıştırılır: önce tüm kimlikler toplanır, ardından küme sorgusuyla ilgili kayıtlar çekilip bellekte eşleştirilir. İki sorgu, kayıt sayısı ne olursa olsun sabit kalır.

Sorgu sayısının ve süresinin ölçülmesi, sorunu tahmin etmeden görmeyi sağlar. Geliştirme ortamında istek başına çalışan sorguların sayılması, listeye yeni bir alan eklendiğinde sorgu sayısının sessizce artmasını fark ettirir. Yavaş sorguların veritabanı tarafında kaydedilmesi de aynı amaca hizmet eder.

Kontrol listesi

  • Bağlantı dizesinde charset=utf8mb4 tanımlı.
  • Hata kipi istisna, öykünmüş hazır sorgular kapalı.
  • Kullanıcı verisi sorguya birleştirilmiyor, parametre olarak bağlanıyor.
  • Tablo ve sütun adları izin listesiyle doğrulanıyor.
  • Bağlantı bilgileri ortam değişkeninden okunuyor.
  • İlişkili yazma işlemleri tek bir işlem bloğunda; hata durumunda geri alınıyor.
  • Liste sayfalarında satır başına ek sorgu çalışmıyor.

İlgili yazılar

PHP eğitim serisi

Bu yazı, temel sözdiziminden üretim ortamına kadar ilerleyen 25 bölümlük serinin bir parçası. Bölümler sırayla okunacak biçimde hazırlandı, ancak her biri tek başına da kullanılabilir.

Temeller

  1. Kurulum ve temel sözdizimi
  2. Döngüler ve akış kontrolü
  3. Diziler
  4. Fonksiyonlar
  5. String fonksiyonları
  6. Süper globaller
  7. Form işleme
  8. Dosya işlemleri

Dilin ayrıntıları

  1. Tip sistemi ve strict_types
  2. Hata ve istisna yönetimi
  3. Düzenli ifadeler
  4. Tarih ve saat işlemleri

Nesne yönelimli PHP

  1. Sınıf ve nesne
  2. Kalıtım, arayüz ve trait
  3. Statik üyeler, sabitler ve enum
  4. Namespace, Composer ve autoload

Veri ve güvenlik

  1. PDO ile veritabanı işlemleri (bu yazı)
  2. Oturum yönetimi ve kimlik doğrulama
  3. Güvenli çıktı ve XSS
  4. JSON ile veri değişimi

Dış dünya ve üretim

  1. cURL ile dış API çağırma
  2. Kendi REST API'ni yazmak
  3. PHPUnit ile birim test
  4. Hata ayıklama ve performans ölçümü
  5. Üretime alma ve güvenlik

Yorumlar

Bu form reCAPTCHA ile korunur; Google Gizlilik Politikası ve Hizmet Şartları geçerlidir.

İlk yorumu sen yap.

İlgili Yazılar