İçeriğe geç
Educora
İleri23 dk16 / 18

JDBC ve veritabanları

Bir Java programını JDBC ile veritabanına bağla: `PreparedStatement`, SQL enjeksiyonundan korunma, işlemler (transaction), bağlantı havuzları ve JPA/Hibernate'e giriş.

Kendini test et
Bu derste öğreneceklerin
  • JDBC ile veritabanına bağlanmak, sorgu göndermek ve ResultSet okumak
  • PreparedStatement ile SQL enjeksiyonunu önlemek
  • İşlemleri yönetmek, bağlantı havuzunun ve JPA'nın rolünü açıklamak

Bir program kapandığında bellekteki bütün nesneler yok olur. Oysa öğrenci notları, siparişler ve kullanıcı hesapları yıllarca saklanmalıdır; bunlar bir veritabanında tutulur. SQL kursunda sorguları elle yazıyordun; şimdi Java programının kendisi veritabanına bağlanacak, SQL gönderecek ve sonucu nesnelere dönüştürecek. Bunun temeli JDBC'dir.

Tanım
JDBC

Java Database Connectivity: java.sql paketindeki standart API. Kod Connection, PreparedStatement ve ResultSet arayüzleriyle yazılır; bunları belirli bir veritabanının (PostgreSQL, MySQL, SQLite, H2) sürücüsü gerçekleştirir. Veritabanını değiştirdiğinde genellikle yalnızca sürücü ve bağlantı dizesi değişir.

Bağlantı ve ilk sorgu

Sürücü projeye sıradan bir bağımlılık gibi eklenir (Maven'da genellikle runtime kapsamıyla). DriverManager.getConnection(url, user, password) bir bağlantı açar; URL, veritabanının türünü ve adresini belirtir. Statement'ın üç ana metodu vardır: execute herhangi bir SQL'i çalıştırır, executeUpdate değişen satır sayısını döndürür, executeQuery ise bir sonuç tablosu, yani ResultSet döndürür. rs.next() imleci sonraki satıra taşır ve satırlar bitince false döndürür. Tüm kaynakları try-with-resources ile kapatmak önemlidir.

VeritabanıÖrnek URLSürücü (Maven)
PostgreSQLjdbc:postgresql://localhost:5432/schoolorg.postgresql:postgresql
MySQLjdbc:mysql://localhost:3306/schoolcom.mysql:mysql-connector-j
SQLitejdbc:sqlite:school.dborg.xerial:sqlite-jdbc
H2 (bellekte)jdbc:h2:mem:schoolcom.h2database:h2
Java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class Main {
    public static void main(String[] args) throws SQLException {
        String url = "jdbc:h2:mem:school";   // the H2 driver must be on the classpath
        try (Connection conn = DriverManager.getConnection(url, "sa", "");
             Statement st = conn.createStatement()) {
            st.execute("CREATE TABLE students (id INT AUTO_INCREMENT PRIMARY KEY, "
                    + "first_name VARCHAR(50), city VARCHAR(50), score INT)");
            int rows = st.executeUpdate("INSERT INTO students (first_name, city, score) VALUES "
                    + "('Aysel', 'Baku', 95), ('Murad', 'Ganja', 78), ('Leyla', 'Baku', 88)");
            System.out.println("Inserted rows: " + rows);

            try (ResultSet rs = st.executeQuery("SELECT first_name, score FROM students ORDER BY score DESC")) {
                while (rs.next()) {
                    System.out.println(rs.getString("first_name") + " " + rs.getInt("score"));
                }
            }
        }
    }
}
Beklenen çıktı
Inserted rows: 3
Aysel 95
Leyla 88
Murad 78
H2 sürücüsüyle örnek çıktı (sürücü yoksa program No suitable driver found hatası verir)

PreparedStatement ve SQL enjeksiyonu

Kullanıcıdan gelen bir değeri SQL dizesine yapıştırmak en tehlikeli hatalardan biridir. Biri giriş alanına ' OR '1'='1 yazarsa koşul her zaman doğru olur ve sorgu bütün kullanıcıları döndürür; buna SQL enjeksiyonu denir. PreparedStatement ise SQL'i ? yer tutucularıyla önceden hazırlar ve değerleri ayrı gönderir: veritabanı onları asla komut olarak değil, her zaman veri olarak görür. Üstelik aynı hazırlanmış sorgu tekrar kullanıldığında daha hızlı olabilir.

Tehlikeli: dize birleştirme
String sql = "SELECT * FROM users WHERE login = '" + login + "'";
ResultSet rs = st.executeQuery(sql);
// login = ' OR '1'='1   ->   WHERE login = '' OR '1'='1'   ->   every user!
Güvenli: `?` parametreleri
String sql = "SELECT * FROM users WHERE login = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, login);   // sent separately, always treated as data
ResultSet rs = ps.executeQuery();
Java
static List<String> findByCity(Connection conn, String city, int minScore) throws SQLException {
    String sql = "SELECT first_name FROM students WHERE city = ? AND score >= ? ORDER BY first_name";
    List<String> names = new ArrayList<>();
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
        ps.setString(1, city);        // parameters are numbered from 1
        ps.setInt(2, minScore);
        try (ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                names.add(rs.getString("first_name"));
            }
        }
    }
    return names;
}
// findByCity(conn, "Baku", 80)  ->  [Aysel, Leyla]
Tipik bir okuma metodu: parametreler, ResultSet ve sonucu listeye toplama

