Eğitim Portalı/Spring/Transaction Yönetimi
Spring02-spring/07-transaction-yonetimi

Transaction Yönetimi

Bir veritabanı işlemi çoğu zaman tek bir adım değildir: para transferinde birinden düşülür, diğerine eklenir; sipariş oluşturulurken başlık ve kalemler birlikte yazılır. Bu adımların hepsi başarılı olmalı ya da hiçbir…

Transaction Yönetimi

Bir veritabanı işlemi çoğu zaman tek bir adım değildir: para transferinde birinden düşülür, diğerine eklenir; sipariş oluşturulurken başlık ve kalemler birlikte yazılır. Bu adımların hepsi başarılı olmalı ya da hiçbiri uygulanmamalı — aksi halde veri tutarsız kalır (para buharlaşır, sipariş yarım kalır). İşte transaction bu "ya hepsi ya hiçbiri" (atomiklik) garantisini verir. Spring, transaction yönetimini hem çok temiz (bildirimsel) hem de gerektiğinde ince ayarlı (programatik) hâle getirir.

Önce sorunu görelim

Örnek 1 (./Ornek1.java) transaction olmadan bir para transferi yapar: Ada'dan 300 TL düşülür, ama araya bir hata girince Burak'a eklenmeden işlem yarıda kalır. Sonuç: 300 TL kaybolur, toplam bakiye tutarsızlaşır.

jdbc.update("UPDATE hesap SET bakiye = bakiye - 300 WHERE ad='Ada'"); // başarılı
throw new RuntimeException("ağ hatası");                              // araya hata
jdbc.update("UPDATE hesap SET bakiye = bakiye + 300 WHERE ad='Burak'"); // hiç çalışmadı

İki güncelleme bölünmez bir bütün olmalıydı. Çözüm: onları bir transaction'a sarmak.

ACID kısaca

Transaction'lar ACID garantileri sağlar:

  • Atomicity (atomiklik): Ya hepsi ya hiçbiri.
  • Consistency (tutarlılık): Kurallar (kısıtlar) her zaman korunur.
  • Isolation (yalıtım): Eşzamanlı transaction'lar birbirini bozmaz.
  • Durability (kalıcılık): Commit edilen değişiklik kalıcıdır.

Bildirimsel transaction: @Transactional

Spring'in en yaygın yolu, bir metoda @Transactional koymaktır. Metot bir transaction'la sarılır: normal biterse commit, bir RuntimeException fırlarsa rollback olur. İş kodun transaction yönetimini hiç görmez:

@Transactional
public void transfer(String kimden, String kime, int tutar) {
    jdbc.update("... - tutar ... kimden");
    jdbc.update("... + tutar ... kime");
}   // metot biterse commit; hata fırlarsa hepsi geri alınır

Bunun için iki şey gerekir: @EnableTransactionManagement ve bir PlatformTransactionManager bean'i (JDBC için DataSourceTransactionManager). Örnek 2 (./Ornek2.java) aynı transferi @Transactional ile yapar: hata enjekte edilen transfer geri alınır (bakiyeler değişmez), başarılı transfer commit edilir.

@Transactional proxy tabanlıdır (AOP). Bu iki önemli sonucu doğurur:

  1. Self-invocation tuzağı: Aynı sınıfın bir metodu, kendi içindeki başka bir @Transactional metodu doğrudan çağırırsa proxy devreye girmez; transaction başlamaz.
  2. Rollback kuralı: Varsayılan olarak yalnızca unchecked (RuntimeException) hatalarda rollback olur. Checked exception'larda rollback istiyorsan @Transactional(rollbackFor = Exception.class) belirtmelisin.

Programatik transaction: TransactionTemplate

Bazen transaction sınırını koddan, daha ince kontrol ederek yönetmek istersin. TransactionTemplate bunu sağlar: bir kod bloğunu transaction içinde çalıştırır; içeride status.setRollbackOnly() ile elle geri alabilir veya exception fırlatabilirsin:

