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.
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.
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.
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.
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.
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.
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.
Transaction Boundary
Transaction’ın nerede başlayıp nerede biteceğidir. Genelde controller’da değil, service katmanında belirlenir.
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.
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.
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.
Propagation
Mevcut bir transaction varken çağrılan method’un bu transaction’a katılıp katılmayacağını belirler.
Rollback-only
Bir transaction’ın artık commit edilemeyecek şekilde işaretlenmesidir. Exception yakalansa bile transaction commit aşamasında geri dönebilir.
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.
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.
@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.
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.
@Entity
public class Account {
@Id
private Long id;
private String iban;
@OneToMany(mappedBy = "account", fetch = FetchType.LAZY)
private List<TransferRecord> transfers = new ArrayList<>();
}“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.
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.
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.
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.
@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.
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.
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.
@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.
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.
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
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.
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.
N+1 production problemidir.
Test datasında görünmeyen küçük tekrarlar, gerçek veride yüzlerce query’ye dönüşebilir.
@Transactional business boundary ifade eder.
Transaction sınırı service katmanında, işin atomikliği düşünülerek belirlenmelidir.
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.