Gün 3 / Production Basics / Modül 09
Modül 09 Gün 3 Production Basics ~100 dk

Data Access & Transaction Management

Bu modülde veritabanı tarafını sadece “repository yaz, save çağır” seviyesinde değil, production’da gerçekten hata çıkaran noktalarıyla ele alacağız. JPA’nın bize ne kazandırdığını, lazy/eager loading davranışını, N+1 problemini, transaction boundary kararını ve propagation seviyelerini MiniBank üzerinden adım adım göreceğiz.

Anlatım
65 dk
MiniBank Case
50 dk
Odak
JPA + Transaction
Checkpoint
lab-09

Bu modülde sık geçecek kavramlar

Data access konularında kelimeler birbirine çok yakın görünebilir: entity, repository, persistence context, transaction, propagation… Bu bölümde önce dili sadeleştiriyoruz; sonra derinliğe geçiyoruz.

01

Entity

Veritabanındaki bir tabloyu temsil eden Java sınıfıdır. Spring/JPA dünyasında entity, API response modeli değil; persistence modelidir.

02

Repository

Entity üzerinde veritabanı işlemleri yapmak için kullandığımız erişim katmanıdır. Service katmanı SQL veya EntityManager detayına doğrudan gömülmemelidir.

03

Persistence Context

JPA’nın takip ettiği managed entity alanıdır. Bir entity burada managed hale gelirse, değişiklikler transaction sonunda flush edilebilir.

04

EntityManager

JPA’nın entity yaşam döngüsünü yöneten ana bileşenidir. Spring Data JPA repository’leri çoğu zaman bu detayı bizim yerimize kullanır.

05

Transaction

Bir işin ya tamamen başarılı olması ya da geri alınması kuralıdır. Para transferi gibi işlerde transaction boundary business kararının parçasıdır.

06

Transaction Boundary

Transaction’ın nerede başlayıp nerede biteceğidir. Genelde controller’da değil, service katmanında belirlenir.

07

Lazy Loading

İlişkili verinin hemen değil, ihtiyaç olduğunda yüklenmesidir. Performans için faydalıdır ama yanlış yerde LazyInitializationException veya N+1 doğurabilir.

08

Eager Loading

İlişkili verinin ana entity ile birlikte yüklenmesidir. Basit görünür ama gereksiz veri çekmeye ve büyük query’lere sebep olabilir.

09

N+1 Problem

Bir liste için 1 ana query, listedeki her kayıt için ayrıca N query atılmasıdır. Production performans problemlerinin klasik JPA sebeplerindendir.

10

Propagation

Mevcut bir transaction varken çağrılan method’un bu transaction’a katılıp katılmayacağını belirler.

11

Rollback-only

Bir transaction’ın artık commit edilemeyecek şekilde işaretlenmesidir. Exception yakalansa bile transaction commit aşamasında geri dönebilir.

12

Fetch Join / EntityGraph

İlişkili veriyi kontrollü şekilde beraber çekmek için kullanılan yaklaşımlardır. N+1 problemini çözmede sık kullanılır.

JPA ne işe yarar?

JPA, veritabanındaki satırlarla Java objeleri arasında köprü kurmamızı sağlar. Ama asıl değer sadece “SQL yazmadan veri çekmek” değildir. Asıl değer; entity yaşam döngüsü, değişiklik takibi, repository modeli ve transaction ile birlikte çalışan tutarlı bir veri erişim katmanı kurmaktır.

Çok basit tanım

JPA, veritabanındaki kayıtları Java objeleri gibi yönetmemizi sağlar. Spring Data JPA ise repository altyapısıyla bu modeli Spring Boot uygulamasında daha hızlı ve standart kullanmamızı sağlar.

JPA hangi problemi çözer?

JDBC ile çalışırken bağlantı açma, SQL yazma, ResultSet okuma, alanları Java objesine map etme, hata yönetimi ve transaction kontrolü çoğu zaman tekrar eden kod üretir. JPA bu tekrarların önemli kısmını soyutlar. Sen entity ve repository modelini kurarsın; framework kayıt bulma, saklama, değişiklik takip etme ve transaction sonunda veritabanına yansıtma süreçlerini yönetir.

JPA olmadan düşünürsek

SQL string’leri, connection yönetimi, result mapping, try/catch/finally blokları ve transaction sınırları daha fazla manuel kontrol gerektirir.

JPA ile düşünürsek

Entity modelini, repository methodlarını ve transaction boundary’yi doğru tasarlarsın. Framework detayların çoğunu yönetir ama performans etkilerini yine sen bilmek zorundasın.

MiniBank’ta nerede karşımıza çıkacak?

