JDBC ile Veritabanı Bağlantısı
Gerçek uygulamalar verilerini kalıcı olarak saklamak ister; işte burada veritabanları devreye girer. JDBC (Java Database Connectivity), Java'nın ilişkisel veritabanlarıyla konuşmak için sunduğu standart API'dir. Sprin…
JDBC ile Veritabanı Bağlantısı
Gerçek uygulamalar verilerini kalıcı olarak saklamak ister; işte burada veritabanları devreye girer. JDBC (Java Database Connectivity), Java'nın ilişkisel veritabanlarıyla konuşmak için sunduğu standart API'dir. Spring Data JPA gibi yüksek seviyeli araçlar bile altta JDBC kullanır; bu yüzden temelini anlamak, üst katmanların "ne yaptığını gerçekten anlamak" demektir. Bu bölümde temelden başlayıp DAO deseni, otomatik anahtarlar, transaction ve ilişkili tablolara kadar gerçek dünyada kullandığın her şeyi göreceğiz.
Örnekler H2 in-memory veritabanını kullanır; kurulum gerekmez, her çalıştırma temiz bir veritabanıyla başlar. Portaldaki "Çalıştır" özelliği H2 sürücüsünü otomatik classpath'e ekler; kendi bilgisayarında
java -cp h2-*.jar Ornek1.javaile çalıştırabilirsin.
JDBC mimarisi
JDBC birkaç temel arayüz etrafında döner:
DriverManager— JDBC URL'ine uygun sürücüyü bulup bağlantı açar.Connection— veritabanıyla açık oturum; işin bitince kapatılmalıdır.Statement/PreparedStatement— SQL komutlarını çalıştırır.ResultSet—SELECTsonucunu satır satır gezdiğin imleç.
JDBC URL hedefi belirtir: jdbc:h2:mem:demo, jdbc:postgresql://localhost/mydb,
jdbc:mysql://.... Çağrı türleri: execute (DDL), executeUpdate (INSERT/UPDATE/DELETE,
etkilenen satır sayısını döndürür), executeQuery (SELECT, ResultSet döndürür).
Örnek 1 (./Ornek1.java) gerçekçi bir müşteri tablosu üzerinde bağlanmayı, tablo oluşturmayı,
parametreli ekleme, filtreli sorgu ve toplama (COUNT/AVG) sorgusunu gösterir. Tüm kaynaklar
try-with-resources ile otomatik kapatılır — veritabanı kodunda en sık hata kaynak sızıntısıdır.
PreparedStatement: güvenli ve parametreli
Kullanıcı verisini doğrudan SQL metnine yapıştırmak ("... WHERE ad = '" + ad + "'") hem
hatalı hem de SQL injection açığına davetiyedir. Doğru yol her zaman PreparedStatement'tır:
değerleri ? yer tutucularıyla, tipli biçimde yerleştirir, böylece hem güvenli hem de verimli
olur:
PreparedStatement ps = con.prepareStatement("INSERT INTO musteri (ad, sehir) VALUES (?, ?)"); ps.setString(1, ad); ps.setString(2, sehir); ps.executeUpdate();
DAO deseni: veri erişimini düzenlemek
Gerçek projelerde SQL'i kodun her yerine serpiştirmeyiz; her varlık için veri erişimini tek bir sınıfta toplarız: DAO (Data Access Object) veya repository. Bu, iş mantığını SQL ayrıntısından ayırır ve test edilebilirliği artırır.
Örnek 2 (./Ornek2.java) bir UrunDAO yazar: ekle, bul, hepsi, fiyatGuncelle, sil —
yani tam bir CRUD. İki önemli teknik içerir:
- Generated keys: Veritabanının ürettiği otomatik
id'yi geri almak:PreparedStatement ps = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.executeUpdate(); ResultSet keys = ps.getGeneratedKeys(); int id = keys.next() ? keys.getInt(1) : -1; - Row mapping: Bir
ResultSetsatırını domain nesnesine (burada birrecord) çevirmek.
Bu desen, Spring Data JPA'nın JpaRepository'sinin elle yazılmış halidir; ileride Spring'in bu
tekrarlayan kodu nasıl ortadan kaldırdığını daha iyi takdir edeceksin.
Transaction, ilişkiler ve batch
Bazı işlemler bölünmez bir bütün olmalıdır. Bir sipariş; başlığı (siparis) ve kalemleri
(siparis_kalem) ile birlikte ya tamamen kaydedilmeli ya da hiç. Ortada bir hata olursa sipariş
yarım kalmamalı. Transaction bu "ya hepsi ya hiçbiri" garantisini verir:
con.setAutoCommit(false); try { // birden çok ilişkili INSERT... con.commit(); // hepsi başarılı -> kalıcı } catch (Exception e) { con.rollback(); // bir hata -> tümünü geri al }
Örnek 3 (./Ornek3.java) bunu uçtan uca gösterir:
- İlişkili tablolar: Bir siparişin başlığı ve kalemleri ayrı tablolarda; kalemler
siparis_idile bağlanır veJOINile birlikte sorgulanır. - Toplu ekleme (batch): Çok sayıda kalemi tek tek değil,
addBatch()+executeBatch()ile toplu göndermek çok daha verimlidir. - Rollback: Geçersiz bir kalem (adet=0) iş kuralını ihlal edince tüm sipariş geri alınır; veritabanında hiçbir iz kalmaz.
Bağlantı havuzu (connection pooling)
Her istekte yeni bir Connection açıp kapatmak pahalıdır. Gerçek uygulamalar bir bağlantı
havuzu (HikariCP gibi) kullanır: hazır bağlantıları havuzda tutar, isteyene ödünç verir, geri
alır. Spring Boot bunu otomatik yapılandırır; sen sadece DataSource'u kullanırsın.
JDBC'den ileriye
JDBC güçlüdür ama tekrarlayan "kazan kodu" (boilerplate) çoktur: aç/kapat, ResultSet'i nesneye
çevir, exception ele al. Spring bu yükü önce JdbcTemplate ile, sonra Spring Data JPA ile
büyük ölçüde ortadan kaldırır — ama hepsinin altında işte bu JDBC vardır.
Özet
JDBC mimarisini ve temel CRUD'u; PreparedStatement ile güvenli sorguları; DAO deseni,
generated keys ve row mapping ile düzenli veri erişimini; transaction, ilişkili
tablolar ve batch ile gerçek dünya senaryolarını gördük. Bağlantı havuzuna değindik. Bu,
Java'nın veriyle buluştuğu sağlam zemindir. Sırada, projelerini ve bağımlılıklarını yöneten
araçlar: Maven ve Gradle.
▶ Kod Örnekleri(3)
Ornek1
ortam gerekir1// Ornek1: JDBC temelleri — gerçekçi bir müşteri tablosu üzerinde bağlan, oluştur, ekle, sorgula.
2// H2 in-memory veritabanı kullanılır (kurulum gerekmez). Portaldaki "Çalıştır" H2'yi
3// otomatik classpath'e ekler; kendi bilgisayarında: java -cp h2-*.jar Ornek1.java
4import java.sql.Connection;
5import java.sql.DriverManager;
6import java.sql.PreparedStatement;
7import java.sql.ResultSet;
8import java.sql.SQLException;
9import java.sql.Statement;
10
11public class Ornek1 {
12
13 static final String URL = "jdbc:h2:mem:magaza;DB_CLOSE_DELAY=-1";
14
15 public static void main(String[] args) throws SQLException {
16 // try-with-resources: Connection blok bitince otomatik kapanır.
17 try (Connection con = DriverManager.getConnection(URL, "sa", "")) {
18 System.out.println("Bağlanıldı: " + con.getMetaData().getDatabaseProductName()
19 + " " + con.getMetaData().getDatabaseProductVersion());
20
21 // DDL: şema oluştur.
22 try (Statement st = con.createStatement()) {
23 st.execute("""
24 CREATE TABLE musteri (
25 id INT AUTO_INCREMENT PRIMARY KEY,
26 ad VARCHAR(60) NOT NULL,
27 sehir VARCHAR(40),
28 bakiye DECIMAL(10,2) DEFAULT 0
29 )
30 """);
31 }
32 System.out.println("Tablo oluşturuldu.\n");
33
34 // PreparedStatement ile parametreli ekleme (güvenli yol).
35 String ekle = "INSERT INTO musteri (ad, sehir, bakiye) VALUES (?, ?, ?)";
36 try (PreparedStatement ps = con.prepareStatement(ekle)) {
37 ekle(ps, "Ada Yılmaz", "İzmir", 1500);
38 ekle(ps, "Burak Demir", "Ankara", 300);
39 ekle(ps, "Ceren Kaya", "İzmir", 2750);
40 ekle(ps, "Deniz Ak", "İstanbul", 50);
41 }
42 System.out.println("4 müşteri eklendi.");
43
44 // Filtreli sorgu: bakiyesi 1000'den fazla olanlar.
45 System.out.println("\nBakiyesi 1000+ olan müşteriler:");
46 String sorgu = "SELECT id, ad, sehir, bakiye FROM musteri WHERE bakiye > ? ORDER BY bakiye DESC";
47 try (PreparedStatement ps = con.prepareStatement(sorgu)) {
48 ps.setDouble(1, 1000);
49 try (ResultSet rs = ps.executeQuery()) {
50 while (rs.next()) {
51 System.out.printf(" #%d %-12s %-10s %,.2f TL%n",
52 rs.getInt("id"), rs.getString("ad"), rs.getString("sehir"), rs.getDouble("bakiye"));
53 }
54 }
55 }
56
57 // Toplama (aggregate) sorgusu.
58 try (Statement st = con.createStatement();
59 ResultSet rs = st.executeQuery("SELECT COUNT(*) adet, AVG(bakiye) ort FROM musteri")) {
60 if (rs.next()) {
61 System.out.printf("%nToplam %d müşteri, ortalama bakiye %,.2f TL%n",
62 rs.getInt("adet"), rs.getDouble("ort"));
63 }
64 }
65 }
66 }
67
68 static void ekle(PreparedStatement ps, String ad, String sehir, double bakiye) throws SQLException {
69 ps.setString(1, ad);
70 ps.setString(2, sehir);
71 ps.setDouble(3, bakiye);
72 ps.executeUpdate();
73 }
74}Bu örnek canlı veritabanı bağlantısı (JDBC sürücüsü) gerektirir; tek başına java Ornek1.java ile çalışmaz. Çalışma mantığı yukarıdaki anlatım ve kodda açıklanmıştır.
Ornek2
ortam gerekir1// Ornek2: DAO (Data Access Object) deseni — veri erişimini temiz bir sınıfta toplamak.
2// Spring Data JPA'nın Repository'lerinin elle yazılmış hali. Tam bir CRUD + generated keys.
3// Çalıştırma: java -cp h2-*.jar Ornek2.java (portalda otomatik)
4import java.sql.Connection;
5import java.sql.DriverManager;
6import java.sql.PreparedStatement;
7import java.sql.ResultSet;
8import java.sql.SQLException;
9import java.sql.Statement;
10import java.util.ArrayList;
11import java.util.List;
12import java.util.Optional;
13
14public class Ornek2 {
15
16 // Domain nesnesi (immutable record).
17 record Urun(Integer id, String ad, double fiyat, int stok) {}
18
19 public static void main(String[] args) throws SQLException {
20 try (Connection con = DriverManager.getConnection("jdbc:h2:mem:dao;DB_CLOSE_DELAY=-1", "sa", "")) {
21 UrunDAO dao = new UrunDAO(con);
22 dao.tabloOlustur();
23
24 // CREATE — ekleme generated key (otomatik id) döndürür.
25 int id1 = dao.ekle(new Urun(null, "Klavye", 450, 20));
26 int id2 = dao.ekle(new Urun(null, "Mouse", 250, 35));
27 dao.ekle(new Urun(null, "Monitör", 3200, 8));
28 System.out.println("Eklendi. İlk iki id: " + id1 + ", " + id2);
29
30 // READ
31 System.out.println("\nTüm ürünler:");
32 dao.hepsi().forEach(u -> System.out.println(" " + u));
33
34 System.out.println("\nid=" + id2 + " ürün: " + dao.bul(id2).orElse(null));
35 System.out.println("id=999 ürün : " + dao.bul(999).map(Object::toString).orElse("(bulunamadı)"));
36
37 // UPDATE — fiyat güncelle
38 dao.fiyatGuncelle(id1, 399);
39 System.out.println("\nGüncel Klavye: " + dao.bul(id1).orElse(null));
40
41 // DELETE
42 dao.sil(id2);
43 System.out.println("\nSilme sonrası ürün sayısı: " + dao.hepsi().size());
44 }
45 }
46
47 // DAO: belirli bir nesne için tüm SQL erişimini tek yerde toplar.
48 static class UrunDAO {
49 private final Connection con;
50 UrunDAO(Connection con) { this.con = con; }
51
52 void tabloOlustur() throws SQLException {
53 try (Statement st = con.createStatement()) {
54 st.execute("CREATE TABLE urun (id INT AUTO_INCREMENT PRIMARY KEY, ad VARCHAR(80), fiyat DECIMAL(10,2), stok INT)");
55 }
56 }
57
58 // RETURN_GENERATED_KEYS: veritabanının ürettiği id'yi geri alırız.
59 int ekle(Urun u) throws SQLException {
60 String sql = "INSERT INTO urun (ad, fiyat, stok) VALUES (?, ?, ?)";
61 try (PreparedStatement ps = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
62 ps.setString(1, u.ad());
63 ps.setDouble(2, u.fiyat());
64 ps.setInt(3, u.stok());
65 ps.executeUpdate();
66 try (ResultSet keys = ps.getGeneratedKeys()) {
67 keys.next();
68 return keys.getInt(1);
69 }
70 }
71 }
72
73 Optional<Urun> bul(int id) throws SQLException {
74 try (PreparedStatement ps = con.prepareStatement("SELECT * FROM urun WHERE id = ?")) {
75 ps.setInt(1, id);
76 try (ResultSet rs = ps.executeQuery()) {
77 return rs.next() ? Optional.of(map(rs)) : Optional.empty();
78 }
79 }
80 }
81
82 List<Urun> hepsi() throws SQLException {
83 List<Urun> liste = new ArrayList<>();
84 try (Statement st = con.createStatement();
85 ResultSet rs = st.executeQuery("SELECT * FROM urun ORDER BY id")) {
86 while (rs.next()) liste.add(map(rs));
87 }
88 return liste;
89 }
90
91 void fiyatGuncelle(int id, double yeniFiyat) throws SQLException {
92 try (PreparedStatement ps = con.prepareStatement("UPDATE urun SET fiyat = ? WHERE id = ?")) {
93 ps.setDouble(1, yeniFiyat);
94 ps.setInt(2, id);
95 ps.executeUpdate();
96 }
97 }
98
99 void sil(int id) throws SQLException {
100 try (PreparedStatement ps = con.prepareStatement("DELETE FROM urun WHERE id = ?")) {
101 ps.setInt(1, id);
102 ps.executeUpdate();
103 }
104 }
105
106 // ResultSet satırını domain nesnesine çevirir (row mapping).
107 private Urun map(ResultSet rs) throws SQLException {
108 return new Urun(rs.getInt("id"), rs.getString("ad"), rs.getDouble("fiyat"), rs.getInt("stok"));
109 }
110 }
111}Bu örnek canlı veritabanı bağlantısı (JDBC sürücüsü) gerektirir; tek başına java Ornek2.java ile çalışmaz. Çalışma mantığı yukarıdaki anlatım ve kodda açıklanmıştır.
Ornek3
ortam gerekir1// Ornek3: Transaction + ilişkili tablolar + toplu ekleme (batch).
2// Gerçek senaryo: bir sipariş ve kalemleri TEK bir transaction'da kaydedilir;
3// bir hata olursa HER ŞEY geri alınır (sipariş yarım kalmaz).
4// Çalıştırma: java -cp h2-*.jar Ornek3.java (portalda otomatik)
5import java.sql.Connection;
6import java.sql.DriverManager;
7import java.sql.PreparedStatement;
8import java.sql.ResultSet;
9import java.sql.SQLException;
10import java.sql.Statement;
11import java.util.List;
12
13public class Ornek3 {
14
15 record Kalem(String urun, int adet, double birimFiyat) {}
16
17 static final String URL = "jdbc:h2:mem:siparis;DB_CLOSE_DELAY=-1";
18
19 public static void main(String[] args) throws SQLException {
20 try (Connection con = DriverManager.getConnection(URL, "sa", "")) {
21 semaOlustur(con);
22
23 // 1) Başarılı sipariş: sipariş başlığı + kalemler tek transaction'da.
24 int siparisId = siparisOlustur(con, "Ada", List.of(
25 new Kalem("Klavye", 2, 450),
26 new Kalem("Mouse", 1, 250),
27 new Kalem("Mousepad", 3, 80)
28 ));
29 System.out.println("Sipariş #" + siparisId + " başarıyla oluşturuldu (commit).");
30 siparisYazdir(con, siparisId);
31
32 // 2) Hatalı sipariş: bir kalemde geçersiz adet -> tüm sipariş geri alınır (rollback).
33 System.out.println("\n--- Geçersiz sipariş deneniyor (adet=0) ---");
34 try {
35 siparisOlustur(con, "Burak", List.of(
36 new Kalem("Monitör", 1, 3200),
37 new Kalem("Kablo", 0, 50) // geçersiz: adet 0
38 ));
39 } catch (RuntimeException e) {
40 System.out.println("Hata: " + e.getMessage() + " -> rollback yapıldı.");
41 }
42
43 // Rollback çalıştıysa Burak'ın siparişi HİÇ oluşmamış olmalı.
44 try (Statement st = con.createStatement();
45 ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM siparis")) {
46 rs.next();
47 System.out.println("Toplam sipariş sayısı: " + rs.getInt(1) + " (Burak'ınki kaydedilmedi)");
48 }
49 }
50 }
51
52 static void semaOlustur(Connection con) throws SQLException {
53 try (Statement st = con.createStatement()) {
54 st.execute("CREATE TABLE siparis (id INT AUTO_INCREMENT PRIMARY KEY, musteri VARCHAR(40), toplam DECIMAL(10,2))");
55 st.execute("""
56 CREATE TABLE siparis_kalem (
57 id INT AUTO_INCREMENT PRIMARY KEY,
58 siparis_id INT,
59 urun VARCHAR(60),
60 adet INT,
61 tutar DECIMAL(10,2)
62 )
63 """);
64 }
65 }
66
67 // Sipariş + kalemleri TEK transaction'da kaydeder; hata olursa tümünü geri alır.
68 static int siparisOlustur(Connection con, String musteri, List<Kalem> kalemler) throws SQLException {
69 con.setAutoCommit(false); // transaction başlat
70 try {
71 // İş kuralı: adet pozitif olmalı. Değilse transaction'ı iptal et.
72 for (Kalem k : kalemler) {
73 if (k.adet() <= 0) throw new RuntimeException("geçersiz adet: " + k.urun());
74 }
75
76 double toplam = kalemler.stream().mapToDouble(k -> k.adet() * k.birimFiyat()).sum();
77
78 // Sipariş başlığını ekle, üretilen id'yi al.
79 int siparisId;
80 try (PreparedStatement ps = con.prepareStatement(
81 "INSERT INTO siparis (musteri, toplam) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS)) {
82 ps.setString(1, musteri);
83 ps.setDouble(2, toplam);
84 ps.executeUpdate();
85 try (ResultSet keys = ps.getGeneratedKeys()) {
86 keys.next();
87 siparisId = keys.getInt(1);
88 }
89 }
90
91 // Kalemleri TOPLU (batch) ekle — tek seferde, daha verimli.
92 try (PreparedStatement ps = con.prepareStatement(
93 "INSERT INTO siparis_kalem (siparis_id, urun, adet, tutar) VALUES (?, ?, ?, ?)")) {
94 for (Kalem k : kalemler) {
95 ps.setInt(1, siparisId);
96 ps.setString(2, k.urun());
97 ps.setInt(3, k.adet());
98 ps.setDouble(4, k.adet() * k.birimFiyat());
99 ps.addBatch(); // sıraya ekle
100 }
101 ps.executeBatch(); // hepsini tek seferde gönder
102 }
103
104 con.commit(); // her şey başarılı -> kalıcı yap
105 return siparisId;
106 } catch (RuntimeException | SQLException e) {
107 con.rollback(); // herhangi bir hata -> hiçbir şey kaydedilmesin
108 throw new RuntimeException(e.getMessage());
109 } finally {
110 con.setAutoCommit(true);
111 }
112 }
113
114 // JOIN ile siparişi ve kalemlerini birlikte yazdırır.
115 static void siparisYazdir(Connection con, int siparisId) throws SQLException {
116 String sql = """
117 SELECT s.musteri, s.toplam, k.urun, k.adet, k.tutar
118 FROM siparis s JOIN siparis_kalem k ON k.siparis_id = s.id
119 WHERE s.id = ?
120 """;
121 try (PreparedStatement ps = con.prepareStatement(sql)) {
122 ps.setInt(1, siparisId);
123 try (ResultSet rs = ps.executeQuery()) {
124 boolean baslik = false;
125 while (rs.next()) {
126 if (!baslik) {
127 System.out.printf(" Müşteri: %s | Sipariş toplamı: %,.2f TL%n",
128 rs.getString("musteri"), rs.getDouble("toplam"));
129 baslik = true;
130 }
131 System.out.printf(" - %-10s x%d = %,.2f TL%n",
132 rs.getString("urun"), rs.getInt("adet"), rs.getDouble("tutar"));
133 }
134 }
135 }
136 }
137}Bu örnek canlı veritabanı bağlantısı (JDBC sürücüsü) gerektirir; tek başına java Ornek3.java ile çalışmaz. Çalışma mantığı yukarıdaki anlatım ve kodda açıklanmıştır.