엔티티들은 대부분 다른 엔티티와 연관 관계를 맺고 있습니다. JPA에서는 엔티티에 연관 관계를 매핑해두고 필요할 때 해당 엔티티와 연관된 엔티티를 사용하여 좀 더 객체지행적으로 프로그래밍 할 수 있도록 도와줍니다. 연관 관꼐 매핑의 기초를 알아보겠습니다.
일대일(1:1) @OneToOne
일대다(1:N) @OneToMany
*다대일(N:1) @ManyToOne
다대다(N:M) @ManyToMany
엔티티 매핑의 방형성은 단방향/ 양방향
CartTest
package com.shop.shop.entity;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.web.bind.annotation.GetMapping;
import javax.persistence.*;
@Entity
@Table(name = "cart")
@Getter
@Setter
@ToString
public class Cart {
@Id
@Column(name = "cart_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToOne
@JoinColumn(name = "member_id")
private Member member;
}
@OneToOne 어노테이션을 이용해 회원 엔티티와 일대일로 매핑을합니다.
@JoinColumn 어노테이션을 이용해 매핑할 외래키를 지정합니다. name속성에는 매핑할 외래키릐 이름을 설정합니다. @JoinColumn 의 name을 명시하지 않으면 JPA가 알아서 ID를 찾지만 컬럼명이 원하는 대로 생성되지 않을수 있기 때문에 직접 지정하겠습니다.
회원엔티티에는 장바구니엔티티 관련소스가 전혀없고 장바구니엔티티가 일방적으로 회원 엔티티를 참조하고 있습니다.장바구니와 회원은 일대일로 매핑되어 있으며, 장바구니 엔티티가 회원엔티티를 참조하는 일대일 단방향 매핑입니다.
package com.shop.shop.repository;
import com.shop.shop.dto.MemberFormDto;
import com.shop.shop.entity.Cart;
import com.shop.shop.entity.Member;
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.crypto.password.PasswordEncoder;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceContext;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
@TestPropertySource(locations= "classpath:application-test.properties")
class CartTest {
@Autowired
CartRepository cartRepository;
@Autowired
MemberRepository memberRepository;
@Autowired
PasswordEncoder passwordEncoder;
@PersistenceContext
EntityManager em;
public Member createMember(){
MemberFormDto memberFormDto = new MemberFormDto();
memberFormDto.setEmail("test@email.com");
memberFormDto.setName("홍길동");
memberFormDto.setAddress("가산");
memberFormDto.setPassword("1234");
return Member.createMember(memberFormDto,passwordEncoder);
}
@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());
}
}
package com.shop.shop.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Setter
@Getter
@Table(name = "cart_item")
public class CatrItem {
@Id
@GeneratedValue
@Column(name = "cart_item_id")
private Long id;
@ManyToOne //cartItem 입장에선 Many
@JoinColumn(name = "cart_id") //매핑이 되어진다.
private Cart cart;
@ManyToOne
@JoinColumn(name = "item_id")
private Item item;
private int count;
}
하나의 장바구니에는 여러 개의 상품을 담을 수 있으므로 @ManyToOne 어노테이션을 이용하여 다대일 관계로 매핑합니다.
장바구니에 담을 상품의 정보를 알아야 하므로, 상품 엔티티를 매핑합니다.
하나의 상품은 여러 장바구니의 장바구니 상품으로 담길수 있으므로 마찬가지로 @ManyToOne 어노테이션을 이용하여 다대일 관계로 매핑합니다.
같은 상품을 장바구니에 몇개 담을 지 저장합니다.
@JoinColumn 어노테이션의 name 으로 설장한 값이 foreign key로 추가되며 @JoinColumn 어노테이션을 사용하는 엔티티 컬럼명이 추가된다고 생각하면 됩니다.
package com.shop.shop.constant;
public enum OrderStatus {
ORDER, CANCEL
}
package com.shop.shop.entity;
import com.shop.shop.constant.OrderStatus;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {
@Id
@Column(name = "order_id")
private Long id;
@ManyToOne//주문만 알수있음 멤버도 내용을 알려면 양방향이 되어야한다.
@JoinColumn(name = "member_id")
private Member member;
private LocalDateTime orderDate; //주문일
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus; //주문상태
@OneToMany(mappedBy = "order")//주인이 아닌쪽은 연관관계매핑시 mappyBy속성의 값으로 연관관계의 주인설정(변수명)
private List<OrderItem> orderItems = new ArrayList<>();
//객체선언하면 연관관계 매핑해주어야한다.
private LocalDateTime regTime;
private LocalDateTime updateTime;
정렬할때 사용하는 'order'키워드가 있기 때문에Order 엔티티에 매핑되는 테이블로 "orders"를 지정합니다. 한명의 회원은 여러번 주문을 할 수 있으므로 주문 엔티티 기준에서 다대일 단방향 매핑을 합니다.
import java.time.LocalDateTime;
@Entity
@Getter
@Setter
public class OrderItem {
@Id
@GeneratedValue
@Column(name = "order_item_id")
private LocalDateTime id;
@ManyToOne//어떤 상품인지 알수 있음
@JoinColumn(name = "item_id")
private Item item;
@ManyToOne//어떤주문인지 알수있음
@JoinColumn(name = "order_id")
private Order order;
private int orderPrice;
private int count;
private LocalDateTime regTime;
private LocalDateTime updateTime;
}
하나의 상품은 여러 주문 상품으로 들어갈 수 있으므로 주문 상품 기준으로 다대일 단방향 매핑을 설정합니다.
한 번의 주문에 여러개이 상품을 주문 할수 있으므로 주문 상품 엔티티와 주문 엔티티를 다대일 단방향 패일을 먼저 설정합니다.
테이블은 외래키하나로 양방향조회가 가능합니다.
엔티티는 테이블과 달리 둘중 누가 외래키를 관리 할지 정해야 합니다.
- 연관관계의 주인은 외래키가 있는 곳으로 설정
- 연관관계의 주인이 외래키를 관리(등록,수정,삭제_
- 주인이 아닌 쪽은 연관 관계 매핑 시 mappedBy 속성의 값으로 연관 관계의 주인을 설정
- 주인이 아닌 쪽은 읽기만 가능
무조건적인 양방향매핑보다는 단방향매핑으로 설계 후 나중에 필요한 경우 양방향 매핑을 추가하는 것을 권합니다.
일대일, 다대일로 매핑할 경우 기본 전략인 즉시로딩을 통해 엔티티를 함께 가지고 오며 작성하고 있는 비즈니스 로직에서 사용하지 않을 데이터까지도 한꺼번에 가져오기 때문에 실무에서 사용하기 힘듭니다 , 즉시 로딩을 사용하는 대신에 지연로딩 방식을 사용해야 합니다.
FetchType.LAZY 방식으로 설정해주세요.
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Getter
@Setter
public class 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;
private int orderPrice;
private int count;
private LocalDateTime regTime;
private LocalDateTime updateTime;
}
@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("========================");
}
package com.shop.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;
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);
}
}
package com.shop.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
public class AuditConfig {
@Bean
public AuditorAware auditorProvider(){
return new AuditorAwareImpl();
}
}
package com.shop.shop.entity;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@EntityListeners(value = {AuditingEntityListener.class})
@MappedSuperclass
@Getter
@Setter
public abstract class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime regTime;
@LastModifiedDate
private LocalDateTime updateTime;
}
package com.shop.shop.entity;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@EntityListeners(value = {AuditingEntityListener.class})
@MappedSuperclass
@Getter
public abstract class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createBy;
@LastModifiedBy
private String modifiedBy;
}
package com.shop.shop.entity;
import com.shop.shop.constant.Role;
import com.shop.shop.dto.MemberFormDto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.persistence.*;
@Entity
@Table(name = "member")
@Getter
@Setter
@ToString
public class Member extends BaseEntity{
@Id
@Column(name ="member_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
@Column(unique = true)
private String email;
private String password;
private String address;
@Enumerated(EnumType.STRING)
private Role role;
public static Member createMember(MemberFormDto memberFormDto, PasswordEncoder passwordEncoder){
Member member = new Member();
member.setName(memberFormDto.getName());
member.setEmail(memberFormDto.getEmail());
member.setAddress(memberFormDto.getAddress());
String password = passwordEncoder.encode(memberFormDto.getPassword());
member.setPassword(password);
member.setRole(Role.ADMIN);
return member;
}
}
package com.shop.shop.entity;
import com.shop.shop.repository.MemberRepository;
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 javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceContext;
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 = "gilgong", 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.getCreateBy());
System.out.println("modify member:" + member.getModifiedBy());
}
}