MiniBank’ta Account, Customer, TransferRecord ve AuditLog gibi entity’ler olacak. Repository’ler bu entity’leri okuyacak/yazacak. Service katmanı ise “para transferi bir bütün olarak başarılı mı başarısız mı?” gibi transaction kararlarını yönetecek.

Basit entity ve repository fikri
@Entity
public class Account {
    @Id
    private Long id;

    private String iban;
    private BigDecimal balance;

    // Entity davranışı: bakiye düş, bakiye artır vb.
}

public interface AccountRepository extends JpaRepository<Account, Long> {
    Optional<Account> findByIban(String iban);
}

Persistence Context ve managed entity mantığı

JPA’yı senior seviyede anlamanın kilit noktalarından biri şudur: Repository’den dönen entity sadece sıradan bir Java objesi gibi görünür ama transaction içinde persistence context tarafından takip ediliyor olabilir.

Managed entity ne demek?

Bir entity persistence context içinde managed hale geldiyse, JPA bu objenin değişikliklerini takip eder. Transaction içinde entity üzerinde alan değiştirdiğinde her zaman açıkça save() çağırman gerekmez; transaction sonunda flush sırasında değişiklik veritabanına yansıyabilir. Bu güçlüdür ama davranışın ne zaman gerçekleştiğini bilmezsen şaşırtıcı olabilir.

01
Repository çağrılır
Account veritabanından okunur.
02
Entity managed olur
Persistence context entity’yi takip eder.
03
Service değiştirir
balance alanı güncellenir.
04
Flush olur
Değişiklik SQL’e çevrilir.
05
Commit
Transaction başarıyla kapanır.
Senior dikkat noktası

Entity’yi API response olarak dışarı vermek genellikle iyi fikir değildir. Entity persistence modelidir; API contract değildir. Lazy alanlar, gereksiz ilişki yüklemeleri ve serialization problemleri doğabilir. Bu yüzden REST modülünde öğrendiğimiz DTO ayrımı burada da kritik hale gelir.

Lazy loading vs eager loading

Entity ilişkilerinde en sık yapılan hata, “ilişkiyi ne zaman yüklemek istiyorum?” sorusunu bilinçli sormamaktır. Lazy ve eager sadece annotation seçimi değildir; query davranışı, performans ve API response tasarımını etkiler.

Lazy loading

İlişkili veri hemen çekilmez. İhtiyaç olduğunda yüklenir. Çoğu ilişkide daha güvenli başlangıçtır çünkü gereksiz veri çekimini azaltır.

Eager loading

İlişkili veri ana entity ile birlikte yüklenir. Bazı küçük ve her zaman gereken ilişkilerde mantıklı olabilir ama kontrolsüz kullanılırsa pahalı hale gelir.

MiniBank örneği

Bir hesabın son transferleri her endpoint’te gerekmeyebilir. GET /accounts/{iban} sadece hesap özetini dönebilir. Ama GET /accounts/{iban}/history transfer geçmişini de isteyebilir. Bu iki endpoint aynı veri ihtiyacına sahip değildir; bu yüzden loading stratejisi endpoint ihtiyacına göre düşünülmelidir.

Account.java — ilişki örneği
@Entity
public class Account {
    @Id
    private Long id;

    private String iban;

    @OneToMany(mappedBy = "account", fetch = FetchType.LAZY)
    private List<TransferRecord> transfers = new ArrayList<>();
}
Doğru soru

“Bu ilişki lazy mi eager mı olsun?” yerine önce şunu sor: “Bu use case bu ilişkiye gerçekten ihtiyaç duyuyor mu?” İhtiyaç varsa kontrollü query ile beraber çek; ihtiyaç yoksa hiç yükleme.

N+1 problemi

N+1 problemi, JPA kullanan ekiplerin production’da en sık karşılaştığı performans problemlerinden biridir. Kod küçük görünür, test datasında hızlıdır, ama gerçek veride yüzlerce SQL üretir.

Çok basit anlatım

Diyelim ki 100 hesabı listeledin. İlk query 100 hesabı getirir. Sonra her hesabın transferlerini ayrı ayrı okumaya başlarsan 100 query daha atılır. Toplamda 1 + 100 query oluşur. Buna N+1 diyoruz.

01
Account listesi
1 query ile 100 account gelir.
02
Loop başlar
Her account için transfers alanına erişilir.
03
Ek query’ler
Her account için ayrı select çalışır.
04
Production etkisi
Sayfa yavaşlar, DB yükü artar.

Nasıl fark ederiz?

SQL loglarını açtığında aynı pattern’de tekrar eden select sorguları görürsün. Bu yüzden JPA performansını konuşurken sadece Java koduna bakmak yetmez; üretilen SQL’i de okumak gerekir.

