연관매핑이란?
Entity와 연관 관계를 매핑해두고, 해당 Entity와 관련된 Entity를 사용하는 것.
1.1 일대일 단방향 매핑
cart - member
- 회원 한명 당 하나의 카트를 가지게 설정하려고 함.
- 카트에 회원의 아이디를 저장하는 칼럼을 만든다.
-> Cart
@Entity
@Table(name = "cart")
@Getter @Setter
@ToString
public class Cart extends BaseEntity {
@Id
@Column(name = "cart_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name="member_id")
private Member member;
public static Cart createCart(Member member){
Cart cart = new Cart();
cart.setMember(member);
return cart;
}
}
@OneToOne
: 회원 엔티티와 일대일 매핑함@JoinColumn
: name 이름인 외래키로 지장할 칼럼을 지정
이렇게 카트와 회원을 연관매핑을 함.
이제 테스트 코드를 작성하기 위해 Repository 작성 후 테스트 코드를 작성해보자
-> CartTest
@Test
@DisplayName("장바구니 회원 엔티티 매핑 조회 테스트")
public void findCartAndMemberTest(){
Member member = createMember();
memberRepository.save(member);
Cart cart = new Cart();
cart.setMember(member);
cartRepository.save(cart);
em.flush();
em.clear();
Cart savedCart = cartRepository.findById(cart.getId())
.orElseThrow(EntityNotFoundException::new);
assertEquals(savedCart.getMember().getId(), member.getId());
}
- 위 테스트는 cart와 member가 연관매핑되어 있기 때문에, cart 조회 시 쿼리문이 연관 매핑된 member 또한 함께 조회하는 지 알아보려는 테스트이다.
- 쿼리는
flush()
시 DB에 반영되기 때문에 save가 제대로 되는지 실행한다. - JPA는 조회 요청이 오면 영속성 컨텍스트의 1차캐시에서 먼저 조회를 하기때문에, DB에서 조회를 위해 영속성 컨텍스트를 초기화한다.
orElseThrow()
: findById를 통한 값이 없을 경우 다음과 같은 익셉션을 전달한다.- 카트에 저장된 회원과 저장하려고 했던 회원이 같은지 검사한다.
- 위 처럼 한번에 관련된 모든 Entity를 조회하는 방식을 FetchType.EAGER 방식이라고 한다.
1.2 다대일 단방향 매핑
카트, 카트에 담긴 상품, 상품 간의 연관 매핑을 해보자.
- 하나에 Cart에는 여러개의 Cart_Item을 담을 수 있다.
- 하나의 Item에는 여러개의 Cart_Item에 들어갈 수 있다.
-> CartItem
@Id
@GeneratedValue
@Column(name = "cart_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="cart_id")
private Cart cart;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item;
private int count;
- CartItem에 외래키로 cart_id와 item_id 칼럼이 추가된다.
1.3 다대일/일대다 양방향
member, orders, order_item 사이의 연관관계를 매핑해보겠다.
- 하나의 member에는 여러개의 order가 들어갈 수 있다.
- 하나의 order에는 여러개의 order_item이 들어 갈 수 있다.
- 하나의 item은 여러 order_ite에 들어갈 수 있다.
-> order
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
private LocalDateTime orderDate; //주문일
...
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL
, orphanRemoval = true, fetch = FetchType.LAZY)
private List<OrderItem> orderItems = new ArrayList<>();
-> orderItem
@Id @GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
- 양방향이란 단방향이 서로 매핑된 것을 말한다.
- 현재
order
에선@ManyToOne
으로order_item
을 단방향 매핑했고,order_item
에선@OneToMany
를 이용해서order
와 매핑했다.
테이블은 외래키 하나로 Join을 사용해서 양방향 조회가 가능하다.
( Join : 2개의 테이블을 엮어서 원하는 데이터를 추출 )
( JOIN 관련 )
( order_item의 외래키(order_id) 하나로 order_item과 order 둘 다 알 수 있다. )
또한, 양방향 매핑에선 연관 관계 주인
을 설정해야한다.
( 연관 관계 주인은 두 엔티티에 관해 제어[삭제, 추가, 수정,,,]이 가능하고, 주인이 아닌 엔티티는 읽기만 가능 )
연관 관계 주인 설정하기
- 연관 관계의 주인은 외래키가 있는 곳으로 설정
- 연관 관계의 주인이 외래키를 관리
- 주인이 아닌 쪽은 연관 관계 매핑 시 mappedBy를 통해 주인을 설정
- 주인이 아닌 쪽은 읽기만 가능
따라서 주인이 아닌 쪽(order)에서 orderItem과 매핑시 mappedBy를 설정하면 끝!
-> Order
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<>();
1.4 다대다 매핑
RDBMS에선 다대다 매핑을 표현할 수 없다.
따라서 다대다를 일대다 혹은 다대일 관계로 풀어낸다.
다대다 매핑을 사용하지 않는 이유
- 연결 테이블에는 칼럼을 추가 할 수 없다.
- 중간 테이블이 있는데 이를 이용하면 어떤 쿼리문이 실행될지 예측 쉽지 않음.
2.1 영속성 전이
영속성 전이 (cascade)
Entity 상태를 변경 시 연관된 Entity 상태 또한 변경 시키는 옵션
CASCADE 종류
- 단일 Entity에 완전히 종속되거나 부모 Entity와 자식 Entity의 life-cycle이 유사할 때 사용을 추천
Order의 자식인 orderItem에게 영속성 전이를 해보자
-> Order
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
이제 정상적으로 영속성 전이가 되는지 테스트 코드를 작성하자
- Order와 OrderItem 3개를 생성 후, Order에 OrderItem 3개를 넣는다.
- ORder를 영속성 컨텍스트에 저장 후 flush한다.
- Order를 DB에서 조회했을때 OrderItem 3개도 같이 조회되는지 검사한다.
2.2 고아 객체 제거
고아 객체란?
부모 엔티티와 관계가 끊어진 자식 엔티티
고악 객체 제거시 주의사항
참조하는 곳이 하나일때만 사용
@OneToOne
,@OneToMany
에서 사용
부모 엔티티(order)에서 자식 엔티티(orderItem) 어노테이션에 orphanRemoval
옵션을 설정
-> Order
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> orderItems = new ArrayList<>();
이제 테스트 코드에서 order 객체의 orderItems 필드의 첫번째 인덱스를 제거한다.
그렇게 되면 order_item은 order와의 연관관계가 제거 되어서 고아 객체가 되고 삭제되게 된다.
order.getOrderItems().remove(0);
을 하게 되면
orderItems의 0번째만 사라지는 것인가? orderItems가 다 사라지는 것인가?
3. 지연 로딩
fetch타입에는 즉시로딩(EAGER)
,지연 로딩(Lazy)
두 가지가 있다.
- 즉시 로딩이 기본으로 설정되어 있다.
- 즉시 로딩은 원치 않는 Entity도 한번에 로딩하기 때문에 성능 문제가 발생 할 수 있으므로 Lazy를 추천하다.
연관매핑을 지연 로딩으로 설정 후, 저번과 같은 테스트 코드를 실행해보자
orderItem.getOrder().getClass() 시, Order 객체가 아닌 프록시 객체가 반환되는 것을 볼 수 있다.
HibernateProxy는 실제 사용 시점에만 쿼리문을 실행한다.
4. Auditing
등록시간, 수정시간, 등록자, 수정자를 이용해서 버그를 수정하거나 변경된 대상을 찾을 수 있다.
Spring Data JPA에선 저장 또는 수정 시 자동으로 위를 바꿔주는 기능을 제공. 이를 Auditing
이라고 함.
Auditing으로 공통 속성 공통화하기
AuditorAware
을 구현하는 구현체 생성 후, 정보를 추적하는 메서드 구현하기- Auditing을 구현할 Config파일 생성 후, AuditorAware 빈 등록
- 등록일, 수정일만 가진 BaseTimeEntity 생성
- 등록자, 수정자까지 가진 BaseTimeEntity를 상속받는 BaseTime 객체 생성
- Auditing 기능을 사용할 엔티티에 상속 추가하기
1
-> AuditorAwareImpl
public class AuditorAwareImpl implements AuditorAware<String> {
// 현재 로그인한 사용자 정보를 조회하여 사용자의 이름을 등록자와 수정자로 지정
@Override
public Optional<String> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String userId = "";
if(authentication != null){
userId = authentication.getName();
}
return Optional.of(userId);
}
}
2
-> AuditConfig
@Configuration
@EnableJpaAuditing // JPA의 Auditing 기능 활성화
public class AuditConfig {
@Bean
public AuditorAware<String> auditorProvider(){ // AuditorAware을 빈으로 등록
return new AuditorAwareImpl();
}
}
3
-> BaseTimeEntity
@EntityListeners(value = {AuditingEntityListener.class}) // Auditing 적용하기 위해
@MappedSuperclass // 공통 매핑 정보가 필요할때 (상속받은 자식 클래스에 매핑 정보만 제공)
@Getter @Setter
public abstract class BaseTimeEntity {
@CreatedDate // 엔티티가 생성되고 저장될때 시간을 자동으로 저장
@Column(updatable = false)
private LocalDateTime regTime;
@LastModifiedDate // 엔티티 값을 변경할 때 시간을 자동으로 저장
private LocalDateTime updateTime;
}
4
-> BaseTime
@EntityListeners(value={AuditingEntityListener.class})
@MappedSuperclass
@Getter
public abstract class BaseEntity extends BaseTimeEntity{
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String modifiedBy;
}
5
Member 엔티티에 Auditing 기능을 사용해보자
-> Member
@Entity
@Table(name="member")
@Getter @Setter
@ToString
public class Member extends BaseEntity{
...
이제 테스트 코드를 작성하고 member 엔티티가 저장 혹은 수정 시, 자동으로 공통 속성이 바뀌는지 확인해보자
@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());
}
테스트 코드 결과
적상적으로 통과하는 것을 볼수있다.
'🛠 백엔드 > 쇼핑몰 클론코딩' 카테고리의 다른 글
[2] JPA (0) | 2023.01.08 |
---|---|
[7] Order (0) | 2023.01.07 |
[6] Item 등록 및 조회 (0) | 2023.01.04 |
[4] Spring Security (1) | 2022.12.30 |
[3] Thymeleaf (0) | 2022.12.25 |