JpaRepository ile CRUD İşlemleri
Giriş
Geleneksel Java'da veritabanından bir kullanıcı çekmek için en az 30 satır JDBC kodu yazmanız gerekiyordu: bağlantı açma, SQL hazırlama, parametre set etme, ResultSet'i parse etme, kaynakları kapatma... Spring Data JPA ile aynı işlem sıfır satır implementasyon kodu ile yapılır. Sadece bir interface tanımlarsınız, Spring gerisini halleder.
Gerçek Dünya Analojisi
Bir restoran düşünün. Mutfakta yemek hazırlamak (CRUD işlemleri) ile garsonun sipariş almak (interface tanımlamak) tamamen farklı işler. Spring Data JPA'da siz garson rolündesiniz: "Bana şu kullanıcıyı getir, bunu kaydet, şunu sil" dersiniz. Mutfaktaki şef (Spring) nasıl pişireceğini (SQL sorguları, bağlantı yönetimi) bilir. Siz sadece sipariş verirsiniz.
Bu ders, Repository Pattern'in Spring Data JPA'daki implementasyonunu ve JpaRepository'nin sunduğu tüm CRUD yeteneklerini kapsamlı şekilde ele alacaktır.
Repository Pattern Nedir?
Repository pattern, veri erişim mantığını (SQL sorguları, bağlantı yönetimi) iş mantığından ayırır. Sınıf hiyerarşisi şöyledir:
Repository<T, ID> (marker interface — metot yok)
└── CrudRepository<T, ID> (temel CRUD: save, findById, delete, count)
└── ListCrudRepository (List döndüren versiyonlar)
└── PagingAndSortingRepository (+ sayfalama ve sıralama)
└── JpaRepository<T, ID> (+ flush, batch, Example query)Neden bu hiyerarşi var? Çünkü her proje aynı yeteneklere ihtiyaç duymaz. Basit bir uygulama CrudRepository ile yetinirken, karmaşık bir e-ticaret JpaRepository'nin tüm özelliklerini kullanabilir.
💡 İpucu: Pratikte neredeyse herkes JpaRepository kullanır çünkü en kapsamlı olanıdır ve diğerlerinin tüm metotlarını içerir. "Fazla özellik" burada bir sorun değildir — kullanmadıklarınız performansı etkilemez.
İlk Repository Oluşturma
Entity Tanımı
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private int age;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role = Role.USER;
@Column(nullable = false)
private boolean active = true;
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt = LocalDateTime.now();
private LocalDateTime updatedAt;
// JPA zorunlu — parametresiz constructor
protected User() {}
public User(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
// Getter/Setter'lar
public Long getId() { return id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public Role getRole() { return role; }
public void setRole(Role role) { this.role = role; }
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}
public enum Role {
USER, ADMIN, MODERATOR
}Repository Interface
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
// Bu kadar! Hiçbir metot yazmaya gerek yok.
// JpaRepository'den onlarca hazır CRUD metodu devralınır.
}JpaRepository<User, Long> ifadesindeki:
User→ yönetilecek entity tipiLong→ entity'nin@Idalanının tipi
Spring Boot uygulaması başladığında, bu interface için runtime'da otomatik olarak bir implementasyon sınıfı oluşturulur. Siz asla UserRepositoryImpl yazmak zorunda kalmazsınız.
CREATE — Kayıt Oluşturma
Tekil Kayıt
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public User createUser(String name, String email, int age) {
User user = new User(name, email, age);
return userRepository.save(user); // INSERT SQL üretir
}
}save() çağrıldığında arka planda neler olur:
Entity'nin
@Idalanı (id) kontrol edilirid == null→ yeni entity →INSERT INTO users (...) VALUES (...)SQL'i çalıştırılırVeritabanı auto-increment ile ID oluşturur
Oluşturulan ID, Java nesnesine set edilir
Managed (yönetilen) entity döndürülür
User user = new User("Ali", "ali@email.com", 25);
System.out.println(user.getId()); // null (henüz kaydedilmedi)
User saved = userRepository.save(user);
System.out.println(saved.getId()); // 1 (veritabanı tarafından atandı)Toplu Kayıt (Batch Insert)
public List<User> createManyUsers() {
List<User> users = List.of(
new User("Ali", "ali@email.com", 25),
new User("Ayşe", "ayse@email.com", 30),
new User("Mehmet", "mehmet@email.com", 28)
);
return userRepository.saveAll(users); // Toplu INSERT
}⚠️ Dikkat: saveAll() varsayılan olarak her entity için ayrı INSERT çalıştırır. Gerçek batch insert için Hibernate ayarlarını yapılandırın:
# application.properties
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=trueREAD — Kayıt Okuma
ID ile Sorgulama
// findById → Optional döner (null-safe)
public User findUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
}
// Alternatif: Varsayılan değer
public User findUserOrDefault(Long id) {
return userRepository.findById(id)
.orElse(new User("Default", "default@email.com", 0));
}
// Var mı yok mu kontrolü (entity çekmeden)
public boolean userExists(Long id) {
return userRepository.existsById(id); // SELECT count(*) > 0
}Neden `Optional` döner? Çünkü ID ile aranan kayıt her zaman bulunamayabilir. Optional, NullPointerException riskini ortadan kaldırır ve geliştiriciye "bu değer olmayabilir, buna hazırlıklı ol" mesajını verir.
Tüm Kayıtları Getirme
// Tüm kullanıcılar
public List<User> findAllUsers() {
return userRepository.findAll(); // SELECT * FROM users
}
// Sıralı getirme
public List<User> findAllUsersSorted() {
return userRepository.findAll(Sort.by("name").ascending());
// SELECT * FROM users ORDER BY name ASC
}
// Toplam kayıt sayısı
public long countUsers() {
return userRepository.count(); // SELECT count(*) FROM users
}ID Listesi ile Toplu Sorgulama
public List<User> findUsersByIds(List<Long> ids) {
return userRepository.findAllById(ids);
// SELECT * FROM users WHERE id IN (1, 2, 3, 5, 8)
}UPDATE — Kayıt Güncelleme
Spring Data JPA'da ayrı bir update() metodu yoktur. Güncelleme de save() ile yapılır:
public User updateEmail(Long id, String newEmail) {
// 1. Mevcut entity'yi veritabanından çek
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
// 2. Alanları güncelle
user.setEmail(newEmail);
user.setUpdatedAt(LocalDateTime.now());
// 3. save() çağrısı — id var olduğu için UPDATE çalıştırır
return userRepository.save(user);
// UPDATE users SET email = ?, updated_at = ? WHERE id = ?
}save() Davranışı: INSERT vs UPDATE Kararı
// @Id alanı null → INSERT (yeni kayıt)
User newUser = new User("Ali", "ali@email.com", 25);
newUser.getId(); // null
userRepository.save(newUser); // INSERT INTO users ...
// @Id alanı dolu → SELECT + UPDATE (mevcut kayıt)
User existingUser = userRepository.findById(1L).get();
existingUser.setEmail("newemail@email.com");
userRepository.save(existingUser); // UPDATE users SET ... WHERE id = 1Karar mekanizması şöyledir:
Entity'nin
@Idalanınullmı? → INSERT@Iddolu ama entity managed (persistence context'te) mi? → Dirty checking ile UPDATE (save bile gerekmez!)@Iddolu ama entity detached mi? →SELECT+UPDATE(merge)
Dirty Checking — save() Çağırmadan Güncelleme
@Transactional bir metot içinde, managed entity'nin alanlarını değiştirdiğinizde, transaction commit anında Hibernate otomatik olarak UPDATE çalıştırır:
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional
public void deactivateUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
user.setActive(false); // Bu değişiklik otomatik veritabanına yansır!
// save() çağırmaya GEREK YOK — dirty checking yapar
}
}💡 İpucu: Dirty checking, Hibernate'in en güçlü özelliklerinden biridir. Transaction içinde bir entity'yi çekip alanlarını değiştirirseniz, transaction commit'te otomatik UPDATE oluşturulur. Ama bu "gizli" davranışı bilmezseniz, beklenmeyen UPDATE'ler karşınıza çıkabilir.
DELETE — Kayıt Silme
// ID ile silme
public void deleteUser(Long id) {
userRepository.deleteById(id);
// DELETE FROM users WHERE id = ?
}
// Entity nesnesi ile silme
public void deleteUser(User user) {
userRepository.delete(user);
}
// Koşullu silme (önce kontrol)
public void deleteUserSafe(Long id) {
if (userRepository.existsById(id)) {
userRepository.deleteById(id);
} else {
throw new UserNotFoundException(id);
}
}
// Toplu silme
public void deleteUsers(List<Long> ids) {
List<User> users = userRepository.findAllById(ids);
userRepository.deleteAll(users);
// Her biri için ayrı DELETE çalışır
}
// Tüm kayıtları silme (DİKKAT!)
public void deleteAllUsers() {
// Yöntem 1: Her biri için ayrı DELETE (yavaş)
userRepository.deleteAll();
// Yöntem 2: Tek SQL ile silme (hızlı)
userRepository.deleteAllInBatch();
// DELETE FROM users (tek sorgu)
}deleteAll() vs deleteAllInBatch()
| Metot | SQL Sayısı | Cascade | Lifecycle Callbacks |
|---|---|---|---|
deleteAll() | N adet DELETE | ✅ Çalışır | ✅ @PreRemove çalışır |
deleteAllInBatch() | 1 adet DELETE | ❌ Çalışmaz | ❌ Çalışmaz |
deleteAllByIdInBatch(ids) | 1 adet DELETE | ❌ Çalışmaz | ❌ Çalışmaz |
⚠️ Dikkat: deleteAllInBatch() hızlıdır ama entity lifecycle callback'leri (@PreRemove) ve cascade delete'leri çalıştırmaz. İlişkili veriler varsa dikkatli kullanın.
flush() ve saveAndFlush()
Hibernate, performans için SQL sorgularını biriktirip (buffering) toplu gönderir. flush() bu biriktirilen sorguları hemen veritabanına gönderir:
// save() → Persistence context'e yazar (veritabanına hemen gitmeyebilir)
User user = userRepository.save(new User("Ali", "ali@email.com", 25));
// SQL henüz veritabanına gitmemiş olabilir
// saveAndFlush() → Hemen veritabanına yazar
User user = userRepository.saveAndFlush(new User("Ali", "ali@email.com", 25));
// SQL kesinlikle veritabanına gitti
// flush() → Bekleyen TÜM değişiklikleri veritabanına gönderir
userRepository.save(user1);
userRepository.save(user2);
userRepository.save(user3);
userRepository.flush(); // 3 INSERT birden veritabanına giderNe zaman flush kullanmalı?
Veritabanı tarafında oluşturulan değerlere (ör: trigger ile doldurulan alanlar) hemen erişmeniz gerektiğinde
Test yazarken, verinin gerçekten veritabanına yazıldığını doğrulamak istediğinizde
Birden fazla entity kaydetip aralarında veritabanı seviyesinde tutarlılık kontrol etmeniz gerektiğinde
getReferenceById() vs findById()
// findById() → Veritabanından SELECT çalıştırır, tam entity döner
Optional<User> user = userRepository.findById(1L);
// SELECT * FROM users WHERE id = 1
// getReferenceById() → Proxy döner, veritabanına gitmez (lazy)
User userRef = userRepository.getReferenceById(1L);
// SELECT yok! Sadece proxy oluşturulur
// Proxy'nin alanlarına eriştiğinizde SELECT çalışırNe zaman kullanılır? Sadece ilişki (foreign key) kurmak için:
@Transactional
public Order createOrder(Long userId, BigDecimal amount) {
// Tam user'ı çekmeye gerek yok, sadece ID referansı yeterli
User userRef = userRepository.getReferenceById(userId);
Order order = new Order();
order.setUser(userRef); // Sadece user_id foreign key set edilir
order.setAmount(amount);
return orderRepository.save(order);
// INSERT INTO orders (user_id, amount) VALUES (1, 99.90)
// users tablosuna SELECT gitmez!
}Example Query — Dinamik Sorgulama
JpaRepository, Query by Example (QBE) özelliğini destekler:
// Bir "örnek" entity oluşturup, buna uyanları sorgulayın
public List<User> findByExample(String name, Role role) {
User probe = new User();
probe.setName(name);
probe.setRole(role);
Example<User> example = Example.of(probe, ExampleMatcher.matching()
.withIgnoreNullValues() // null alanları yoksay
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING) // LIKE %...%
.withIgnoreCase()); // Case-insensitive
return userRepository.findAll(example);
}
// Daha spesifik matcher
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("name", match -> match.startsWith()) // name LIKE 'Ali%'
.withMatcher("email", match -> match.endsWith()) // email LIKE '%@gmail.com'
.withIgnorePaths("age", "active"); // Bu alanları yoksayBütünleşik Gerçek Dünya Örneği: Kullanıcı Yönetim Servisi
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true) // Tüm okuma metotları için
public class UserManagementService {
private final UserRepository userRepository;
// === CREATE ===
@Transactional
public User registerUser(RegisterRequest request) {
// Email benzersizlik kontrolü
if (userRepository.existsByEmail(request.email())) {
throw new DuplicateEmailException(request.email());
}
User user = new User(request.name(), request.email(), request.age());
user.setRole(Role.USER);
return userRepository.save(user);
}
// === READ ===
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
public Page<User> getAllUsers(Pageable pageable) {
return userRepository.findAll(pageable);
}
public UserStats getUserStats() {
return new UserStats(
userRepository.count(),
userRepository.countByActiveTrue(),
userRepository.countByRole(Role.ADMIN)
);
}
// === UPDATE ===
@Transactional
public User updateProfile(Long id, UpdateProfileRequest request) {
User user = getUserById(id);
user.setName(request.name());
user.setAge(request.age());
user.setUpdatedAt(LocalDateTime.now());
// Dirty checking: save() çağırmaya gerek yok ama açıkça yazmak okunabilirliği artırır
return userRepository.save(user);
}
@Transactional
public User changeRole(Long id, Role newRole) {
User user = getUserById(id);
user.setRole(newRole);
return userRepository.save(user);
}
// === DELETE ===
@Transactional
public void deactivateUser(Long id) {
// Soft delete: Gerçekten silmiyoruz, pasif yapıyoruz
User user = getUserById(id);
user.setActive(false);
user.setUpdatedAt(LocalDateTime.now());
userRepository.save(user);
}
@Transactional
public void hardDeleteUser(Long id) {
// Gerçek silme: Sadece admin yetkisi ile
if (!userRepository.existsById(id)) {
throw new UserNotFoundException(id);
}
userRepository.deleteById(id);
}
}
// Repository — ek derived query methods
public interface UserRepository extends JpaRepository<User, Long> {
boolean existsByEmail(String email);
long countByActiveTrue();
long countByRole(Role role);
Optional<User> findByEmail(String email);
}Yaygın Hatalar ve Çözümleri
Hata 1: Optional'ı Yanlış Kullanmak
// ❌ YANLIŞ — .get() çağırmak Optional'ın amacını yok eder
User user = userRepository.findById(id).get(); // NoSuchElementException riski!
// ❌ YANLIŞ — isPresent() + get() anti-pattern
if (userRepository.findById(id).isPresent()) {
User user = userRepository.findById(id).get(); // İki kez sorgu çalışır!
}
// ✅ DOĞRU — orElseThrow ile anlamlı exception
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("Kullanıcı bulunamadı: " + id));Hata 2: save() Dönüş Değerini Kullanmamak
// ❌ YANLIŞ — dönüş değeri kullanılmıyor, id null kalır
User user = new User("Ali", "ali@email.com", 25);
userRepository.save(user);
System.out.println(user.getId()); // Bazı durumlarda null olabilir!
// ✅ DOĞRU — save'in döndürdüğü managed entity'yi kullan
User user = new User("Ali", "ali@email.com", 25);
User savedUser = userRepository.save(user);
System.out.println(savedUser.getId()); // 1 (her zaman doğru)Hata 3: Transactional Olmadan Lazy Loading
// ❌ YANLIŞ — Transaction dışında lazy alanına erişim
public User getUser(Long id) {
User user = userRepository.findById(id).get();
user.getOrders().size(); // LazyInitializationException!
return user;
}
// ✅ DOĞRU — @Transactional ile
@Transactional(readOnly = true)
public User getUser(Long id) {
User user = userRepository.findById(id).get();
user.getOrders().size(); // Transaction içinde çalışır
return user;
}Hata 4: deleteById Null ID
// ❌ deleteById(null) → EmptyResultDataAccessException veya NPE
userRepository.deleteById(null);
// ✅ Null kontrolü yapın
public void deleteUser(Long id) {
Objects.requireNonNull(id, "User ID cannot be null");
userRepository.deleteById(id);
}JpaRepository Tüm Metot Referansı
| Metot | SQL Karşılığı | Dönüş Tipi |
|---|---|---|
save(entity) | INSERT veya UPDATE | T |
saveAll(entities) | Toplu INSERT/UPDATE | List<T> |
saveAndFlush(entity) | INSERT/UPDATE + hemen gönder | T |
findById(id) | SELECT WHERE id = ? | Optional<T> |
findAll() | SELECT * | List<T> |
findAll(Sort) | SELECT * ORDER BY ... | List<T> |
findAll(Pageable) | SELECT * LIMIT ... OFFSET ... | Page<T> |
findAllById(ids) | SELECT WHERE id IN (...) | List<T> |
existsById(id) | SELECT count(*) > 0 | boolean |
count() | SELECT count(*) | long |
deleteById(id) | DELETE WHERE id = ? | void |
delete(entity) | DELETE WHERE id = ? | void |
deleteAll(entities) | N × DELETE | void |
deleteAllInBatch() | Tek DELETE | void |
flush() | Biriktirilen SQL'leri gönder | void |
getReferenceById(id) | Proxy döner (lazy) | T |
Özet
JpaRepository extends edip sıfır kod yazarak tam CRUD alırsınız — implementasyon otomatik oluşturulur
`save()` akıllıdır:
@Idnull ise INSERT, dolu ise UPDATE çalıştırır`findById()`
Optionaldöner —orElseThrow()ile null-safe kullanınDirty checking sayesinde
@Transactionalmetot içinde entity alanlarını değiştirmek yeterli,save()bile gerekmeyebilir`deleteAllInBatch()` hızlıdır ama cascade ve lifecycle callback'leri atlar
`getReferenceById()` sadece foreign key ilişkisi kurmak için kullanın — gereksiz SELECT'ten kaçının
`flush()` ve `saveAndFlush()` veritabanına anında yazım gerektiğinde kullanılır
AI Asistan
Sorularını yanıtlamaya hazır