application.yml — eğitimde SQL’i görünür yapma
spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true
logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.orm.jdbc.bind: TRACE

Çözüm yaklaşımları

Fetch join

İhtiyaç duyulan ilişkiyi query içinde bilinçli şekilde beraber çekersin. Özellikle use-case bazlı okumalarda etkilidir.

@EntityGraph

Repository method’u için hangi ilişkilerin yüklenmesini istediğini daha deklaratif ifade edebilirsin.

DTO projection

Entity graph yerine doğrudan ihtiyacın olan alanları response modeline yakın şekilde çekebilirsin.

Repository — fetch join / EntityGraph örnekleri
public interface AccountRepository extends JpaRepository<Account, Long> {

    @Query("select a from Account a left join fetch a.transfers where a.iban = :iban")
    Optional<Account> findByIbanWithTransfers(String iban);

    @EntityGraph(attributePaths = "transfers")
    Optional<Account> findDetailedByIban(String iban);
}

@Transactional doğru kullanımı

@Transactional bir “veritabanı işlemi başlat” annotation’ı gibi görünür, ama aslında bir business boundary ifadesidir. “Bu method içindeki iş tek bir bütün olarak başarılı mı olmalı?” sorusunun cevabıdır.

Nereye koymalıyız?

Genel pratik: transaction boundary service katmanında olur. Controller HTTP detayını yönetir, repository veri erişimini yapar, service ise iş akışını ve transaction sınırını belirler. Para transferi gibi bir işte hem gönderen hesabın düşmesi hem alan hesabın artması aynı transaction içinde olmalıdır.

TransferService.java — transaction boundary
@Service
public class TransferService {

    private final AccountRepository accountRepository;
    private final AuditService auditService;

    public TransferService(AccountRepository accountRepository,
                           AuditService auditService) {
        this.accountRepository = accountRepository;
        this.auditService = auditService;
    }

    @Transactional
    public void transfer(String fromIban, String toIban, BigDecimal amount) {
        Account from = accountRepository.findByIban(fromIban).orElseThrow();
        Account to = accountRepository.findByIban(toIban).orElseThrow();

        from.withdraw(amount);
        to.deposit(amount);

        auditService.recordTransferAttempt(fromIban, toIban, amount);
    }
}

Proxy mantığı burada da geçerli

AOP modülünde gördüğümüz proxy mantığı burada tekrar karşımıza çıkar. @Transactional davranışı Spring proxy’si üzerinden uygulanır. Bu yüzden aynı class içinde this.someTransactionalMethod() çağrısı yaptığında transaction beklendiği gibi çalışmayabilir.

Senior dikkat noktası

Transaction içinde uzun süren external API çağrıları yapmak risklidir. DB connection ve lock süresi uzayabilir. External servis yavaşlarsa transaction da gereksiz yere açık kalır. Bu konuyu bir sonraki modüldeki External API bölümüne bağlayacağız.

Transaction propagation seviyeleri

Propagation, mevcut bir transaction varken çağrılan başka bir method’un o transaction’a katılıp katılmayacağını belirler. Bu bir enum ezberi değil; iş kuralı kararıdır.

En basit soru

TransferService.transfer() transaction içinde çalışıyor. İçeriden AuditService.saveAudit() çağrılırsa audit aynı transaction’a mı katılsın, yoksa bağımsız mı commit olsun?

REQUIRED

Varsa mevcut transaction’a katılır, yoksa yeni transaction açar. Default davranıştır ve çoğu service method’u için doğaldır.

REQUIRES_NEW

Mevcut transaction’ı askıya alır ve bağımsız yeni transaction açar. Audit/outbox gibi bazı durumlarda bilinçli kullanılır.

NESTED

Aynı physical transaction içinde savepoint mantığı sağlar. Kısmi rollback ihtiyacı varsa anlamlıdır; altyapı desteği önemlidir.

SUPPORTS

Transaction varsa katılır, yoksa transaction’sız çalışır. Genelde read/helper akışlarında görülür.

MANDATORY

Transaction olmak zorundadır. Yoksa hata verir. “Bu method transaction dışında anlamlı değil” demenin yoludur.

NOT_SUPPORTED / NEVER

Transaction istemeyen akışlar için kullanılır. NOT_SUPPORTED mevcut transaction’ı askıya alır; NEVER transaction varsa hata verir.

REQUIRED vs REQUIRES_NEW — MiniBank audit kararı

