
cascade: "폭포, 폭포처럼 흐르다" 라는 사전적 의미를 바탕으로 Entity의 상태를 변경할 때 해당 Entity와 연관된 Entity의 상태 변화를 전파시키는 옵션.
a@email.com에서b@email.com로 변경하면 그와 연결된item등을b로 전파해야한다.
- 부모는 One 에 해당하는 Entity
- 자식은 Many 에 해당하는 Entity
- 부모 Entity 가 존재해야 자식 Entity 가 부모를 참조하며 생성될 수 있음
CASCADE 종류
Cascade 설명 PERSIST 부모가 영속화될 때 자식도 영속화 MERGE 부모가 병합될 때 자식도 병합 REMOVE 부모가 삭제될 때 자식도 삭제 REFRESH 부모가 refresh 되면 자식도 refresh DETACH 부모가 detach 되면 자식도 detach ⚡ALL 부모의 상태 변화를 자식에게 모두 전이⚡
package com.shop.repository;
import com.shop.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderRepository extends JpaRepository<Order, Long> {
}
...java
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL) // order_item 테이블의 order 필드에 매핑
private List<OrderItem> orderItems = new ArrayList<>();
...
package com.shop.entity;
import com.shop.constant.ItemSellStatus;
import com.shop.repository.ItemRepository;
import com.shop.repository.OrderRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.PersistenceContext;
import org.aspectj.weaver.ast.Or;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class OrderTest {
@Autowired
OrderRepository orderRepository;
@Autowired
ItemRepository itemRepository;
@PersistenceContext
EntityManager em;
public Item createItem(){
Item item = new Item();
item.setItemNm("테스트 상품");
item.setPrice(10000);
item.setItemDetail("상세 설명");
item.setItemSellStatus(ItemSellStatus.SELL);
item.setRegTime(LocalDateTime.now());
item.setUpdateTime(LocalDateTime.now());
return item;
}
// 부모인 Order Entity 가 저장될 때, 자식인 OrderItem 또한 저장되는 영속성 전이
// Order 객체 저장 시, 참조되는 Order_Item 객체 저장
@Test
@DisplayName("영속성 전이 테스트")
public void cascadeTest(){
Order order = new Order();
for (int i = 0; i < 3; i++) {
Item item = this.createItem();
itemRepository.save(item);
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setCount(10);
orderItem.setOrderPrice(1000);
orderItem.setOrder(order); // 하나의 order에 3개의 item
order.getOrderItems().add(orderItem); // List니까 add
}
orderRepository.saveAndFlush(order); // 주문 추가 flush()
em.clear();;
Order savedOrder = orderRepository.findById(order.getId()).orElseThrow(EntityNotFoundException::new);
assertEquals(3, savedOrder.getOrderItems().size());
}
}

👉 3개의 Item 생성

👉 orders 생성