Gerçek bir programda her satır genellikle bir nesneye dönüştürülür: new Student(rs.getLong("id"), rs.getString("first_name"), rs.getInt("score")). Bir tablonun tüm okuma ve yazma metotları tek bir sınıfta toplanır; buna DAO (Data Access Object) ya da depo (repository) denir. Böylece SQL tek bir yerde kalır, programın geri kalanı yalnızca Student nesneleriyle çalışır ve arkada hangi veritabanının olduğunu bilmez.

İşlemler (transaction)

Aysel, Murad'ın hesabına 50 manat gönderiyor: para bir hesaptan çıkmalı, diğerine girmelidir. Sunucu ilk UPDATE'ten sonra kapanırsa para “kaybolur”. Bir transaction (işlem) birkaç işlemi bölünmez tek bir birime dönüştürür: ya hepsi kaydedilir (commit) ya da hiçbiri (rollback). JDBC'de bağlantı varsayılan olarak auto-commit kipindedir; her sorgu hemen kaydedilir. setAutoCommit(false) bu kipi kapatır ve işlemi kendin yönetmeni sağlar.

Java
static void transfer(Connection conn, int fromId, int toId, int amount) throws SQLException {
    String sql = "UPDATE accounts SET balance = balance + ? WHERE id = ?";
    conn.setAutoCommit(false);                    // start a transaction
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
        ps.setInt(1, -amount);
        ps.setInt(2, fromId);
        ps.executeUpdate();
        ps.setInt(1, amount);
        ps.setInt(2, toId);
        ps.executeUpdate();
        conn.commit();                            // both changes are saved together...
    } catch (SQLException e) {
        conn.rollback();                          // ...or neither of them
        throw e;
    } finally {
        conn.setAutoCommit(true);
    }
}
İki UPDATE ya birlikte başarılı olur ya da birlikte geri alınır

Bağlantı havuzu

Yeni bir bağlantı açmak pahalıdır: ağ, parola denetimi, veritabanı tarafında bellek. Bunu her istek için yapan bir web uygulaması yük altında çöker. Bir bağlantı havuzu (connection pool) önceden birkaç bağlantı açar ve onları “ödünç verir”: getConnection() hazır bir bağlantıyı alır, close() ise onu kapatmaz, havuza geri verir. Java'daki en popüler havuz HikariCP'dir; Spring Boot onu varsayılan olarak kullanır.

Java
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/school");
config.setUsername("school_app");
config.setPassword(System.getenv("DB_PASSWORD"));
config.setMaximumPoolSize(10);

try (HikariDataSource pool = new HikariDataSource(config)) {    // create once per application
    try (Connection conn = pool.getConnection()) {              // borrowed from the pool
        System.out.println(findByCity(conn, "Baku", 80));
    }                                                           // close() returns it to the pool
}
HikariCP (com.zaxxer:HikariCP); parolayı koda yazma, bir ortam değişkeninden oku

JPA ve Hibernate: SQL'siz nesneler

JDBC'de her sütunu elle bir nesneye aktarmak gerekir. ORM (Object-Relational Mapping) bunu otomatikleştirir: sınıf tabloya, alan sütuna, nesne satıra eşlenir. Java'da ORM standardı JPA'dır (Jakarta Persistence, jakarta.persistence paketi); en popüler gerçekleştirimi ise Hibernate'tir. Sınıfı @Entity ile işaretlersin; @Id birincil anahtarı, @Column ise sütun adını ve kısıtlamaları belirtir.

Java
import jakarta.persistence.*;

@Entity
@Table(name = "students")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "first_name", nullable = false)
    private String firstName;
    private String city;
    private int score;

    protected Student() {}                        // JPA needs a no-argument constructor

    public Student(String firstName, String city, int score) {
        this.firstName = firstName;
        this.city = city;
        this.score = score;
    }

    public String getFirstName() { return firstName; }
    public int getScore() { return score; }
}
Java sınıfı olarak tanımlanmış bir tablo
Java
EntityManager em = emf.createEntityManager();

em.getTransaction().begin();
em.persist(new Student("Nigar", "Sumgait", 91));        // Hibernate generates the INSERT
em.getTransaction().commit();

Student first = em.find(Student.class, 1L);             // SELECT ... WHERE id = 1

List<Student> best = em.createQuery(
        "SELECT s FROM Student s WHERE s.score >= :min ORDER BY s.score DESC", Student.class)
    .setParameter("min", 90)
    .getResultList();
EntityManager SQL'i kendisi üretir; sorgular JPQL ile sınıf ve alan adları kullanılarak yazılır

JPQL'de students tablosu yerine Student sınıfı, first_name sütunu yerine firstName alanı yazılır. Bir sonraki derste Spring Data JPA'nın bu kodu da kısalttığını göreceksin: interface StudentRepository extends JpaRepository<Student, Long> yazmak yeterli, save, findById ve findAll metotlarını Spring kendisi oluşturur. Yine de ORM'in altında JDBC çalışır; bu yüzden JDBC'yi bilmek sorunları anlamana yardım eder.

Önemli noktalar

  • JDBC: Connection → PreparedStatement → ResultSet; sürücü URL'ye göre seçilir, kaynaklar try-with-resources ile kapatılır.
  • Kullanıcı girdisini asla SQL'e yapıştırma; 1'den numaralanan ? parametrelerini kullan.
  • executeQuery bir ResultSet, executeUpdate ise değişen satır sayısını döndürür; rs.next() imleci ilerletir.
  • İşlem: setAutoCommit(false) → commit() ya da rollback(); ya hepsi ya hiçbiri.
  • Bağlantı havuzu (HikariCP) bağlantıları yeniden kullanır; JPA/Hibernate, @Entity ve @Id ile sınıfları tablolara eşler.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
executeUpdate ne döndürür?