Transfer başarısız olursa audit kaydı da geri alınsın mı? Cevap “evet” ise REQUIRED doğal olabilir. Cevap “transfer rollback olsa bile deneme kaydı kalsın” ise REQUIRES_NEW düşünülebilir. Burada doğru cevap teknik değil, business kararıdır.

AuditService.java — bağımsız audit transaction
@Service
public class AuditService {

    private final AuditLogRepository auditLogRepository;

    public AuditService(AuditLogRepository auditLogRepository) {
        this.auditLogRepository = auditLogRepository;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void recordTransferAttempt(String fromIban, String toIban, BigDecimal amount) {
        auditLogRepository.save(AuditLog.transferAttempt(fromIban, toIban, amount));
    }
}

Rollback-only ve UnexpectedRollbackException

Bazı durumlarda iç method aynı transaction’a katılır ve hata oluştuğunda transaction rollback-only işaretlenir. Sen exception’ı yakalasan bile transaction artık commit edilemez hale gelmiş olabilir. Bu durumda dış method sonunda commit beklerken rollback gerçekleşebilir.

Önemli mesaj

Exception’ı catch etmek her zaman transaction’ı kurtarmaz. Transaction rollback-only olduysa commit aşamasında yine rollback davranışı görebilirsin.

readOnly, timeout, isolation kısa farkları

readOnly

Bu transaction’ın okuma ağırlıklı olduğunu ifade eder. Query niyetini netleştirir ve bazı altyapılarda optimizasyon sağlayabilir.

timeout

Transaction’ın ne kadar süre açık kalabileceğini sınırlar. Uzun süren işlemlerde kaynak tüketimini kontrol etmeye yardımcı olur.

isolation

Aynı anda çalışan transaction’ların birbirinin verisini nasıl göreceğini belirler. Her yerde yükseltmek performans maliyeti doğurabilir.

MiniBank Case 09 — JPA Relations, N+1 Optimization & Transfer Audit Propagation

Bu case’de MiniBank veri modelini gerçekçi hale getiriyoruz: account-transfer ilişkisi kuruyoruz, history endpointinde N+1 davranışını kontrollü şekilde gözlemliyoruz ve transfer audit kaydının transaction davranışını netleştiriyoruz. Görev listesi, doğrulama komutları, kod adımları ve kabul kriterleri Lab Dashboard'da.

Başlangıç
lab-09-start
Tamamlanmış
lab-09-complete
Checkpoint kuralı

Lab sırasında geride kalırsan lab-09-complete branch'ine geçip herkesle aynı checkpoint'ten devam edebilirsin.

Bu modülden akılda kalması gerekenler

1

JPA kolaylık sağlar ama SQL etkisini saklamaz.

Repository kodu temiz görünebilir ama arka planda üretilen SQL’i okumadan performans kararını doğrulayamazsın.

2

Lazy/eager seçimi use-case ile ilgilidir.

Her ilişkiyi eager yapmak çözüm değil; ihtiyaç duyulan veriyi kontrollü query ile çekmek daha sağlıklı yaklaşımdır.

3

N+1 production problemidir.

Test datasında görünmeyen küçük tekrarlar, gerçek veride yüzlerce query’ye dönüşebilir.

4

@Transactional business boundary ifade eder.

Transaction sınırı service katmanında, işin atomikliği düşünülerek belirlenmelidir.

5

Propagation teknik değil, iş kararıdır.

Audit kaydı transfer rollback olsa da kalsın mı? Bu sorunun cevabı REQUIRED mı REQUIRES_NEW mi seçeceğini belirler.

5 soru ile hızlı kontrol

1. Entity ile DTO arasındaki temel fark nedir?

Entity persistence modelidir ve veritabanı yapısına yakındır. DTO ise API contract veya use-case çıktısıdır. Entity’yi doğrudan dışarı vermek lazy loading, serialization ve contract problemleri doğurabilir.

2. N+1 problemi nedir?

Bir liste için önce 1 ana query, sonra listedeki her kayıt için ayrıca query atılmasıdır. Örneğin 100 account için 1 + 100 query oluşması N+1 problemidir.

3. @Transactional genelde hangi katmanda kullanılmalıdır?

Genellikle service katmanında kullanılır. Çünkü transaction boundary HTTP değil, business operation sınırıdır.

4. REQUIRED ve REQUIRES_NEW farkı nedir?

REQUIRED mevcut transaction varsa ona katılır, yoksa yeni açar. REQUIRES_NEW mevcut transaction’dan bağımsız yeni bir transaction açar.

5. Transaction içinde external API çağırmak neden risklidir?

DB connection ve lock’lar açık kalabilir. External servis yavaş veya hatalıysa transaction gereksiz uzar ve sistem kaynakları baskı altına girebilir.