👉 order_item을 넣지도 않았는데 알아서 insert ? 👉 영속성 전이. 부하가 부모걸 읽어서 반영.
A라는 상품을 3개 주문 👉 3개의 주문서가 "A라는 상품을 3개 주문" 에 의해 3개의 주문서가 알아서 생성보통 우리가 주문 할 때 A상품 3개를 주문하면 합쳐져서 주문되지만
지금 한 테스트는 영속성전이를 이해하기 위해 for문으로 같은 객체를 3개 주문해 주문서 3개를 만든 것
부모 엔티티와 연관 관계가 끊어진 자식 엔티티 👉 자식 엔티티는 남아 있으니까 우리가 일일히 확인하며 삭제 할 수 없기 때문에
알아서 제거 된다. 단, 자식의 엔티티를다른 엔티티가 참조하고 있다면 제거 하지 않는다.
orphanRemoval = true/false사용@OneToMany(mappedBy = "order", cascade = CascadeType.ALL , orphanRemoval = true)CASCADE.REMOVE vs 고아객체 제거
CASCADE.REMOVE- 부모Entity가 삭제될 때 같이 삭제되는 것
고아객체 제거- 부모Entity와의 연관관계가 끊어질 때 삭제되는 것
...
@Autowired
MemberRepository memberRepository;
...
// 부모 Entity 인 Order 객체 생성
public Order createOrder(){
Order order = new Order();
for (int i = 0; i < 3; i++) {
Item item = this.createItem();
itemRepository.save(item);
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setCount(10);
orderItem.setOrderPrice(1000);
orderItem.setOrder(order); // 하나의 order에 3개의 item
order.getOrderItems().add(orderItem); // List니까 add
}
Member member = new Member();
memberRepository.save(member);
order.setMember(member);
orderRepository.save(order);
return order;
}
@Test
@DisplayName("고아객체 제거 테스트")
/*
Order 객체가 관리하는 OrderItem 리스트에서 0번째 요소의 id를 추출
0번째 요소를 제거한 뒤, OrderItem 조회 (id 이용)
조회 결과 Optional.empty() 라면 제대로 제거된 것이므로 테스트 통과
*/
public void orphanRemovalTest(){
Order order = this.createOrder();
order.getOrderItems().remove(0); // 연관 관계 끊기
em.flush();
}
}
Hibernate:
delete
from
order_item
where
order_item_id=?
👉 고아 객체 제거
엔티티를 조회할 때 연관된 엔티티도 함께 조회하는
Fetch EAGER타입. 기본 default 값이다
package com.shop.repository;
import com.shop.entity.OrderItem;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
}
...
@Autowired
OrderItemRepository orderItemRepository;
...
@Test
@DisplayName("즉시 로딩 테스트")
public void lazyLoadingTest(){
Order order = this.createOrder();
Long orderItemId = order.getOrderItems().get(0).getId();
em.flush();
em.clear();
OrderItem orderItem = orderItemRepository.findById(orderItemId)
.orElseThrow(EntityNotFoundException::new);
System.out.println("Order class : " + orderItem.getOrder().getClass());
System.out.println("============================");
orderItem.getOrder().getOrderDate();
System.out.println(orderItem.getOrder().getOrderDate());
System.out.println("============================");
}
Hibernate:
select
oi1_0.order_item_id,
oi1_0.count,
i1_0.item_id,
i1_0.item_detail,
i1_0.item_nm,
i1_0.item_sell_status,
i1_0.price,
i1_0.reg_time,
i1_0.stock_number,
i1_0.update_time,
o1_0.order_id,
m1_0.member_id,
m1_0.address,
m1_0.email,
m1_0.name,
m1_0.password,
m1_0.role,
m1_0.tel_number,
o1_0.order_date,
o1_0.order_status,
o1_0.reg_time,
o1_0.update_time,
oi1_0.order_price,
oi1_0.reg_time,
oi1_0.update_time
from
order_item oi1_0
left join
item i1_0
on i1_0.item_id=oi1_0.item_id
left join
orders o1_0
on o1_0.order_id=oi1_0.order_id
left join
member m1_0
on m1_0.member_id=o1_0.member_id
where
oi1_0.order_item_id=?
Order class : class com.shop.entity.Order
============================
null
============================
👉 즉시로딩은 로딩을 바로바로 하니까 join을 3개나 불렀다. 이러니까 현업에선 사용하지 않는다
연관된 엔티티는 사용할 때 조회하는
Fetch LAZY타입
- 실제 비즈니스 구현 시 매핑되는 엔티티의 개수 ↑
- 사용하지 않는 데이터도 한꺼번에 조회하므로 성능 ↓
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id") // 외래키
private Item item;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id") // 외래키
...
@Test
@DisplayName("지연 로딩 테스트")
public void lazyLoadingTest(){
Order order = this.createOrder();
Long orderItemId = order.getOrderItems().get(0).getId();
em.flush();
em.clear();
OrderItem orderItem = orderItemRepository.findById(orderItemId)
.orElseThrow(EntityNotFoundException::new);
System.out.println("Order class : " + orderItem.getOrder().getClass());
System.out.println("============================");
orderItem.getOrder().getOrderDate();
System.out.println("============================");
}
👉 이름만 변경
Hibernate:
select
oi1_0.order_item_id,
oi1_0.count,
oi1_0.item_id,
oi1_0.order_id,
oi1_0.order_price,
oi1_0.reg_time,
oi1_0.update_time
from
order_item oi1_0
where
oi1_0.order_item_id=?
Order class : class com.shop.entity.Order$HibernateProxy$PAJtIzFU
👉 많이 짧아 졌다. 그 이유는?
❓ Order class : class com.shop.entity.Order$HibernateProxy$0COhfF1V 로 HibernateProxy 가 나왔다
엔티티 대신 위 프록시에 객체를 넣어뒀다가(지연) 프록시 객체는 실제 사용 시점에 HibernateProxy 에서 받아와 조회 쿼리문이 실행한다.
============================
Hibernate:
select
o1_0.order_id,
m1_0.member_id,
m1_0.address,
m1_0.email,
m1_0.name,
m1_0.password,
m1_0.role,
m1_0.tel_number,
o1_0.order_date,
o1_0.order_status,
o1_0.reg_time,
o1_0.update_time
from
orders o1_0
left join
member m1_0
on m1_0.member_id=o1_0.member_id
where
o1_0.order_id=?
============================
👉 rderItem.getOrder().getOrderDate() 수행 결과
👉실제 사용 시점에 select 쿼리문이 수행

👉 Cart

👉 CartItem

👉 item

