Takip et

Spring JDBC ile İşlem Yönetimi: Kapsamlı Bir Rehber ve Örnek Uygulama

Spring JDBC ile İşlem Yönetimi: Kapsamlı Bir Rehber ve Örnek Uygulama Giriş Modern yazılım uygulamaları, veri tutarlılığı ve güvenilirl

Spring JDBC ile İşlem Yönetimi: Kapsamlı Bir Rehber ve Örnek Uygulama

Giriş

Modern yazılım uygulamaları, veri tutarlılığı ve güvenilirliği sağlamak için veritabanlarıyla yoğun bir şekilde etkileşim kurar. Bu etkileşimlerde, birden fazla veritabanı işleminin tek bir mantıksal birim olarak ele alınması hayati öneme sahiptir. İşte bu noktada “işlem yönetimi” (transaction management) devreye girer. İşlem yönetimi, bir dizi operasyonun ya tamamen başarılı bir şekilde tamamlanmasını (commit) ya da herhangi bir hata durumunda tüm değişikliklerin geri alınmasını (rollback) garanti eden mekanizmadır. Bu, özellikle bankacılık, e-ticaret veya envanter yönetimi gibi kritik sistemlerde veri bütünlüğünü korumanın temelidir.

Spring Framework, Java ekosisteminde işlem yönetimini basitleştiren ve standartlaştıran güçlü bir soyutlama katmanı sunar. Geliştiricilerin doğrudan JDBC API’sinin karmaşık ve tekrarlayan işlem yönetimi kodlarıyla uğraşmak yerine, daha temiz ve deklaratif yöntemlerle işlem sınırlarını belirlemesine olanak tanır. Bu makalede, Spring’in JDBC tabanlı uygulamalarda işlem yönetimini nasıl sağladığını, temel kavramları, programatik ve deklaratif yaklaşımları detaylı bir şekilde inceleyecek ve pratik bir örnek uygulama ile konuyu pekiştireceğiz. Amacımız, Spring ile sağlam ve hataya dayanıklı veritabanı işlemleri geliştirmek isteyen okuyuculara kapsamlı bir rehber sunmaktır.

Temel Kavramlar

Spring ile işlem yönetimine dalmadan önce, bir veritabanı işleminin temelini oluşturan kavramları anlamak önemlidir. Bu kavramlar, herhangi bir işlem yönetim sisteminin temelini oluşturur ve Spring’in sunduğu soyutlamaları daha iyi kavramamızı sağlar.

İşlem (Transaction)

İşlem, bir veya daha fazla veritabanı operasyonunun mantıksal olarak tek bir birim olarak kabul edildiği bir dizidir. Bu operasyonlar, ya hepsi başarılı bir şekilde tamamlanır ve kalıcı hale gelir (commit) ya da herhangi bir adımda bir hata meydana gelirse, daha önce yapılan tüm değişiklikler geri alınır (rollback). Bir işlemin temel amacı, veritabanının tutarlı bir durumda kalmasını sağlamaktır. Örneğin, bir banka uygulamasında bir hesaptan diğerine para transferi, iki ayrı operasyon (bir hesaptan para çekme, diğerine para yatırma) içerir. Bu iki operasyonun da başarılı olması veya ikisinin de başarısız olması gerekir; aksi takdirde veritabanı tutarsız bir duruma düşer.

ACID Özellikleri

Veritabanı işlemlerinin güvenilirliğini ve tutarlılığını sağlamak için dört temel özellik seti kullanılır. Bunlara ACID özellikleri denir:

* Atomicity (Bütünlük): Bir işlemdeki tüm operasyonlar ya tamamen başarılı olur ya da hiçbiri olmaz. Kısmi bir başarı söz konusu değildir. Yukarıdaki banka transferi örneğinde, para çekme başarılı olup para yatırma başarısız olursa, çekilen para geri yatırılmalıdır.
* Consistency (Tutarlılık): Bir işlem başladığında ve bittiğinde veritabanı tutarlı bir durumda olmalıdır. İşlem, veritabanı kısıtlamalarını (constraints), tetikleyicileri (triggers) ve iş kurallarını ihlal etmemelidir.
* Isolation (İzolasyon): Eşzamanlı olarak yürütülen işlemler birbirini etkilememelidir. Her işlem, sanki sistemde tek başına çalışıyormuş gibi görünmelidir. Bu özellik, farklı izolasyon seviyeleri ile kontrol edilir ve karmaşık bir konudur.
* Durability (Kalıcılık): Başarılı bir şekilde tamamlanan (commit edilen) bir işlemin sonuçları, sistem arızaları (güç kesintisi, sistem çökmesi vb.) durumunda bile kalıcı olmalıdır. Veritabanı, commit edilmiş veriyi kalıcı depolama birimlerine yazmayı garanti etmelidir.

