← Kursa Dön
📄 Text · 20 min

JDBC ve Veritabanı Bağlantısı

Hemen hemen her uygulama bir noktada veri saklamak ve sorgulamak zorundadır. Kullanıcı bilgileri, siparişler, ürünler, loglar... Bunların hepsi veritabanında yaşar. Java'nın veritabanıyla konuşma yolu JDBC (Java Database Connectivity) üzerinden geçer.

Bu derste JDBC'nin ne olduğunu, veritabanına nasıl bağlanacağını, veri ekleme-okuma-güncelleme-silme (CRUD) işlemlerini, SQL injection'dan korunmayı, transaction yönetimini ve connection pooling'i sıfırdan öğreneceğiz.


1. JDBC Nedir?

JDBC, Java'nın veritabanlarıyla iletişim kurmasını sağlayan standart bir API'dir. java.sql paketinde yaşar ve Java'nın kendisiyle birlikte gelir — ekstra bir şey kurman gerekmez.

JDBC'nin güzelliği veritabanı bağımsız olmasıdır. Aynı Java koduyla MySQL, PostgreSQL, Oracle, SQLite veya başka herhangi bir ilişkisel veritabanına bağlanabilirsin. Tek değişen şey JDBC driver ve connection string.

🎯 Analoji — Evrensel Kumanda:

>

JDBC'yi bir evrensel TV kumandası gibi düşün. Kumandanın "aç", "kapat", "ses yükselt" tuşları her TV'de aynı işi yapar. Ama her TV markası için farklı bir "kod" girersin — Samsung için bir kod, LG için başka bir kod. JDBC de böyle: API (kumanda) aynı, ama her veritabanı için farklı bir driver (kod) kullanırsın.

JDBC Mimarisi

Java Uygulaması
      ↓
   JDBC API (java.sql)
      ↓
   JDBC Driver (veritabanına özel)
      ↓
   Veritabanı (MySQL, PostgreSQL, SQLite...)

JDBC driver, veritabanı üreticisi tarafından sağlanır. Maven veya Gradle ile projeye eklenir:

<!-- MySQL Driver -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.3.0</version>
</dependency>

<!-- PostgreSQL Driver -->
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.7.1</version>
</dependency>

<!-- H2 (gömülü, test için ideal) -->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.2.224</version>
</dependency>

2. Veritabanına Bağlanma — Connection

Her şey bir Connection (bağlantı) ile başlar. DriverManager.getConnection() metodu ile veritabanına bağlanırsın.

Connection String Formatı

Her veritabanının kendine özel bir URL formatı vardır:

jdbc:mysql://localhost:3306/mydb        ← MySQL
jdbc:postgresql://localhost:5432/mydb   ← PostgreSQL
jdbc:h2:mem:testdb                      ← H2 (in-memory)
jdbc:sqlite:database.db                 ← SQLite
jdbc:oracle:thin:@localhost:1521:orcl   ← Oracle

Genel format: jdbc:<veritabanı>://<host>:<port>/<database_adı>

İlk Bağlantı

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

class Main {
    public static void main(String[] args) {
        String url = "jdbc:h2:mem:testdb";
        String user = "sa";
        String password = "";

        try (Connection conn = DriverManager.getConnection(url, user, password)) {
            System.out.println("Bağlantı başarılı!");
            System.out.println("Veritabanı: " + conn.getMetaData().getDatabaseProductName());
            System.out.println("Versiyon: " + conn.getMetaData().getDatabaseProductVersion());
        } catch (SQLException e) {
            System.err.println("Bağlantı hatası: " + e.getMessage());
        }
    }
}

Burada try-with-resources kullandık. Connection bir kaynak (resource) olduğu için işimiz bittiğinde mutlaka kapatılmalı. try-with-resources bunu otomatik yapar — hata olsa bile close() çağrılır.

⚠️ Dikkat: Connection'ı kapatmayı unutmak Java'daki en yaygın veritabanı hatalarından biridir. Kapatılmayan connection'lar birikir ve bir süre sonra veritabanı yeni bağlantı kabul edemez hale gelir. Her zaman try-with-resources kullan.


3. Statement ile SQL Çalıştırma

Bağlantıyı kurduktan sonra SQL komutları çalıştırmak için Statement kullanırız. İki tür Statement var:

  • Statement — Basit, sabit SQL'ler için

  • PreparedStatement — Parametreli SQL'ler için (bunu kullan!)