txTemplate.execute(status -> {
    jdbc.update("...");
    if (kuralIhlali) status.setRollbackOnly(); // commit edilmez
    return null;
});

Örnek 3 (./Ornek3.java) üç senaryoyu gösterir: başarılı commit, iş kuralı ihlalinde setRollbackOnly() ile geri alma ve exception ile otomatik rollback.

İleri kavramlar

Transaction'ların davranışını ayarlayan parametreler:

  • Propagation (yayılım): İç içe çağrılan @Transactional metotların davranışı. REQUIRED (varsayılan — varsa mevcut transaction'a katıl), REQUIRES_NEW (her zaman yeni transaction), NESTED, SUPPORTS vb.
  • Isolation (yalıtım düzeyi): Eşzamanlı transaction'ların birbirini ne kadar gördüğü (READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE...). Dirty/non-repeatable/phantom read sorunlarını dengeler.
  • readOnly: Salt-okunur transaction'lar için optimizasyon ipucu.
  • rollbackFor / noRollbackFor: Hangi istisnalarda rollback olacağını özelleştirir.
  • timeout: Transaction için zaman aşımı.

Özet

Transaction olmadan oluşan veri tutarsızlığını (Örnek 1), @Transactional ile bildirimsel "ya hep ya hiç" garantisini (Örnek 2) ve TransactionTemplate ile programatik kontrolü (Örnek 3) gördük; proxy temelli davranışı (self-invocation, rollback kuralı) ve propagation/isolation gibi ileri kavramları öğrendik. Sırada, kullanıcıdan ve dış dünyadan gelen veriyi güvenle karşılamanın yolu: doğrulama (validation).

Kod Örnekleri(4)

Ornek1

ortam gerekir
Ornek1.java
1// Ornek1: PROBLEM — transaction olmadan, çok adımlı bir işlem yarıda kalırsa veri TUTARSIZ olur.
2// Para transferi: borçlandırma başarılı, alacaklandırma başarısız -> para "buharlaşır".
3// Çalıştırma: portal Spring + H2 classpath'iyle çalıştırır.
4import org.springframework.jdbc.core.JdbcTemplate;
5import org.springframework.jdbc.datasource.DriverManagerDataSource;
6
7public class Ornek1 {
8
9    public static void main(String[] args) {
10        var ds = new DriverManagerDataSource("jdbc:h2:mem:notx;DB_CLOSE_DELAY=-1", "sa", "");
11        ds.setDriverClassName("org.h2.Driver");
12        JdbcTemplate jdbc = new JdbcTemplate(ds);
13
14        jdbc.execute("CREATE TABLE hesap (ad VARCHAR(20) PRIMARY KEY, bakiye INT)");
15        jdbc.update("INSERT INTO hesap VALUES ('Ada', 1000), ('Burak', 500)");
16        System.out.println("Başlangıç: " + bakiyeler(jdbc));
17
18        // Transaction YOK: iki güncelleme birbirinden bağımsız çalışır.
19        try {
20            jdbc.update("UPDATE hesap SET bakiye = bakiye - 300 WHERE ad = 'Ada'");   // adım 1 (başarılı)
21            if (true) throw new RuntimeException("ağ hatası! (alacaklandırmadan önce)"); // araya hata
22            jdbc.update("UPDATE hesap SET bakiye = bakiye + 300 WHERE ad = 'Burak'");  // adım 2 (hiç çalışmadı)
23        } catch (RuntimeException e) {
24            System.out.println("Hata: " + e.getMessage());
25        }
26
27        System.out.println("Sonra    : " + bakiyeler(jdbc));
28        System.out.println("""
29
30                --- SORUN ---
31                Ada'dan 300 düştü ama Burak'a eklenmedi -> 300 TL KAYBOLDU; veri tutarsız.
32                İki güncelleme 'ya hepsi ya hiçbiri' olmalıydı. Bunu transaction sağlar (Örnek 2).""");
33    }
34
35    static String bakiyeler(JdbcTemplate jdbc) {
36        Integer ada = jdbc.queryForObject("SELECT bakiye FROM hesap WHERE ad='Ada'", Integer.class);
37        Integer burak = jdbc.queryForObject("SELECT bakiye FROM hesap WHERE ad='Burak'", Integer.class);
38        return "Ada=" + ada + ", Burak=" + burak + " (toplam " + (ada + burak) + ")";
39    }
40}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek1.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.

Ornek2

ortam gerekir
Ornek2.java
1// Ornek2: ÇÖZÜM — @Transactional (deklaratif). İşlem "ya hepsi ya hiçbiri" olur.
2// @Transactional proxy tabanlıdır; bu yüzden portal bu dosyayı derleyip çalıştırır.
3package com.egitim.spring.tx;
4
5import org.springframework.context.annotation.AnnotationConfigApplicationContext;
6import org.springframework.context.annotation.Bean;
7import org.springframework.context.annotation.Configuration;
8import org.springframework.jdbc.core.JdbcTemplate;
9import org.springframework.jdbc.datasource.DataSourceTransactionManager;
10import org.springframework.jdbc.datasource.DriverManagerDataSource;
11import org.springframework.transaction.PlatformTransactionManager;
12import org.springframework.transaction.annotation.EnableTransactionManagement;
13import org.springframework.transaction.annotation.Transactional;
14
15import javax.sql.DataSource;
16
17public class Ornek2 {
18
19    public static void main(String[] args) {
20        var ctx = new AnnotationConfigApplicationContext(TxConfig.class);
21        BankaServisi banka = ctx.getBean(BankaServisi.class);
22
23        System.out.println("Başlangıç: " + banka.bakiyeler());
24
25        // Hata enjekte edilen transfer: @Transactional sayesinde TÜMÜ geri alınır (rollback).
26        System.out.println("\n300 TL transfer (araya hata) deneniyor...");
27        try {
28            banka.transfer("Ada", "Burak", 300, true); // hata fırlatır
29        } catch (RuntimeException e) {
30            System.out.println("Hata: " + e.getMessage() + " -> transaction geri alındı.");
31        }
32        System.out.println("Sonra    : " + banka.bakiyeler() + "  <- DEĞİŞMEDİ (tutarlı)");
33
34        // Başarılı transfer: commit.
35        System.out.println("\n200 TL transfer (başarılı)...");
36        banka.transfer("Ada", "Burak", 200, false);
37        System.out.println("Sonra    : " + banka.bakiyeler());
38
39        ctx.close();
40        System.out.println("""
41
42                --- @Transactional ---
43                Metot bir transaction'la SARILIR: normal biterse COMMIT, RuntimeException fırlarsa ROLLBACK.
44                Dikkat: varsayılan olarak yalnızca unchecked (RuntimeException) hatalarda rollback olur;
45                checked exception'larda rollback için @Transactional(rollbackFor = ...) gerekir.
46                Ayrıca proxy tabanlı olduğu için 'self-invocation' (aynı sınıf içi çağrı) transaction'ı tetiklemez.""");
47    }
48
49    @Configuration
50    @EnableTransactionManagement // @Transactional'ı etkinleştirir (proxy auto-creator)
51    public static class TxConfig {
52        @Bean DataSource dataSource() {
53            var ds = new DriverManagerDataSource("jdbc:h2:mem:tx;DB_CLOSE_DELAY=-1", "sa", "");
54            ds.setDriverClassName("org.h2.Driver");
55            return ds;
56        }
57        @Bean JdbcTemplate jdbcTemplate(DataSource ds) { return new JdbcTemplate(ds); }
58        // Transaction yöneticisi: commit/rollback'i bu yönetir.
59        @Bean PlatformTransactionManager txManager(DataSource ds) { return new DataSourceTransactionManager(ds); }
60        @Bean BankaServisi bankaServisi(JdbcTemplate jdbc) { return new BankaServisi(jdbc); }
61    }
62
63    public static class BankaServisi {
64        private final JdbcTemplate jdbc;
65        BankaServisi(JdbcTemplate jdbc) {
66            this.jdbc = jdbc;
67            jdbc.execute("CREATE TABLE hesap (ad VARCHAR(20) PRIMARY KEY, bakiye INT)");
68            jdbc.update("INSERT INTO hesap VALUES ('Ada', 1000), ('Burak', 500)");
69        }
70
71        // Tüm metot tek bir transaction; içindeki güncellemeler 'ya hep ya hiç'.
72        @Transactional
73        public void transfer(String kimden, String kime, int tutar, boolean hataYarat) {
74            jdbc.update("UPDATE hesap SET bakiye = bakiye - ? WHERE ad = ?", tutar, kimden);
75            if (hataYarat) throw new RuntimeException("ağ hatası! (transfer ortasında)");
76            jdbc.update("UPDATE hesap SET bakiye = bakiye + ? WHERE ad = ?", tutar, kime);
77        }
78
79        public String bakiyeler() {
80            Integer ada = jdbc.queryForObject("SELECT bakiye FROM hesap WHERE ad='Ada'", Integer.class);
81            Integer burak = jdbc.queryForObject("SELECT bakiye FROM hesap WHERE ad='Burak'", Integer.class);
82            return "Ada=" + ada + ", Burak=" + burak + " (toplam " + (ada + burak) + ")";
83        }
84    }
85}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek2.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.

Ornek3

ortam gerekir
Ornek3.java
1// Ornek3: Programatik transaction (TransactionTemplate) — kodla açık kontrol.
2// @Transactional bildirimseldir; bazen koddan daha ince kontrol istenir: TransactionTemplate.
3// Çalıştırma: portal Spring + H2 classpath'iyle çalıştırır.
4import org.springframework.jdbc.core.JdbcTemplate;
5import org.springframework.jdbc.datasource.DataSourceTransactionManager;
6import org.springframework.jdbc.datasource.DriverManagerDataSource;
7import org.springframework.transaction.support.TransactionTemplate;
8
9public class Ornek3 {
10
11    public static void main(String[] args) {
12        var ds = new DriverManagerDataSource("jdbc:h2:mem:ptx;DB_CLOSE_DELAY=-1", "sa", "");
13        ds.setDriverClassName("org.h2.Driver");
14        JdbcTemplate jdbc = new JdbcTemplate(ds);
15        jdbc.execute("CREATE TABLE stok (urun VARCHAR(20) PRIMARY KEY, adet INT)");
16        jdbc.update("INSERT INTO stok VALUES ('Klavye', 10)");
17
18        // TransactionTemplate: transaction sınırını KODLA çizeriz.
19        var txTemplate = new TransactionTemplate(new DataSourceTransactionManager(ds));
20
21        // 1) Başarılı işlem -> commit
22        txTemplate.execute(status -> {
23            jdbc.update("UPDATE stok SET adet = adet - 3 WHERE urun = 'Klavye'");
24            return null;
25        });
26        System.out.println("Başarılı işlem sonrası stok: " + stok(jdbc)); // 7
27
28        // 2) İş kuralı ihlali -> setRollbackOnly() ile elle geri al
29        txTemplate.execute(status -> {
30            jdbc.update("UPDATE stok SET adet = adet - 20 WHERE urun = 'Klavye'"); // negatif olurdu
31            Integer adet = stok(jdbc);
32            if (adet < 0) {
33                System.out.println("(stok negatife düştü: " + adet + " -> rollback işaretlendi)");
34                status.setRollbackOnly(); // bu transaction commit edilMEyecek
35            }
36            return null;
37        });
38        System.out.println("Rollback sonrası stok: " + stok(jdbc) + "  <- 7 (değişmedi)");
39
40        // 3) Exception -> otomatik rollback
41        try {
42            txTemplate.execute(status -> {
43                jdbc.update("UPDATE stok SET adet = adet - 2 WHERE urun = 'Klavye'");
44                throw new RuntimeException("doğrulama hatası");
45            });
46        } catch (RuntimeException e) {
47            System.out.println("Exception sonrası stok: " + stok(jdbc) + "  <- 7 (rollback)");
48        }
49
50        System.out.println("""
51
52                --- Deklaratif (@Transactional) vs programatik (TransactionTemplate) ---
53                @Transactional   : en yaygın; metoda anotasyon koyarsın, sınırı Spring yönetir (temiz).
54                TransactionTemplate: transaction'ı koddan açıkça yönetmen gerektiğinde (ince kontrol).
55                Ek kavramlar: PROPAGATION (iç içe transaction davranışı), ISOLATION (eşzamanlılık düzeyi),
56                readOnly (salt-okunur optimizasyonu), rollbackFor (checked exception'larda rollback).""");
57    }
58
59    static Integer stok(JdbcTemplate jdbc) {
60        return jdbc.queryForObject("SELECT adet FROM stok WHERE urun='Klavye'", Integer.class);
61    }
62}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek3.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.

Ornek4

ortam gerekir
Ornek4.java
1// Ornek4: Propagation (yayılım) — REQUIRED vs REQUIRES_NEW.
2// Dış transaction geri alınınca, iç işlem de geri alınır mı? Propagation'a bağlı.
3// @Transactional proxy tabanlıdır; portal bu dosyayı derleyip çalıştırır.
4package com.egitim.spring.tx;
5
6import org.springframework.context.annotation.AnnotationConfigApplicationContext;
7import org.springframework.context.annotation.Bean;
8import org.springframework.context.annotation.Configuration;
9import org.springframework.jdbc.core.JdbcTemplate;
10import org.springframework.jdbc.datasource.DataSourceTransactionManager;
11import org.springframework.jdbc.datasource.DriverManagerDataSource;
12import org.springframework.transaction.PlatformTransactionManager;
13import org.springframework.transaction.annotation.EnableTransactionManagement;
14import org.springframework.transaction.annotation.Propagation;
15import org.springframework.transaction.annotation.Transactional;
16
17import javax.sql.DataSource;
18
19public class Ornek4 {
20
21    public static void main(String[] args) {
22        var ctx = new AnnotationConfigApplicationContext(Config.class);
23        SiparisServisi siparis = ctx.getBean(SiparisServisi.class);
24        JdbcTemplate jdbc = ctx.getBean(JdbcTemplate.class);
25
26        // --- Senaryo A: denetim logu REQUIRES_NEW ile -> AYRI transaction ---
27        // Sipariş başarısız olup geri alınsa bile, denetim logu KALIR (ayrı commit edildi).
28        System.out.println("Senaryo A: log REQUIRES_NEW (ayrı transaction)");
29        try { siparis.siparisVerLogluAyriTx("Klavye"); } catch (RuntimeException e) {
30            System.out.println("  sipariş hata aldı -> geri alındı: " + e.getMessage());
31        }
32        System.out.println("  sipariş sayısı: " + say(jdbc, "siparis") + " (0 bekleniyor — rollback)");
33        System.out.println("  denetim logu  : " + say(jdbc, "denetim") + " (1 bekleniyor — REQUIRES_NEW korudu)");
34
35        // --- Senaryo B: denetim logu REQUIRED ile -> AYNI transaction ---
36        // Sipariş geri alınınca log da geri alınır (ikisi tek transaction).
37        System.out.println("\nSenaryo B: log REQUIRED (aynı transaction)");
38        try { siparis.siparisVerLogluAyniTx("Mouse"); } catch (RuntimeException e) {
39            System.out.println("  sipariş hata aldı -> geri alındı: " + e.getMessage());
40        }
41        System.out.println("  sipariş sayısı: " + say(jdbc, "siparis") + " (yine 0)");
42        System.out.println("  denetim logu  : " + say(jdbc, "denetim") + " (hâlâ 1 — yeni log REQUIRED ile geri alındı)");
43
44        ctx.close();
45        System.out.println("""
46
47                --- Propagation (yayılım) ---
48                REQUIRED (varsayılan): Mevcut transaction varsa ona KATILIR; yoksa yeni açar.
49                                        Dış rollback -> iç de geri alınır (hepsi tek transaction).
50                REQUIRES_NEW         : Her zaman AYRI bir transaction açar (dışarıdakini askıya alır).
51                                        Dış rollback'ten ETKİLENMEZ; bağımsız commit/rollback.
52                Tipik kullanım: REQUIRES_NEW ile 'ne olursa olsun kalsın' denetim/log kayıtları.
53                Diğer modlar: NESTED, SUPPORTS, MANDATORY, NEVER, NOT_SUPPORTED.""");
54    }
55
56    static int say(JdbcTemplate j, String tablo) {
57        return j.queryForObject("SELECT COUNT(*) FROM " + tablo, Integer.class);
58    }
59
60    @Configuration
61    @EnableTransactionManagement
62    public static class Config {
63        @Bean DataSource ds() {
64            var ds = new DriverManagerDataSource("jdbc:h2:mem:prop;DB_CLOSE_DELAY=-1", "sa", "");
65            ds.setDriverClassName("org.h2.Driver");
66            return ds;
67        }
68        @Bean JdbcTemplate jdbcTemplate(DataSource ds) { return new JdbcTemplate(ds); }
69        @Bean PlatformTransactionManager txm(DataSource ds) { return new DataSourceTransactionManager(ds); }
70        @Bean DenetimServisi denetim(JdbcTemplate j) { return new DenetimServisi(j); }
71        @Bean SiparisServisi siparis(JdbcTemplate j, DenetimServisi d) { return new SiparisServisi(j, d); }
72    }
73
74    // Denetim (audit) servisi — log kaydını farklı propagation'larla yazar.
75    public static class DenetimServisi {
76        private final JdbcTemplate jdbc;
77        DenetimServisi(JdbcTemplate jdbc) {
78            this.jdbc = jdbc;
79            jdbc.execute("CREATE TABLE denetim (mesaj VARCHAR(100))");
80        }
81        @Transactional(propagation = Propagation.REQUIRES_NEW) // AYRI transaction
82        public void logAyriTx(String mesaj) { jdbc.update("INSERT INTO denetim VALUES (?)", mesaj); }
83
84        @Transactional(propagation = Propagation.REQUIRED)     // AYNI transaction (varsayılan)
85        public void logAyniTx(String mesaj) { jdbc.update("INSERT INTO denetim VALUES (?)", mesaj); }
86    }
87
88    public static class SiparisServisi {
89        private final JdbcTemplate jdbc;
90        private final DenetimServisi denetim;
91        SiparisServisi(JdbcTemplate jdbc, DenetimServisi denetim) {
92            this.jdbc = jdbc; this.denetim = denetim;
93            jdbc.execute("CREATE TABLE siparis (urun VARCHAR(50))");
94        }
95
96        @Transactional
97        public void siparisVerLogluAyriTx(String urun) {
98            jdbc.update("INSERT INTO siparis VALUES (?)", urun);
99            denetim.logAyriTx("sipariş denendi: " + urun); // REQUIRES_NEW -> ayrı commit
100            throw new RuntimeException("ödeme reddedildi");  // dış transaction rollback
101        }
102
103        @Transactional
104        public void siparisVerLogluAyniTx(String urun) {
105            jdbc.update("INSERT INTO siparis VALUES (?)", urun);
106            denetim.logAyniTx("sipariş denendi: " + urun);  // REQUIRED -> aynı transaction
107            throw new RuntimeException("ödeme reddedildi");  // her şey rollback
108        }
109    }
110}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek4.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.