İşlem İzolasyon Seviyeleri (Isolation Levels)

İzolasyon, eşzamanlı işlemlerin birbirini ne ölçüde etkileyeceğini belirler. Daha yüksek izolasyon seviyeleri, veri tutarlılığını artırırken, eşzamanlılık (concurrency) ve performansı düşürebilir. ANSI SQL standardı dört ana izolasyon seviyesi tanımlar:

* READ UNCOMMITTED: En düşük izolasyon seviyesidir. Bir işlem, başka bir işlem tarafından henüz commit edilmemiş verileri (dirty reads) okuyabilir. Bu, ciddi veri tutarsızlıklarına yol açabilir ve genellikle tavsiye edilmez.
* READ COMMITTED: Bir işlem, yalnızca başka bir işlem tarafından commit edilmiş verileri okuyabilir. Dirty reads’i önler. Ancak, aynı işlem içinde aynı veriyi birden fazla okuma durumunda farklı sonuçlar alabilir (non-repeatable reads), çünkü başka bir işlem arada veriyi güncelleyip commit edebilir.
* REPEATABLE READ: Bir işlem, kendi süresi boyunca okuduğu tüm verilerin değerlerinin değişmeden kalmasını sağlar. Non-repeatable reads’i önler. Ancak, bir sorgunun ilk çalıştırıldığında görmediği ancak daha sonra başka bir işlem tarafından eklenen yeni satırları (phantom reads) görebilir.
* SERIALIZABLE: En yüksek izolasyon seviyesidir. Tüm işlemler, sanki birbiri ardına sırayla çalışıyormuş gibi görünür. Dirty reads, non-repeatable reads ve phantom reads’i tamamen önler. Ancak, eşzamanlılık üzerinde önemli bir performans etkisi vardır ve genellikle sadece çok kritik tutarlılık gerektiren durumlar için kullanılır.

İşlem Yayılımı (Propagation Behaviors)

Spring’de, bir metodun işlem kapsamında nasıl davranacağını belirten “yayılım davranışı” (propagation behavior) kavramı vardır. Bu, iç içe geçmiş işlem çağrılarının nasıl ele alınacağını kontrol eder.

* REQUIRED (Default): Mevcut bir işlem varsa, bu işlemi kullanır. Yoksa, yeni bir işlem başlatır. Çoğu durum için uygun olan varsayılan davranıştır.
* SUPPORTS: Mevcut bir işlem varsa, bu işlemi kullanır. Yoksa, işlemsiz (non-transactional) bir şekilde çalışır.
* MANDATORY: Mevcut bir işlem varsa, bu işlemi kullanır. Yoksa, bir istisna fırlatır. İşlemin kesinlikle var olması gereken durumlar için.
* REQUIRES_NEW: Her zaman yeni bir işlem başlatır. Mevcut bir işlem varsa, onu askıya alır ve yeni işlemi başlatır. Yeni işlem kendi başına commit veya rollback yapar.
* NOT_SUPPORTED: Mevcut bir işlem varsa, onu askıya alır ve işlemsiz bir şekilde çalışır.
* NEVER: Mevcut bir işlem varsa, bir istisna fırlatır. İşlemin kesinlikle olmaması gereken durumlar için.
* NESTED: Mevcut bir işlem varsa, iç içe geçmiş bir işlem (savepoint) başlatır. Bu iç işlem kendi başına geri alınabilir (rollback) ancak ana işlemden bağımsız commit edilemez. Ana işlem rollback yaparsa, iç işlem de rollback olur. JDBC savepoint’leri gerektirir.

Rollback Kuralları (Rollback Rules)