Statement (Basit Kullanım)

try (Connection conn = DriverManager.getConnection(url, user, password);
     Statement stmt = conn.createStatement()) {

    // Tablo oluştur
    stmt.execute("""
        CREATE TABLE IF NOT EXISTS users (
            id INT AUTO_INCREMENT PRIMARY KEY,
            name VARCHAR(100) NOT NULL,
            email VARCHAR(150) UNIQUE,
            age INT
        )
    """);

    // Veri ekle
    int affected = stmt.executeUpdate(
        "INSERT INTO users (name, email, age) VALUES ('Ali', 'ali@test.com', 25)"
    );
    System.out.println(affected + " satır eklendi");

    // Veri oku
    var rs = stmt.executeQuery("SELECT * FROM users");
    while (rs.next()) {
        System.out.println(rs.getInt("id") + " - " +
                           rs.getString("name") + " - " +
                           rs.getString("email"));
    }
}

Üç farklı execute metodu:

  • execute(sql) — DDL (CREATE, ALTER, DROP) için. boolean döner

  • executeUpdate(sql) — DML (INSERT, UPDATE, DELETE) için. Etkilenen satır sayısı döner

  • executeQuery(sql) — SELECT için. ResultSet döner


4. PreparedStatement — SQL Injection'dan Korunma

Statement basit iş görür ama ciddi bir güvenlik açığı barındırır: SQL Injection.

SQL Injection Nedir?

Kullanıcıdan alınan veriyi doğrudan SQL'e yapıştırırsan kötü niyetli kullanıcılar SQL komutları enjekte edebilir:

// ❌ TEHLİKELİ — Asla böyle yapma!
String username = userInput; // kullanıcı: "'; DROP TABLE users; --"
String sql = "SELECT * FROM users WHERE name = '" + username + "'";
stmt.executeQuery(sql);
// Oluşan SQL: SELECT * FROM users WHERE name = ''; DROP TABLE users; --'
// Tüm tablo silindi!

Bu, web güvenliğindeki en yaygın saldırı vektörlerinden biridir. Çözüm? PreparedStatement.

PreparedStatement ile Güvenli Sorgulama

PreparedStatement, SQL'i ve parametreleri ayrı ayrı veritabanına gönderir. Veritabanı önce SQL'in yapısını derler, sonra parametreleri değer olarak yerleştirir. Parametre ne olursa olsun SQL yapısını bozamaz.

// ✅ GÜVENLİ — Her zaman böyle yap
String sql = "SELECT * FROM users WHERE name = ? AND age > ?";

try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
    pstmt.setString(1, username);  // 1. parametre (?)
    pstmt.setInt(2, 18);           // 2. parametre (?)

    try (ResultSet rs = pstmt.executeQuery()) {
        while (rs.next()) {
            System.out.println(rs.getString("name") + " - " + rs.getInt("age"));
        }
    }
}

? işaretleri placeholder (yer tutucu). setString(), setInt(), setDouble() gibi tip-güvenli metodlarla değer atanır. Parametre indeksleri 1'den başlar (0'dan değil!).

⚠️ Dikkat: Kullanıcıdan gelen hiçbir veriyi doğrudan SQL string'ine yapıştırma. Her zaman PreparedStatement kullan. Bu sadece güvenlik değil, aynı zamanda veritabanı sorgu planı cache'lemesi sayesinde performans da sağlar.

PreparedStatement'ın Diğer Avantajları

  1. Güvenlik: SQL Injection imkansız

  2. Performans: Veritabanı sorguyu önceden derler, tekrar kullanımda hızlı

  3. Tip güvenliği: setInt(), setString() ile doğru tipi garanti edersin

  4. Okunabilirlik: Parametreler net, SQL yapısı temiz


5. ResultSet ile Veri Okuma

executeQuery() bir ResultSet döner. ResultSet, sorgu sonucunu satır satır dolaşmamızı sağlayan bir imleç (cursor) gibi çalışır.

String sql = "SELECT id, name, email, age FROM users WHERE age >= ?";

