- JDBC ile veritabanına bağlanmak, sorgu göndermek ve
ResultSetokumak PreparedStatementile 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.
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 URL | Sürücü (Maven) |
|---|---|---|
| PostgreSQL | jdbc:postgresql://localhost:5432/school | org.postgresql:postgresql |
| MySQL | jdbc:mysql://localhost:3306/school | com.mysql:mysql-connector-j |
| SQLite | jdbc:sqlite:school.db | org.xerial:sqlite-jdbc |
| H2 (bellekte) | jdbc:h2:mem:school | com.h2database:h2 |
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"));
}
}
}
}
}Inserted rows: 3 Aysel 95 Leyla 88 Murad 78
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.
String sql = "SELECT * FROM users WHERE login = '" + login + "'";
ResultSet rs = st.executeQuery(sql);
// login = ' OR '1'='1 -> WHERE login = '' OR '1'='1' -> every user!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();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]ResultSet ve sonucu listeye toplamaGerç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.
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);
}
}UPDATE ya birlikte başarılı olur ya da birlikte geri alınırBağ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.
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
}com.zaxxer:HikariCP); parolayı koda yazma, bir ortam değişkeninden okuJPA 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.
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; }
}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ırJPQL'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. executeQuerybirResultSet,executeUpdateise değişen satır sayısını döndürür;rs.next()imleci ilerletir.- İşlem:
setAutoCommit(false)→commit()ya darollback(); ya hepsi ya hiçbiri. - Bağlantı havuzu (HikariCP) bağlantıları yeniden kullanır; JPA/Hibernate,
@Entityve@Idile sınıfları tablolara eşler.
Kendini test et
10 soru. Her doğru cevap XP kazandırır.
executeUpdate ne döndürür?