Spring, bir işlem sırasında hangi istisnaların (exceptions) işlemin geri alınmasına (rollback) yol açacağını ve hangilerinin işlemin devam etmesine (commit) izin vereceğini belirleme esnekliği sunar. Varsayılan olarak, Spring, çalışma zamanı istisnaları (runtime exceptions – RuntimeException ve alt sınıfları) ve hatalar (Error ve alt sınıfları) için rollback yapar. Kontrollü istisnalar (checked exceptions) için ise varsayılan olarak rollback yapmaz. Bu davranış, @Transactional anotasyonunun rollbackFor ve noRollbackFor parametreleri ile özelleştirilebilir. Örneğin, belirli bir kontrollü istisna durumunda rollback yapmak veya belirli bir çalışma zamanı istisnası durumunda rollback yapmamak mümkündür.

Spring İşlem Yönetimine Giriş

Spring Framework, işlem yönetimini Java uygulamalarında kolaylaştırmak için güçlü bir soyutlama katmanı sağlar. Bu soyutlama, temel JDBC, Hibernate, JPA veya JMS gibi farklı işlem teknolojileri üzerinde çalışabilir. Spring’in işlem yönetimini kullanmanın temel avantajları şunlardır:

* Basitleştirme: Geliştiricilerin doğrudan veritabanı bağlantısı, commit, rollback gibi düşük seviyeli API çağrılarıyla uğraşmasını engeller. Bu “boilerplate” kodunu ortadan kaldırır.
* Tutarlılık: Farklı işlem teknolojileri için tutarlı bir programlama modeli sunar.
* Deklaratif Yaklaşım: İşlem sınırlarını kod içinde belirlemek yerine, metotlar veya sınıflar üzerinde anotasyonlar veya XML konfigürasyonu aracılığıyla tanımlamaya olanak tanır. Bu, iş mantığını işlem yönetiminden ayırır.
* AOP Entegrasyonu: Spring’in AOP (Aspect-Oriented Programming) yeteneklerini kullanarak işlem yönetimi gibi çapraz kesen endişeleri (cross-cutting concerns) kolayca uygulayabilir.

Spring’de iki ana işlem yönetimi yaklaşımı vardır: programatik ve deklaratif.

Programatik İşlem Yönetimi

Programatik işlem yönetimi, işlem sınırlarını kod içinde açıkça tanımladığınız yaklaşımdır. Bu, daha fazla kontrol sağlar ancak daha fazla kod yazmayı gerektirir ve iş mantığı ile işlem yönetiminin birbirine karışmasına neden olabilir. Spring, programatik işlem yönetimi için iki temel soyutlama sunar:

* PlatformTransactionManager arayüzü: Bu arayüz, Spring’in tüm işlem stratejileri için merkezi bir bileşendir. getTransaction(), commit(), rollback() gibi metotları tanımlar. JDBC için DataSourceTransactionManager, JPA için JpaTransactionManager gibi farklı implementasyonları vardır.
* TransactionDefinition arayüzü: Bir işlemin özelliklerini (yayılım, izolasyon, salt okunur olup olmadığı, zaman aşımı) tanımlar.
* TransactionStatus arayüzü: Devam eden bir işlemin durumunu temsil eder ve setRollbackOnly() gibi metotlar içerir.

Programatik işlem yönetiminin en yaygın ve temiz yolu, TransactionTemplate sınıfını kullanmaktır. TransactionTemplate, PlatformTransactionManager‘ı sarmalar ve execute() metodu aracılığıyla bir geri çağırma (callback) mekanizması sunar. Bu, işlem yönetiminin temel adımlarını (işlem başlatma, commit/rollback, kaynak temizleme) sizin yerinize otomatik olarak halleder.

// Örnek: TransactionTemplate ile programatik işlem yönetimi
public class AccountServiceProgrammatic {

    private final TransactionTemplate transactionTemplate;
    private final JdbcTemplate jdbcTemplate;