try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
    pstmt.setInt(1, 20);

    try (ResultSet rs = pstmt.executeQuery()) {
        while (rs.next()) {  // bir sonraki satıra ilerle
            int id = rs.getInt("id");           // kolon adıyla
            String name = rs.getString("name");
            String email = rs.getString(3);     // kolon indeksiyle (1-based)
            int age = rs.getInt("age");

            System.out.printf("ID: %d, Ad: %s, E-posta: %s, Yaş: %d%n",
                              id, name, email, age);
        }
    }
}

ResultSet ipuçları:

  • rs.next() bir sonraki satıra ilerler. Satır varsa true, yoksa false döner

  • Kolon değerlerini isme veya indekse göre alabilirsin (isim tercih et — daha okunabilir)

  • İndeksler 1'den başlar

  • rs.wasNull() ile son okunan değerin NULL olup olmadığını kontrol edebilirsin

  • Primitive tipler (int, double) için NULL değer 0 döner — dikkat!

NULL Kontrolü

int age = rs.getInt("age");
if (rs.wasNull()) {
    System.out.println("Yaş bilgisi yok");
} else {
    System.out.println("Yaş: " + age);
}

// Veya wrapper tipleri kullan
Integer ageObj = rs.getObject("age", Integer.class); // null olabilir

6. CRUD İşlemleri — Tam Örnek

Şimdi tüm CRUD (Create, Read, Update, Delete) işlemlerini birleştiren pratik bir örnek yapalım:

import java.sql.*;

class UserDao {
    private final String url;
    private final String user;
    private final String password;

    UserDao(String url, String user, String password) {
        this.url = url;
        this.user = user;
        this.password = password;
    }

    // CREATE
    int createUser(String name, String email, int age) throws SQLException {
        String sql = "INSERT INTO users (name, email, age) VALUES (?, ?, ?)";
        try (Connection conn = DriverManager.getConnection(url, user, password);
             PreparedStatement pstmt = conn.prepareStatement(sql,
                 Statement.RETURN_GENERATED_KEYS)) {

            pstmt.setString(1, name);
            pstmt.setString(2, email);
            pstmt.setInt(3, age);
            pstmt.executeUpdate();

            try (ResultSet keys = pstmt.getGeneratedKeys()) {
                if (keys.next()) {
                    return keys.getInt(1);  // oluşturulan ID
                }
            }
            return -1;
        }
    }

    // READ
    void findByAge(int minAge) throws SQLException {
        String sql = "SELECT * FROM users WHERE age >= ? ORDER BY name";
        try (Connection conn = DriverManager.getConnection(url, user, password);
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setInt(1, minAge);
            try (ResultSet rs = pstmt.executeQuery()) {
                while (rs.next()) {
                    System.out.printf("[%d] %s (%s) - %d yaşında%n",
                        rs.getInt("id"), rs.getString("name"),
                        rs.getString("email"), rs.getInt("age"));
                }
            }
        }
    }

    // UPDATE
    int updateEmail(int userId, String newEmail) throws SQLException {
        String sql = "UPDATE users SET email = ? WHERE id = ?";
        try (Connection conn = DriverManager.getConnection(url, user, password);
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setString(1, newEmail);
            pstmt.setInt(2, userId);
            return pstmt.executeUpdate();  // etkilenen satır sayısı
        }
    }

    // DELETE
    int deleteUser(int userId) throws SQLException {
        String sql = "DELETE FROM users WHERE id = ?";
        try (Connection conn = DriverManager.getConnection(url, user, password);
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setInt(1, userId);
            return pstmt.executeUpdate();
        }
    }
}

Statement.RETURN_GENERATED_KEYS parametresi, INSERT sonrası otomatik oluşturulan ID'yi almamızı sağlar. Veritabanı yeni kaydın ID'sini döner ve biz getGeneratedKeys() ile okuruz.


7. Transaction Yönetimi

Bazı işlemler birden fazla SQL komutundan oluşur ve ya hepsi başarılı olmalı ya da hiçbiri. Örneğin banka havalesi: bir hesaptan para düş, diğerine ekle. İkisinden biri başarısız olursa para havada kalır!

Auto-Commit ve Manuel Transaction

JDBC varsayılan olarak auto-commit modundadır — her SQL komutu otomatik olarak commit edilir. Transaction kullanmak için auto-commit'i kapatman gerekir.

Connection conn = DriverManager.getConnection(url, user, password);
conn.setAutoCommit(false);  // Transaction başlat