👉 Order
엔티티에 공통으로 들어가는 멤버변수들
(등록시간, 수정시간, 등록자, 수정자 등)을추상클래스로 만들고, 해당추상 클래스를 상속받아 엔티티에 공통적인 기능을 수행하도록 하며 엔티티의 생성과 수정을 감시(Audit)하는 기법. 👉 코드를 깔끔하게 만드는 리팩토링.
Spirng JPA Audition기능 제공 👉 엔티티가 저장 또는 수정될 때 자동으로 동작한다.Auditing의 필요성
- 여러 엔티티에 공통된 멤버변수가 존재할 때 하나의 추상클래스로 통합하여 구현할 수 있음
- 등록시간, 수정시간, 등록자, 수정자 등의 엔티티 상태 변경에 대한 정보를 기록할 수 있음
- 기록을 바탕으로 버그 문의, 업데이트 변경 대상 조회 등등 여러 상황에서 사용됨
package com.shop.config;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Optional;
// 로그인한 사용자를 등록자 및 수정자로 지정하기 위해 AuditorAware 인터페이스를 구현
public class AuditorAwareImpl implements AuditorAware<String > {
// Optional 클래스는 아래와 같은 value 값을 저장하기 때문에
// 값이 "null이더라도 바로 NullPointError"가 발생하지 않으며, 클래스이기 때문에 각종 메소드를 제공
@Override
public Optional<String> getCurrentAuditor(){ // getCurrentAuditor 👉 현재 로그인된 상태
// 현재 로그인한 사용자의 정보를 추출
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String userId = "";
if(authentication != null){
//현재 로그인 한 사용자의 정보를 조회하여 사용자의 이름을 등록자와 수정자로 지정
userId = authentication.getName();
}
return Optional.of(userId);
}
}
package com.shop.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing // Auditing 기능 활성화
public class AuditConfig {
@Bean
public AuditorAware<String> auditorProvider(){
return new AuditorAwareImpl(); // AuditorAware<String> 구현체를 Bean 객체에 등록
}
}
package com.shop.entity;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
// 등록자 및 수정자 제외한 시간관련 Auditing 기능 수행 Entity
@EntityListeners(value = {AuditingEntityListener.class}) // Auditing을 하기 위해 엔티티 리스너 추가
@MappedSuperclass // 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공
@Getter
@Setter
public abstract class BaseTimeEntity {
@CreatedDate // 생성 시 자동 저장
@Column(updatable = false) // 생성시에만 업데이트 한다
private LocalDateTime regTime;
@LastModifiedDate // 변경 시 자동 저장
private LocalDateTime updateTime;
}
package com.shop.entity;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.hibernate.sql.Update;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.jdbc.object.UpdatableSqlQuery;
// BaseTimeEntity 상속를 상속 받아
// 등록자 및 수정자, 등록 및 수정 시간 4가지 모두 갖는 Entity
@EntityListeners(value = {AuditingEntityListener.class}) // Auditing을 하기 위해 엔티티 리스너 추가
@MappedSuperclass // 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공
@Getter
public abstract class BaseEntity extends BaseTimeEntity{
@CreatedBy // 등록자
@Column(updatable = false)
private String createdBy;
@LastModifiedBy // 수정자
private String modifiedBy;
}
@Entity
@Table (name = "member")
@Getter
@Setter
@ToString
public class Member extends BaseEntity{
@Id
...
package com.shop.entity;
import com.shop.repository.MemberRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class MemberTest {
@Autowired
MemberRepository memberRepository;
@PersistenceContext
EntityManager em;
@Test
@DisplayName("Auditing 테스트")
@WithMockUser(username = "gildong", roles = "USER")
public void auditingTest(){
Member newMember = new Member();
memberRepository.save(newMember);
em.flush();
em.clear();
Member member = memberRepository.findById(newMember.getId())
.orElseThrow(EntityNotFoundException::new);
System.out.println("register time : " + member.getRegTime());
System.out.println("update time : " + member.getUpdateTime());
System.out.println("create member : " + member.getCreatedBy());
System.out.println("modify member : " + member.getModifiedBy());
}
}

👉 잘 감시하고 있다
@EntityListeners기능을 이용해서Entity등록 및 수정 시에Auditing기능을 수행하는 구조이다.
@MappedSuperclass- 해당 클래스를 상속 받는Entity들이 공통되는 필드를 사용할 수 있도록 부모 클래스에 지정1. AuditorAwareImpl
AuditorAware인터페이스를 구현한 클래스 생성2. AuditConfig
@Configuration,@EnableJpaAuditing을 통해서JPA Auditing기능 활성화
동시에AuditorAware인터페이스 구현체@Bean등록3. BaseEntity
Auditing활성화 상태에서는AuditorAware구현체@Bean을 자동으로 찾아서AuditingEntityListener와 매핑4. Member
이제 원하는 엔티티에
@EntityListeners,@MappedSuperclass지정5. 엔티티 등록 및 수정 시에 관련된 해당 필드
insert및update됨




👉 미리 상속받아 사용하자