    public AccountServiceProgrammatic(PlatformTransactionManager transactionManager, JdbcTemplate jdbcTemplate) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
        this.jdbcTemplate = jdbcTemplate;
    }

    public void transferMoney(long fromAccountId, long toAccountId, double amount) {
        transactionTemplate.execute(status -> {
            try {
                // Hesaptan para çekme
                jdbcTemplate.update("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromAccountId);
                System.out.println("Hesap " + fromAccountId + " den " + amount + " çekildi.");

                // Hata simülasyonu için
                // if (true) throw new RuntimeException("Simüle edilmiş hata!");

                // Hesaba para yatırma
                jdbcTemplate.update("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toAccountId);
                System.out.println("Hesap " + toAccountId + " ye " + amount + " yatırıldı.");

                return null; // Başarılıysa null döner
            } catch (Exception e) {
                status.setRollbackOnly(); // İşlemi geri al
                throw new RuntimeException("Para transferi başarısız oldu: " + e.getMessage(), e);
            }
        });
    }
}

Yukarıdaki örnekte, transferMoney metodu transactionTemplate.execute() içine alınmıştır. Bu, içindeki tüm JDBC operasyonlarının tek bir işlem kapsamında yürütülmesini sağlar. Herhangi bir Exception fırlatıldığında, status.setRollbackOnly() çağrılır ve işlem geri alınır. Bu yaklaşım, işlem kontrolünü doğrudan kodda tutar, ancak daha az tercih edilir çünkü deklaratif yaklaşım genellikle daha temiz ve sürdürülebilirdir.

Deklaratif İşlem Yönetimi

Deklaratif işlem yönetimi, Spring’in en güçlü özelliklerinden biridir ve genellikle tercih edilen yöntemdir. Bu yaklaşım, iş mantığını işlem yönetiminden tamamen ayırır. İşlem sınırları, metotlar veya sınıflar üzerinde @Transactional anotasyonu kullanılarak veya XML konfigürasyonu aracılığıyla tanımlanır. Spring, bu tanımlamaları kullanarak çalışma zamanında gerekli işlem yönetimini otomatik olarak uygular.

Nasıl Çalışır?

Spring, deklaratif işlem yönetimini AOP (Aspect-Oriented Programming – Yönelim Odaklı Programlama) prensipleri aracılığıyla gerçekleştirir. Bir @Transactional anotasyonuna sahip bir metot çağrıldığında, Spring bir proxy nesnesi oluşturur. Bu proxy, gerçek metot çağrılmadan önce ve sonra işlem yönetimi mantığını (işlem başlatma, commit etme, geri alma) ekler. Bu sayede, geliştiricinin iş mantığı koduna işlemle ilgili herhangi bir kod yazması gerekmez.

@Transactional Anotasyonunun Parametreleri

@Transactional anotasyonu, bir işlemin davranışını özelleştirmek için çeşitli parametreler sunar:

* propagation: İşlem yayılım davranışını belirler (örn: Propagation.REQUIRED, Propagation.REQUIRES_NEW). Varsayılan REQUIRED‘dir.
* isolation: İşlem izolasyon seviyesini belirler (örn: Isolation.READ_COMMITTED, Isolation.SERIALIZABLE). Varsayılan, veritabanının varsayılan izolasyon seviyesidir.
* readOnly: İşlemin salt okunur olup olmadığını belirtir (true veya false). true olarak ayarlandığında, veritabanı performans optimizasyonları yapabilir ve yazma operasyonlarına izin vermez.
* timeout: İşlemin saniye cinsinden zaman aşımı süresini belirler. Varsayılan olarak -1 (zaman aşımı yok) değerindedir.
* rollbackFor: Belirtilen istisna tipleri fırlatıldığında işlemin geri alınmasını sağlar. Bir Class[] dizisi alır.
* noRollbackFor: Belirtilen istisna tipleri fırlatıldığında işlemin geri alınmamasını sağlar. Bir Class[] dizisi alır.
* value veya transactionManager: Birden fazla PlatformTransactionManager bean’i tanımlandığında, hangi işlem yöneticisinin kullanılacağını belirtmek için kullanılır.

// Örnek: @Transactional ile deklaratif işlem yönetimi
@Service
public class AccountServiceDeclarative {

    private final JdbcTemplate jdbcTemplate;

    public AccountServiceDeclarative(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = {RuntimeException.class, CustomBusinessException.class})
    public void transferMoney(long fromAccountId, long toAccountId, double amount) throws CustomBusinessException {
        // Hesaptan para çekme
        int updatedRows = jdbcTemplate.update("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromAccountId);
        if (updatedRows == 0) {
            throw new CustomBusinessException("Kaynak hesap bulunamadı veya bakiye yetersiz.");
        }
        System.out.println("Hesap " + fromAccountId + " den " + amount + " çekildi.");

        // Hata simülasyonu için
        // if (true) throw new RuntimeException("Simüle edilmiş hata!");

        // Hesaba para yatırma
        updatedRows = jdbcTemplate.update("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toAccountId);
        if (updatedRows == 0) {
            throw new CustomBusinessException("Hedef hesap bulunamadı.");
        }
        System.out.println("Hesap " + toAccountId + " ye " + amount + " yatırıldı.");
    }
}

Bu örnekte, transferMoney metodu @Transactional anotasyonu ile işaretlenmiştir. Bu sayede, metot çağrıldığında Spring otomatik olarak bir işlem başlatır, metot başarıyla tamamlandığında commit eder ve herhangi bir RuntimeException veya CustomBusinessException fırlatıldığında rollback yapar.

Spring JDBC ile İşlem Yönetimi Uygulama Örneği

Şimdi, Spring JDBC kullanarak deklaratif işlem yönetimini pratik bir örnek üzerinde gösterelim. Bir banka uygulamasında hesaplar arası para transferi senaryosunu ele alacağız.

Proje Yapısı ve Bağımlılıklar

Maven tabanlı bir proje için pom.xml dosyanıza aşağıdaki bağımlılıkları eklemeniz gerekir:



    4.0.0

    com.example
    spring-jdbc-transaction-example
    1.0-SNAPSHOT

    
        17
        17
        6.1.5 
    

    
        
        
            org.springframework
            spring-context
            ${spring.version}
        
        
        
            org.springframework
            spring-jdbc
            ${spring.version}
        
        
        
            com.h2database
            h2
            2.2.224
        
        
        
            com.zaxxer
            HikariCP
            5.1.0
        
        
        
            org.junit.jupiter
            junit-jupiter-api
            5.10.0
            test
        
        
            org.junit.jupiter
            junit-jupiter-engine
            5.10.0
            test
        
    

Veritabanı Yapılandırması

Spring konfigürasyon sınıfımızda DataSource, JdbcTemplate ve PlatformTransactionManager bean’lerini tanımlayacağız. @EnableTransactionManagement anotasyonu, Spring’in @Transactional anotasyonunu taramasını ve işlem proxy’lerini oluşturmasını sağlar.

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration
@ComponentScan(basePackages = "com.example.springjdbctransaction") // Servis ve DAO sınıflarımızı tarar
@EnableTransactionManagement // @Transactional anotasyonunu etkinleştirir
public class AppConfig {

    @Bean
    public DataSource dataSource() {
        // H2 in-memory veritabanı için HikariCP konfigürasyonu
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;INIT=RUNSCRIPT FROM 'classpath:schema.sql'");
        config.setUsername("sa");
        config.setPassword("");
        return new HikariDataSource(config);
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

schema.sql dosyası src/main/resources altında bulunmalıdır ve veritabanı şemasını oluşturur:

CREATE TABLE accounts (
    id INT PRIMARY KEY AUTO_INCREMENT,
    account_holder VARCHAR(255) NOT NULL,
    balance DECIMAL(10, 2) NOT NULL
);

INSERT INTO accounts (account_holder, balance) VALUES ('Alice', 1000.00);
INSERT INTO accounts (account_holder, balance) VALUES ('Bob', 500.00);

DAO Katmanı

DAO (Data Access Object) katmanı, veritabanı işlemlerini soyutlar. AccountDao arayüzü ve AccountDaoImpl implementasyonu, hesap verilerine erişim sağlar.

// com.example.springjdbctransaction.dao.Account.java
package com.example.springjdbctransaction.dao;

public class Account {
    private long id;
    private String accountHolder;
    private double balance;

    // Constructors, getters, setters, toString
    public Account() {}

    public Account(long id, String accountHolder, double balance) {
        this.id = id;
        this.accountHolder = accountHolder;
        this.balance = balance;
    }

    public long getId() { return id; }
    public void setId(long id) { this.id = id; }
    public String getAccountHolder() { return accountHolder; }
    public void setAccountHolder(String accountHolder) { this.accountHolder = accountHolder; }
    public double getBalance() { return balance; }
    public void setBalance(double balance) { this.balance = balance; }

    @Override
    public String toString() {
        return "Account{" +
               "id=" + id +
               ", accountHolder='" + accountHolder + '\'' +
               ", balance=" + balance +
               '}';
    }
}
// com.example.springjdbctransaction.dao.AccountDao.java
package com.example.springjdbctransaction.dao;

import java.util.Optional;

public interface AccountDao {
    Optional findById(long id);
    void updateBalance(long id, double amount);
}
// com.example.springjdbctransaction.dao.AccountDaoImpl.java
package com.example.springjdbctransaction.dao;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public class AccountDaoImpl implements AccountDao {

    private final JdbcTemplate jdbcTemplate;

    public AccountDaoImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public Optional findById(long id) {
        String sql = "SELECT id, account_holder, balance FROM accounts WHERE id = ?";
        try {
            return Optional.ofNullable(jdbcTemplate.queryForObject(sql,
                    (rs, rowNum) -> new Account(rs.getLong("id"), rs.getString("account_holder"), rs.getDouble("balance")),
                    id));
        } catch (org.springframework.dao.EmptyResultDataAccessException e) {
            return Optional.empty();
        }
    }

    @Override
    public void updateBalance(long id, double amount) {
        String sql = "UPDATE accounts SET balance = ? WHERE id = ?";
        jdbcTemplate.update(sql, amount, id);
    }
}

Servis Katmanı

Servis katmanı, iş mantığını içerir ve @Transactional anotasyonunu kullanarak işlem yönetimini sağlar. transferMoney metodu, iki AccountDao operasyonunu tek bir işlem altında birleştirir.

// com.example.springjdbctransaction.service.CustomBusinessException.java
package com.example.springjdbctransaction.service;

public class CustomBusinessException extends Exception {
    public CustomBusinessException(String message) {
        super(message);
    }
}
// com.example.springjdbctransaction.service.AccountService.java
package com.example.springjdbctransaction.service;

import com.example.springjdbctransaction.dao.Account;
import com.example.springjdbctransaction.dao.AccountDao;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountService {

    private final AccountDao accountDao;

    public AccountService(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    @Transactional(propagation = Propagation.REQUIRED,
                   isolation = Isolation.READ_COMMITTED,
                   rollbackFor = CustomBusinessException.class) // CustomBusinessException durumunda da rollback yap
    public void transferMoney(long fromAccountId, long toAccountId, double amount) throws CustomBusinessException {
        System.out.println("--------------------------------------------------");
        System.out.println("Para transferi başlatılıyor: " + fromAccountId + " -> " + toAccountId + ", Miktar: " + amount);

        Account fromAccount = accountDao.findById(fromAccountId)
                .orElseThrow(() -> new CustomBusinessException("Kaynak hesap bulunamadı: " + fromAccountId));
        Account toAccount = accountDao.findById(toAccountId)
                .orElseThrow(() -> new CustomBusinessException("Hedef hesap bulunamadı: " + toAccountId));

        if (fromAccount.getBalance() < amount) {
            throw new CustomBusinessException("Kaynak hesapta yetersiz bakiye: " + fromAccount.getBalance());
        }

        // Kaynak hesaptan para çek
        accountDao.updateBalance(fromAccountId, fromAccount.getBalance() - amount);
        System.out.println("Hesap " + fromAccountId + " den " + amount + " çekildi. Yeni bakiye: " + (fromAccount.getBalance() - amount));

        // Hata simülasyonu: Bu satırın yorumunu kaldırırsanız, işlem rollback olur.
        // if (true) throw new RuntimeException("Simüle edilmiş çalışma zamanı hatası!");
        // if (true) throw new CustomBusinessException("Simüle edilmiş iş mantığı hatası!");

        // Hedef hesaba para yatır
        accountDao.updateBalance(toAccountId, toAccount.getBalance() + amount);
        System.out.println("Hesap " + toAccountId + " ye " + amount + " yatırıldı. Yeni bakiye: " + (toAccount.getBalance() + amount));

        System.out.println("Para transferi başarılı.");
        System.out.println("--------------------------------------------------");
    }

    @Transactional(readOnly = true)
    public Account getAccount(long id) {
        return accountDao.findById(id).orElse(null);
    }
}

Ana Uygulama (Main Application)

Ana uygulamamızda Spring context'i başlatacak ve AccountService'i kullanarak para transferi senaryolarını test edeceğiz.

// com.example.springjdbctransaction.MainApplication.java
package com.example.springjdbctransaction;

import com.example.springjdbctransaction.dao.Account;
import com.example.springjdbctransaction.service.AccountService;
import com.example.springjdbctransaction.service.CustomBusinessException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MainApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        AccountService accountService = context.getBean(AccountService.class);

        // Başlangıç bakiyelerini göster
        printAccountBalances(accountService);

        // Senaryo 1: Başarılı para transferi
        System.out.println("\n--- Senaryo 1: Başarılı Transfer (100 -> 200) ---");
        try {
            accountService.transferMoney(1, 2, 100.00);
        } catch (CustomBusinessException e) {
            System.err.println("Transfer başarısız oldu: " + e.getMessage());
        }
        printAccountBalances(accountService);

        // Senaryo 2: Hatalı para transferi (yetersiz bakiye)
        System.out.println("\n--- Senaryo 2: Yetersiz Bakiye (1 -> 2, 1500) ---");
        try {
            accountService.transferMoney(1, 2, 1500.00);
        } catch (CustomBusinessException e) {
            System.err.println("Transfer başarısız oldu: " + e.getMessage());
        }
        printAccountBalances(accountService);

        // Senaryo 3: Hatalı para transferi (simüle edilmiş RuntimeException)
        // AccountService'deki "if (true) throw new RuntimeException(...)" satırını açın.
        // Bu durumda, ilk hesapta yapılan değişiklik de geri alınacaktır.
        System.out.println("\n--- Senaryo 3: Runtime Hatası Simülasyonu (1 -> 2, 50) ---");
        // accountService.transferMoney metodu içindeki hata simülasyonunu etkinleştirin
        try {
            accountService.transferMoney(1, 2, 50.00);
        } catch (CustomBusinessException e) {
            System.err.println("Transfer başarısız oldu: " + e.getMessage());
        } catch (RuntimeException e) {
            System.err.println("Transfer Runtime hatası nedeniyle başarısız oldu: " + e.getMessage());
        }
        printAccountBalances(accountService);


        context.close();
    }

    private static void printAccountBalances(AccountService accountService) {
        System.out.println("\n--- Mevcut Hesap Bakiyeleri ---");
        Account account1 = accountService.getAccount(1);
        Account account2 = accountService.getAccount(2);
        System.out.println("Hesap 1: " + (account1 != null ? account1.getBalance() : "Bulunamadı"));
        System.out.println("Hesap 2: " + (account2 != null ? account2.getBalance() : "Bulunamadı"));
        System.out.println("-------------------------------\n");
    }
}

Çıktı Analizi:
* Senaryo 1: Alice'den Bob'a 100.00 birim para başarılı bir şekilde transfer edilir. Her iki hesabın bakiyesi de doğru şekilde güncellenir.
* Senaryo 2: Alice'in hesabında yetersiz bakiye olduğu için CustomBusinessException fırlatılır. @Transactional anotasyonu sayesinde, transferMoney metodu içindeki hiçbir değişiklik (eğer yapılmış olsaydı) veritabanına yansımaz; tüm işlem geri alınır ve bakiyeler değişmez kalır.
* Senaryo 3: AccountService içindeki simüle edilmiş RuntimeException etkinleştirildiğinde, para çekme işlemi başarılı olsa bile, sonraki hata nedeniyle tüm işlem geri alınır. Alice'in bakiyesi başlangıçtaki haline döner.

Bu örnek, Spring'in @Transactional anotasyonu ile deklaratif işlem yönetiminin ne kadar güçlü ve kullanımı kolay olduğunu göstermektedir. Geliştirici, veritabanı işlemlerinin detaylarıyla uğraşmak yerine doğrudan iş mantığına odaklanabilir.

İşlem Yönetiminde Dikkat Edilmesi Gerekenler

Spring'in işlem yönetimi oldukça güçlü olsa da, doğru kullanımı için bazı önemli noktaları göz önünde bulundurmak gerekir:

* @Transactional Anotasyonunun Sınırlamaları:
* Public Metotlar: @Transactional anotasyonu yalnızca public metotlar üzerinde etkilidir. Spring AOP, metot çağrılarını yakalamak için proxy'leri kullanır ve proxy'ler genellikle yalnızca public metotları ve arayüz metotlarını sarabilir.
* Self-invocation (Aynı Sınıf İçi Çağrılar): Bir @Transactional metot, aynı sınıf içindeki başka bir metodu (yine @Transactional olsa bile) çağırdığında, bu iç çağrı için işlem proxy'si atlanır. Bu durumda, iç metot için yeni bir işlem başlatılmaz veya mevcut işlem kullanılmaz; çağrı basitçe doğrudan yapılır. Bu, genellikle yayılım davranışlarının beklendiği gibi çalışmamasına neden olur. Bu durumu aşmak için, çağrıları farklı bir servis bean'ine taşımak veya ApplicationContext'ten servis bean'ini almak gerekebilir (ancak bu son yöntem genellikle önerilmez).
* Performans Etkileri: Yüksek izolasyon seviyeleri (özellikle SERIALIZABLE), eşzamanlılık üzerinde önemli bir kısıtlama getirerek performansı düşürebilir. İşlem kapsamını çok geniş tutmak da performansı olumsuz etkileyebilir çünkü veritabanı kaynakları daha uzun süre kilitli kalır. İhtiyaç duyulan en düşük izolasyon seviyesini ve en dar işlem kapsamını kullanmaya özen gösterin.
* Veritabanı Kilitlenmeleri (Deadlocks): Eşzamanlı işlemlerin birbirini beklemesi sonucu oluşan kilitlenmeler, işlem yönetiminin karmaşık bir yönüdür. Spring'in kendisi kilitlenmeleri çözmez, ancak işlem zaman aşımı (timeout) gibi özelliklerle uzun süren işlemleri sonlandırabilir. Kilitlenmeleri önlemek için veritabanı tasarımını, sorgu optimizasyonlarını ve kilitlenme stratejilerini (optimistik/pesimistik kilitlenme) iyi anlamak gerekir.
* İşlem Kapsamını Doğru Belirleme: Bir işlemin kapsamı, sadece ilgili veritabanı operasyonlarını içermelidir. Uzun süren hesaplamalar, dış sistem çağrıları (örneğin, bir REST API'sine çağrı) veya kullanıcı girişi bekleyen operasyonlar işlem dışında tutulmalıdır. Aksi takdirde, veritabanı bağlantıları gereksiz yere açık kalır, kilitler uzun süre tutulur ve sistem performansı düşer.
* Test Edilebilirlik: İşlem yönetimini doğru bir şekilde test etmek önemlidir. Spring, @Transactional test anotasyonu ve test bağlamı yönetimi ile entegrasyon testlerini kolaylaştırır. Testlerde rollbackFor ve noRollbackFor davranışlarını, farklı yayılım ve izolasyon seviyelerini test etmek, uygulamanın sağlamlığını artırır.
* DataSource ve PlatformTransactionManager Seçimi: Kullanılan veritabanı ve ORM/JDBC teknolojisine uygun DataSource (örn: HikariCP, Apache Commons DBCP2) ve PlatformTransactionManager (örn: DataSourceTransactionManager, JpaTransactionManager) implementasyonlarını seçmek önemlidir.

Sonuç

Spring Framework, JDBC tabanlı uygulamalarda işlem yönetimini inanılmaz derecede basitleştiren ve standartlaştıran güçlü bir soyutlama katmanı sunar. Bu makalede, işlem yönetiminin temel kavramlarını (ACID, izolasyon, yayılım), Spring'in programatik ve deklaratif yaklaşımlarını detaylı bir şekilde inceledik. Özellikle @Transactional anotasyonunun sağladığı deklaratif modelin, iş mantığını işlem yönetimi detaylarından ayırarak kodu daha temiz, okunabilir ve sürdürülebilir hale getirdiğini gördük.

Sağladığımız pratik örnek uygulama, bir banka transferi senaryosu üzerinden Spring JDBC ve @Transactional anotasyonunun nasıl birlikte çalıştığını somut bir şekilde gösterdi. Başarılı transferler, yetersiz bakiye ve çalışma zamanı hataları gibi farklı senaryolarda işlemlerin nasıl yönetildiğini, commit ve rollback mekanizmalarının nasıl devreye girdiğini gözlemledik.

Spring'in işlem yönetimi yetenekleri sayesinde geliştiriciler, veri tutarlılığı ve güvenilirliği sağlama gibi kritik görevleri çok daha az "boilerplate" kodu ile gerçekleştirebilirler. Ancak, bu gücü etkili bir şekilde kullanmak için izolasyon seviyeleri, yayılım davranışları ve @Transactional anotasyonunun sınırlamaları gibi temel kavramları iyi anlamak esastır. Doğru yapılandırma ve dikkatli bir tasarım ile Spring JDBC, sağlam ve yüksek performanslı veritabanı uygulamaları geliştirmek için vazgeçilmez bir araçtır. Gelecekte, Spring Boot'un otomatik yapılandırma özellikleriyle bu kurulumların daha da basitleştirilebileceğini unutmamak gerekir.

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Gönder

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.
Mobil sürümden çık