try {
    // Gönderenin bakiyesinden düş
    PreparedStatement withdraw = conn.prepareStatement(
        "UPDATE accounts SET balance = balance - ? WHERE id = ?");
    withdraw.setDouble(1, 500.0);
    withdraw.setInt(2, fromAccountId);
    int rows1 = withdraw.executeUpdate();

    // Alıcının bakiyesine ekle
    PreparedStatement deposit = conn.prepareStatement(
        "UPDATE accounts SET balance = balance + ? WHERE id = ?");
    deposit.setDouble(1, 500.0);
    deposit.setInt(2, toAccountId);
    int rows2 = deposit.executeUpdate();

    if (rows1 == 0 || rows2 == 0) {
        throw new SQLException("Hesap bulunamadı!");
    }

    conn.commit();  // Her şey başarılıysa onayla
    System.out.println("Havale başarılı!");

} catch (SQLException e) {
    conn.rollback();  // Hata varsa tüm değişiklikleri geri al
    System.err.println("Havale başarısız, geri alındı: " + e.getMessage());

} finally {
    conn.setAutoCommit(true);  // Eski duruma dön
    conn.close();
}

Transaction kuralları:

  • setAutoCommit(false) — Transaction başlatır

  • commit() — Tüm değişiklikleri kalıcı yapar

  • rollback() — Tüm değişiklikleri geri alır

  • Hata durumunda her zaman rollback yap

Savepoint

Uzun transaction'larda belirli bir noktaya kadar geri almak isteyebilirsin:

conn.setAutoCommit(false);

// İlk işlem
stmt.executeUpdate("INSERT INTO log (message) VALUES ('İşlem başladı')");

Savepoint sp = conn.setSavepoint("after_log");

try {
    // Riskli işlem
    stmt.executeUpdate("UPDATE inventory SET stock = stock - 1 WHERE id = 42");
} catch (SQLException e) {
    conn.rollback(sp);  // Sadece savepoint'ten sonrasını geri al
    // log kaydı hâlâ duruyor
}

conn.commit();

8. Batch İşlemler

Çok sayıda INSERT veya UPDATE yapmak gerektiğinde tek tek göndermek yerine batch (toplu) gönderim kullanmak performansı dramatik şekilde artırır.

String sql = "INSERT INTO products (name, price) VALUES (?, ?)";

try (Connection conn = DriverManager.getConnection(url, user, password);
     PreparedStatement pstmt = conn.prepareStatement(sql)) {

    conn.setAutoCommit(false);

    String[][] products = {
        {"Laptop", "15999.99"}, {"Telefon", "8999.99"},
        {"Tablet", "5499.99"}, {"Kulaklık", "1299.99"},
        {"Mouse", "399.99"}
    };

    for (String[] product : products) {
        pstmt.setString(1, product[0]);
        pstmt.setDouble(2, Double.parseDouble(product[1]));
        pstmt.addBatch();  // Kuyruğa ekle
    }

    int[] results = pstmt.executeBatch();  // Toplu gönder
    conn.commit();

    System.out.println(results.length + " ürün eklendi");
}

1000 kayıt eklerken tek tek gönderim 10 saniye sürerken, batch ile 0.5 saniyede tamamlanabilir. Fark bu kadar büyük.


9. Connection Pooling — HikariCP

Her CRUD işlemi için yeni bir Connection açmak ve kapatmak çok pahalı bir operasyondur. Veritabanına TCP bağlantısı kurmak, kimlik doğrulaması yapmak, kaynakları ayırmak... Bunların hepsi zaman alır.

Connection pooling, önceden açılmış bağlantıları bir havuzda tutar. İhtiyaç olduğunda havuzdan alır, işin bitince havuza geri koyarsın. Bağlantı açma/kapama maliyeti ortadan kalkar.

HikariCP — En Hızlı Connection Pool

HikariCP, Java ekosisteminin en hızlı ve en yaygın connection pool kütüphanesidir. Spring Boot bile varsayılan olarak HikariCP kullanır.

Maven'a ekle:

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>5.1.0</version>
</dependency>

Kullanımı:

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

class DatabasePool {
    private static final HikariDataSource dataSource;

