← Kursa Dön
📄 Text · 20 min

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 tipi

  • Long → entity'nin @Id alanı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:

  1. Entity'nin @Id alanı (id) kontrol edilir

  2. id == nullyeni entityINSERT INTO users (...) VALUES (...) SQL'i çalıştırılır

  3. Veritabanı auto-increment ile ID oluşturur

  4. Oluşturulan ID, Java nesnesine set edilir

  5. 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=true

READ — 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 = 1

Karar mekanizması şöyledir:

  1. Entity'nin @Id alanı null mı? → INSERT

  2. @Id dolu ama entity managed (persistence context'te) mi? → Dirty checking ile UPDATE (save bile gerekmez!)

  3. @Id dolu 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()

MetotSQL SayısıCascadeLifecycle 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 gider

Ne 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ışır

Ne 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ı yoksay

Bü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ı

MetotSQL KarşılığıDönüş Tipi
save(entity)INSERT veya UPDATET
saveAll(entities)Toplu INSERT/UPDATEList<T>
saveAndFlush(entity)INSERT/UPDATE + hemen gönderT
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(*) > 0boolean
count()SELECT count(*)long
deleteById(id)DELETE WHERE id = ?void
delete(entity)DELETE WHERE id = ?void
deleteAll(entities)N × DELETEvoid
deleteAllInBatch()Tek DELETEvoid
flush()Biriktirilen SQL'leri göndervoid
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: @Id null ise INSERT, dolu ise UPDATE çalıştırır

  • `findById()` Optional döner — orElseThrow() ile null-safe kullanın

  • Dirty checking sayesinde @Transactional metot 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