Cascade & orphanRemoval Detaylı
Giriş — Neden Bu Konu Önemli?
Bir sipariş ve onun kalemleri (order items) arasındaki ilişkiyi düşünün. Sipariş kaydedildiğinde kalemleri de kaydedilmeli, sipariş silindiğinde kalemleri de silinmeli, bir kalem siparişten çıkarıldığında veritabanından da kalkmalı. Bu davranışları elle yönetmek hem hataya açık hem de kod tekrarına yol açar.
JPA'da cascade (zincirleme) ve orphanRemoval (yetim kaldırma) mekanizmaları, parent-child ilişkilerinde entity yaşam döngüsünü yönetmenin temel araçlarıdır. Bu mekanizmaları yanlış kullanmak veri kaybına veya performans sorunlarına yol açabilir — bu yüzden her bir cascade tipini derinlemesine anlamak kritiktir.
Gerçek Hayat Analojisi: Bir şirket ve çalışanları düşünün. Şirket iflas ettiğinde (REMOVE) çalışanlar da işsiz kalır — bu cascade. Bir çalışan departmandan çıkarıldığında (koleksiyondan kaldırıldığında) eğer başka yerde çalışmıyorsa kontratı feshedilir — bu orphanRemoval. Cascade operasyonun yukarıdan aşağı yayılması, orphanRemoval ise "sahipsiz kalan" entity'nin temizlenmesidir.
CascadeType Türleri
JPA altı cascade tipi tanımlar. Her biri, parent entity üzerindeki bir operasyonun child entity'lere yayılmasını kontrol eder:
CascadeType.PERSIST
Parent entity persist() edildiğinde, child entity'ler de otomatik olarak persist edilir:
@OneToMany(mappedBy = "author", cascade = CascadeType.PERSIST)
private List<Book> books;
// Kullanım:
Author author = new Author("Orhan Pamuk");
Book book1 = new Book("Kar");
Book book2 = new Book("İstanbul");
author.addBook(book1);
author.addBook(book2);
entityManager.persist(author);
// → 1 INSERT author + 2 INSERT book = 3 SQL statement
// author VE tüm book'lar veritabanına kaydedilirPERSIST olmadan, her child entity'yi ayrı ayrı persist() etmeniz gerekirdi:
// ❌ Cascade PERSIST olmadan — her birini ayrı kaydet
entityManager.persist(author);
entityManager.persist(book1); // Unutursanız kaybolur!
entityManager.persist(book2);💡 İpucu: Spring Data JPA'da
repository.save()kullandığınızda, entity yeniysepersist(), mevcutsamerge()çağrılır. Cascade.PERSIST yeni entity'lerin otomatik kaydedilmesini sağlar.
CascadeType.MERGE
Parent entity merge() edildiğinde, child entity'ler de merge edilir. Bu, detached (oturumdan kopmuş) entity'leri güncellerken önemlidir:
@OneToMany(mappedBy = "author", cascade = CascadeType.MERGE)
private List<Book> books;
// Detached author objesini güncelleme
author.setName("Orhan Pamuk - Güncellendi");
author.getBooks().get(0).setTitle("Kar - Yeni Baskı");
entityManager.merge(author);
// → UPDATE author SET name = ... WHERE id = ?
// → UPDATE book SET title = ... WHERE id = ?
// author VE book güncellenirMERGE olmadan, detached parent'ı merge ettiğinizde child'lardaki değişiklikler kaybolur.
CascadeType.REMOVE
Parent entity silindiğinde, child entity'ler de silinir:
@OneToMany(mappedBy = "author", cascade = CascadeType.REMOVE)
private List<Book> books;
entityManager.remove(author);
// → Hibernate şu sırayla çalışır:
// 1. DELETE FROM book WHERE author_id = ? (tüm kitaplar)
// 2. DELETE FROM author WHERE id = ?
// Önce child'lar silinir, sonra parent — FK constraint ihlali olmaz⚠️ Dikkat:
CascadeType.REMOVE, child entity'lerin başka parent'larla da ilişkisi varsa tehlikelidir! Örneğin birBookbirden fazlaAuthorile ilişkiliyse (@ManyToMany), bir author silindiğinde book da silinir ve diğer author'lar bozulur. REMOVE cascade'ini sadece gerçek parent-child (sahiplik) ilişkilerinde kullanın.
CascadeType.REFRESH
Parent entity refresh() edildiğinde, child'lar da veritabanından yeniden yüklenir:
@OneToMany(mappedBy = "department", cascade = CascadeType.REFRESH)
private List<Employee> employees;
// Senaryo: Başka bir thread veritabanını güncelledi
entityManager.refresh(department);
// → SELECT * FROM department WHERE id = ?
// → SELECT * FROM employee WHERE department_id = ?
// department VE employees veritabanından yeniden okunur
// In-memory değişiklikler atılır!REFRESH nadiren kullanılır, ancak concurrent erişim durumlarında veritabanındaki güncel veriyi almak için faydalıdır.
CascadeType.DETACH
Parent entity detach edildiğinde, child'lar da persistence context'ten çıkarılır:
@OneToMany(mappedBy = "department", cascade = CascadeType.DETACH)
private List<Employee> employees;
entityManager.detach(department);
// → department VE employees artık managed değil
// Yapılan değişiklikler veritabanına yansımazCascadeType.ALL
Tüm cascade tiplerini birden aktif eder: PERSIST + MERGE + REMOVE + REFRESH + DETACH.
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
private List<Book> books;ALL kullanmak basit ve pratiktir, ancak gerçekten tüm operasyonların yayılmasını istiyor musunuz? Çoğu durumda PERSIST + MERGE yeterlidir. REMOVE cascade'i dikkatli değerlendirilmelidir.
orphanRemoval = true
orphanRemoval, cascade'den farklı bir mekanizmadır. Cascade, parent entity üzerindeki operasyonları child'lara yayarken, orphanRemoval koleksiyondan çıkarılan child'ları siler:
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Book> books;
// Senaryo 1: Bir kitabı koleksiyondan çıkarmak
author.getBooks().remove(book);
// → DELETE FROM book WHERE id = ?
// Book veritabanından SİLİNİR!
// Senaryo 2: Koleksiyonu tamamen temizlemek
author.getBooks().clear();
// → DELETE FROM book WHERE id = ? (her kitap için)
// Tüm kitaplar silinir!
// Senaryo 3: Yeni koleksiyon atamak
author.setBooks(newBookList);
// → Eski listedeki kitaplar silinir, yeni listedekiler eklenirorphanRemoval = false (Default)
orphanRemoval = false durumunda, koleksiyondan çıkarılan child silinmez — sadece foreign key NULL yapılır:
@OneToMany(mappedBy = "author", orphanRemoval = false) // default
private List<Book> books;
author.getBooks().remove(book);
// → UPDATE book SET author_id = NULL WHERE id = ?
// Book veritabanında KALIR, sadece author ilişkisi kopar!Cascade vs orphanRemoval Karşılaştırma
Bu iki mekanizma farklı zamanlarda tetiklenir:
| Senaryo | CascadeType.REMOVE | orphanRemoval = true |
|---|---|---|
| Parent silindiğinde | ✅ Child'lar silinir | ✅ Child'lar silinir |
| Child koleksiyondan çıkarıldığında | ❌ Child silinmez (FK=NULL) | ✅ Child silinir |
| Koleksiyon clear() yapıldığında | ❌ Child'lar silinmez | ✅ Tüm child'lar silinir |
| Yeni koleksiyon atandığında | ❌ Eski child'lar kalır | ✅ Eski child'lar silinir |
Analoji: CascadeType.REMOVE, evin yıkılmasıyla içindekilerin de yok olmasıdır. orphanRemoval ise bir eşyanın evden atıldığında çöpe gitmesidir. Ev yıkılınca ikisi de aynı sonucu verir, ama "eşyayı evden çıkarmak" durumunda davranış farklıdır.
Cascade Yönü
Cascade, annotation'ın tanımlandığı taraftan karşı tarafa doğru çalışır:
// Parent → Child yönünde cascade (DOĞRU)
@Entity
public class Order {
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> items;
// Order persist → OrderItem'lar da persist
// Order remove → OrderItem'lar da remove
}
// Child → Parent yönünde cascade (TEHLİKELİ!)
@Entity
public class OrderItem {
@ManyToOne(cascade = CascadeType.REMOVE) // ❌ ASLA!
private Order order;
// OrderItem silindiğinde Order da silinir!
// Diğer OrderItem'lar da cascade ile silinir!
}⚠️ Altın Kural: Cascade her zaman parent → child yönünde tanımlanır. Child → parent yönünde cascade kullanmak, beklenmeyen toplu silmelere yol açar.
Gerçek Dünya Örneği: Sipariş Sistemi
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_number", unique = true, nullable = false)
private String orderNumber;
@Enumerated(EnumType.STRING)
private OrderStatus status = OrderStatus.PENDING;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt = LocalDateTime.now();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
// ✅ Güvenli cascade — sadece PERSIST ve MERGE
// orphanRemoval — kalemden çıkarılan ürün silinir
@OneToMany(
mappedBy = "order",
cascade = {CascadeType.PERSIST, CascadeType.MERGE},
orphanRemoval = true
)
private List<OrderItem> items = new ArrayList<>();
// --- Helper Methods ---
public void addItem(Product product, int quantity, BigDecimal unitPrice) {
OrderItem item = new OrderItem(this, product, quantity, unitPrice);
items.add(item);
}
public void removeItem(OrderItem item) {
items.remove(item);
item.setOrder(null);
// orphanRemoval = true → item veritabanından silinir
}
public BigDecimal getTotalAmount() {
return items.stream()
.map(OrderItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public void clearItems() {
items.clear();
// orphanRemoval = true → tüm item'lar silinir
}
}
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false)
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false)
private Product product;
private int quantity;
@Column(name = "unit_price", nullable = false)
private BigDecimal unitPrice;
protected OrderItem() {}
public OrderItem(Order order, Product product, int quantity, BigDecimal unitPrice) {
this.order = order;
this.product = product;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
public BigDecimal getSubtotal() {
return unitPrice.multiply(BigDecimal.valueOf(quantity));
}
}
public enum OrderStatus {
PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
}Service Katmanı ile Kullanım
@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
public Order createOrder(Long customerId, List<OrderItemRequest> itemRequests) {
Customer customer = customerRepo.findById(customerId)
.orElseThrow(() -> new ResourceNotFoundException("Customer not found"));
Order order = new Order();
order.setOrderNumber(generateOrderNumber());
order.setCustomer(customer);
for (OrderItemRequest req : itemRequests) {
Product product = productRepository.findById(req.productId())
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
order.addItem(product, req.quantity(), product.getPrice());
// CascadeType.PERSIST → OrderItem otomatik persist edilir
}
return orderRepository.save(order);
// 1 INSERT order + N INSERT order_item
}
public Order updateOrderItems(Long orderId, List<OrderItemRequest> newItems) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Order not found"));
// orphanRemoval = true → eski item'lar silinir
order.clearItems();
for (OrderItemRequest req : newItems) {
Product product = productRepository.findById(req.productId())
.orElseThrow();
order.addItem(product, req.quantity(), product.getPrice());
}
return orderRepository.save(order);
// DELETE eski item'lar + INSERT yeni item'lar
}
public void removeItemFromOrder(Long orderId, Long itemId) {
Order order = orderRepository.findById(orderId)
.orElseThrow();
OrderItem item = order.getItems().stream()
.filter(i -> i.getId().equals(itemId))
.findFirst()
.orElseThrow(() -> new ResourceNotFoundException("Item not found"));
order.removeItem(item);
// orphanRemoval = true → DELETE FROM order_items WHERE id = ?
}
}Best Practices ve Anti-Pattern'ler
1. CascadeType.ALL Yerine Spesifik Cascade
// ❌ Genellikle fazla geniş
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> items;
// ✅ Sadece ihtiyacınız olanları belirtin
@OneToMany(
mappedBy = "order",
cascade = {CascadeType.PERSIST, CascadeType.MERGE},
orphanRemoval = true
)
private List<OrderItem> items;2. @ManyToMany'de REMOVE Kullanmayın
// ❌ TEHLİKELİ — Tag silindiğinde Article de silinir!
@ManyToMany(cascade = CascadeType.ALL)
private Set<Tag> tags;
// ✅ GÜVENLİ — sadece PERSIST ve MERGE
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<Tag> tags;3. orphanRemoval Sadece Aggregate Root'ta
orphanRemoval, sadece aggregate root pattern'de kullanılmalıdır — yani child entity'ler sadece parent üzerinden erişiliyorsa ve bağımsız varlıkları yoksa:
// ✅ Uygun — OrderItem bağımsız var olamaz, sadece Order üzerinden erişilir
@OneToMany(mappedBy = "order", orphanRemoval = true)
private List<OrderItem> items;
// ❌ Uygunsuz — Employee bağımsız bir varlıktır, departmandan çıksa da var olmalı
@OneToMany(mappedBy = "department", orphanRemoval = true)
private List<Employee> employees;
// Departmandan çıkarılan çalışan silinmemeli, sadece departmansız kalmalı!4. Cascade ve Performans
Cascade operasyonları entity bazlı çalışır — yani Hibernate her child için ayrı SQL statement oluşturur:
// 100 OrderItem olan bir Order silindiğinde:
// CascadeType.REMOVE ile:
// → 100 adet DELETE FROM order_items WHERE id = ?
// → 1 adet DELETE FROM orders WHERE id = ?
// Toplam: 101 SQL statement!
// Daha performanslı alternatif — bulk delete:
@Modifying
@Query("DELETE FROM OrderItem oi WHERE oi.order.id = :orderId")
void deleteByOrderId(@Param("orderId") Long orderId);
// Sonra order'ı sil
orderRepository.deleteById(orderId);
// Toplam: 2 SQL statement!Büyük koleksiyonlarda cascade yerine bulk delete kullanmak dramatik performans farkı yaratır.
5. Cascade Debugging
Cascade sorunlarını debug etmek için Hibernate SQL loglarını açın:
# application.yml
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACEBu ayarlar, her SQL statement'ı ve parametre değerlerini loglara yazar. Cascade'in ne zaman tetiklendiğini görmek için invaluable'dır.
Cascade ile Test Yazma
Cascade davranışlarını test etmek önemlidir — beklenmeyen silmeler veya persist'ler üretim ortamında veri kaybına yol açabilir:
@DataJpaTest
class CascadeTest {
@Autowired
private TestEntityManager em;
@Test
void shouldCascadePersistOrderItems() {
Order order = new Order();
order.setOrderNumber("ORD-001");
Product product = em.persist(new Product("Laptop", new BigDecimal("999.99")));
order.addItem(product, 2, product.getPrice());
em.persistAndFlush(order);
em.clear(); // First-level cache temizle
Order found = em.find(Order.class, order.getId());
assertNotNull(found);
assertEquals(1, found.getItems().size());
assertEquals("Laptop", found.getItems().get(0).getProduct().getName());
}
@Test
void shouldOrphanRemoveWhenItemRemoved() {
// Setup
Order order = createOrderWithItems(3);
em.persistAndFlush(order);
Long removedItemId = order.getItems().get(0).getId();
// Act
order.removeItem(order.getItems().get(0));
em.flush();
em.clear();
// Assert
assertNull(em.find(OrderItem.class, removedItemId)); // Silinmeli!
Order found = em.find(Order.class, order.getId());
assertEquals(2, found.getItems().size());
}
@Test
void shouldOrphanRemoveAllWhenCleared() {
Order order = createOrderWithItems(5);
em.persistAndFlush(order);
List<Long> itemIds = order.getItems().stream()
.map(OrderItem::getId).toList();
order.clearItems();
em.flush();
em.clear();
for (Long itemId : itemIds) {
assertNull(em.find(OrderItem.class, itemId)); // Hepsi silinmeli!
}
}
@Test
void shouldNotCascadeRemoveWhenNotConfigured() {
// cascade'de REMOVE yok, sadece PERSIST + MERGE
Department dept = new Department("Engineering");
Employee emp = new Employee("Ali");
dept.addEmployee(emp);
em.persistAndFlush(dept);
Long empId = emp.getId();
// Department silindiğinde employee silinmemeli
// (cascade REMOVE olmadığı için FK constraint hatası verebilir
// — bu durumda önce employee'yi detach etmek gerekir)
}
}Cascade Karar Ağacı
Hangi cascade tipini kullanacağınıza karar verirken şu soruları sorun:
Child entity, parent olmadan var olabilir mi?
- Evet → cascade REMOVE kullanma, orphanRemoval kullanma - Hayır → cascade REMOVE ve/veya orphanRemoval uygun
Child entity başka parent'larla paylaşılıyor mu?
- Evet → cascade REMOVE asla kullanma (@ManyToMany durumu) - Hayır → cascade REMOVE güvenle kullanılabilir
Koleksiyondan çıkarılan child silinmeli mi?
- Evet → orphanRemoval = true - Hayır → orphanRemoval = false (default)
Parent kaydedilirken child'lar da otomatik kaydedilsin mi?
- Evet → cascade PERSIST ekle - Hayır → child'ları ayrı persist et
Performans kritik mi ve koleksiyon büyük mü?
- Evet → cascade yerine bulk operasyonlar kullan - Hayır → cascade yeterli
Özet
CascadeType.PERSIST: Parent kaydedildiğinde child'lar da kaydedilir — yeni entity'ler için
CascadeType.MERGE: Parent güncellendiğinde child'lar da güncellenir — detached entity'ler için
CascadeType.REMOVE: Parent silindiğinde child'lar da silinir — sadece aggregate ilişkilerde kullanın
orphanRemoval = true: Koleksiyondan çıkarılan child veritabanından silinir — aggregate root pattern'de
CascadeType.ALL dikkatli kullanın — çoğunlukla
PERSIST + MERGEyeterlidir@ManyToMany'de REMOVE cascade asla kullanılmamalı — paylaşılan entity'ler silinir
Cascade yönü parent → child olmalıdır — child → parent cascade tehlikelidir
Büyük koleksiyonlarda bulk delete cascade'den çok daha performanslıdır
AI Asistan
Sorularını yanıtlamaya hazır