    static {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        config.setUsername("root");
        config.setPassword("secret");

        // Pool ayarları
        config.setMaximumPoolSize(10);       // Max 10 bağlantı
        config.setMinimumIdle(2);            // Min 2 boşta bağlantı
        config.setConnectionTimeout(30000);  // 30 sn bağlantı timeout
        config.setIdleTimeout(600000);       // 10 dk boşta kalma timeout
        config.setMaxLifetime(1800000);      // 30 dk max yaşam süresi

        dataSource = new HikariDataSource(config);
    }

    static Connection getConnection() throws SQLException {
        return dataSource.getConnection();  // Havuzdan al
    }

    static void close() {
        dataSource.close();  // Uygulama kapanırken
    }
}

Kullanımda hiçbir fark yok — sadece DriverManager yerine pool'dan alırsın:

// Eskisi: DriverManager.getConnection(url, user, password)
// Yenisi:
try (Connection conn = DatabasePool.getConnection();
     PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users")) {

    // ... normal JDBC kullanımı
    // try-with-resources bitince bağlantı havuza geri döner (kapanmaz!)
}

💡 İpucu: try-with-resources ile aldığın pooled connection kapanmaz, havuza geri döner. Bu yüzden kodu hiç değiştirmeden DriverManager'dan HikariCP'ye geçebilirsin.

Pool Boyutu Ne Olmalı?

Yaygın bir hata: "Daha fazla connection = daha hızlı" düşüncesi. Aslında tam tersi olabilir! HikariCP'nin yaratıcısı şu formülü önerir:

Pool size = (core_count * 2) + effective_spindle_count

Çoğu uygulama için 5-10 connection yeterlidir. 100+ connection kurmak veritabanını boğar.


10. try-with-resources ve Kaynak Yönetimi

JDBC'de en kritik konu kaynak yönetimidir. Connection, Statement ve ResultSet hepsi kapatılması gereken kaynaklardır. Kapatılmazlarsa bellek sızıntısı (memory leak) ve connection sızıntısı oluşur.

Doğru Yol — try-with-resources

// ✅ Tüm kaynaklar otomatik kapatılır
try (Connection conn = DriverManager.getConnection(url, user, password);
     PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users");
     ResultSet rs = pstmt.executeQuery()) {

    while (rs.next()) {
        System.out.println(rs.getString("name"));
    }

} catch (SQLException e) {
    e.printStackTrace();
}
// Burada conn, pstmt ve rs hepsi kapatılmış durumda

Yanlış Yol — Manuel Kapatma

// ❌ Hata olursa kaynaklar açık kalabilir
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;

try {
    conn = DriverManager.getConnection(url, user, password);
    pstmt = conn.prepareStatement("SELECT * FROM users");
    rs = pstmt.executeQuery();

    while (rs.next()) {
        System.out.println(rs.getString("name"));
    }
} catch (SQLException e) {
    e.printStackTrace();
} finally {
    // Her birini ayrı try-catch'te kapatmalısın... Karmaşık ve hata yapmaya açık
    if (rs != null) try { rs.close(); } catch (SQLException e) { }
    if (pstmt != null) try { pstmt.close(); } catch (SQLException e) { }
    if (conn != null) try { conn.close(); } catch (SQLException e) { }
}

Gördüğün gibi, try-with-resources hem daha kısa hem daha güvenli. Modern Java'da JDBC kullanırken her zaman try-with-resources tercih et.


11. DAO Pattern — Veritabanı Kodunu Organize Etme

Gerçek projelerde veritabanı kodunu doğrudan iş mantığına (business logic) karıştırmazsın. DAO (Data Access Object) pattern'i ile veritabanı operasyonlarını ayrı bir katmanda toplarız.

// Veri modeli
record User(int id, String name, String email, int age) {}

// DAO arayüzü
interface UserDao {
    User findById(int id);
    List<User> findAll();
    int save(User user);
    boolean update(User user);
    boolean delete(int id);
}

// JDBC implementasyonu
class JdbcUserDao implements UserDao {
    private final DataSource dataSource;

    JdbcUserDao(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public User findById(int id) {
        String sql = "SELECT * FROM users WHERE id = ?";
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setInt(1, id);
            try (ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    return new User(
                        rs.getInt("id"),
                        rs.getString("name"),
                        rs.getString("email"),
                        rs.getInt("age")
                    );
                }
            }
        } catch (SQLException e) {
            throw new RuntimeException("Kullanıcı bulunamadı: " + id, e);
        }
        return null;
    }

    @Override
    public List<User> findAll() {
        String sql = "SELECT * FROM users ORDER BY id";
        List<User> users = new ArrayList<>();
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql);
             ResultSet rs = pstmt.executeQuery()) {

            while (rs.next()) {
                users.add(new User(
                    rs.getInt("id"),
                    rs.getString("name"),
                    rs.getString("email"),
                    rs.getInt("age")
                ));
            }
        } catch (SQLException e) {
            throw new RuntimeException("Kullanıcılar listelenemedi", e);
        }
        return users;
    }

    // save, update, delete benzer şekilde...
}

DAO pattern'in avantajları:

  • Veritabanı kodu tek bir yerde toplanır

  • İş mantığı veritabanından bağımsız olur

  • Test yazmak kolaylaşır (mock DAO kullanabilirsin)

  • Veritabanı değiştirmek istersen sadece DAO implementasyonunu değiştirirsin


12. Yaygın Hatalar ve Best Practice

1. Connection Sızıntısı

Her zaman try-with-resources kullan. Kapatılmayan connection'lar sunucuyu çökertir.

2. SQL Injection

Kullanıcı girdisini asla string birleştirme ile SQL'e koyma. PreparedStatement kullan.

3. SELECT * Kullanma

Sadece ihtiyacın olan kolonları seç. SELECT name, email FROM users — gereksiz veri transferini önler.

4. N+1 Sorgu Problemi

Bir listede her eleman için ayrı sorgu atmak yerine JOIN veya IN clause kullan:

// ❌ N+1 sorgu — yavaş
for (int orderId : orderIds) {
    pstmt.setInt(1, orderId);
    rs = pstmt.executeQuery();  // Her sipariş için ayrı sorgu
}

// ✅ Tek sorgu — hızlı
String placeholders = String.join(",", Collections.nCopies(orderIds.size(), "?"));
String sql = "SELECT * FROM orders WHERE id IN (" + placeholders + ")";

5. Exception Yutma

catch (SQLException e) {} gibi boş catch blokları kullanma. En azından logla veya RuntimeException'a çevir.


13. JDBC'den Sonrası — ORM ve JPA

JDBC güçlü ama çok fazla tekrar eden kod (boilerplate) yazarsın. Her sorgu için Connection aç, PreparedStatement hazırla, ResultSet oku, objeye dönüştür... Bunun çözümü ORM (Object-Relational Mapping) araçlarıdır.

JPA (Java Persistence API) ve Hibernate gibi ORM framework'leri, Java nesnelerini doğrudan veritabanı tablolarına eşler. SQL yazmadan CRUD yapabilirsin:

// JPA ile — JDBC boilerplate yok
@Entity
class User {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private String email;
}

// Kaydet
entityManager.persist(new User("Ali", "ali@test.com"));

// Bul
User user = entityManager.find(User.class, 1L);

Ama JPA'yı anlamak için önce JDBC'yi bilmelisin. Çünkü JPA arka planda JDBC kullanır. JDBC'yi bilmeden JPA sorunlarını çözmek çok zor.


Özet

  • JDBC, Java'nın veritabanlarıyla iletişim kurmasını sağlayan standart API'dir. java.sql paketinde yaşar ve veritabanı bağımsızdır — sadece driver ve connection string değişir.

  • PreparedStatement her zaman tercih et. SQL Injection'ı önler, performans sağlar ve tip-güvenlidir. Kullanıcı girdisini asla string birleştirme ile SQL'e koyma.

  • Transaction yönetiminde setAutoCommit(false) ile başla, başarılıysa commit(), hata varsa rollback() yap. Banka havalesi gibi kritik işlemlerde olmazsa olmaz.

  • Connection pooling (HikariCP) üretim ortamında zorunludur. Her işlem için yeni bağlantı açmak pahalıdır; pool bağlantıları yeniden kullanır.

  • try-with-resources ile Connection, Statement ve ResultSet'i her zaman otomatik kapat. Kaynak sızıntısı en yaygın JDBC hatasıdır.

  • DAO pattern ile veritabanı kodunu iş mantığından ayır. JDBC'yi öğrendikten sonra JPA/Hibernate'e geçiş çok daha kolay ve anlamlı olur.