[어드민 페이지 만들기] 연관관계 설정하기
연관관계 설정하기
지난 시간 동안 ERD 설계, Table 생성, Entity 생성, Repository 생성, JUnit 테스트 코드 작성이 끝났다. 이제 연관관계 설정을 할 차례이다.
지난 시간에 설계한 테이블간 연관관계도를 보면서 진행해보자. 관련 글은 아래 링크를 통해 확인할 수 있다.
[어드민 페이지 만들기] ERD 설계 & Table 생성 & Entity 생성
1. User : OrderGroup = 1 : N
User 엔티티
@Data // 기본 생성자와 변수에 대해 get, set 메서드 생성
@AllArgsConstructor // 모든 매개변수를 가진 생성자도 추가
@NoArgsConstructor // 파라미터가 없는 생성자 생성
@Entity // Entity임을 선언 = table
@ToString(exclude = {"orderGroup"}) // OneToMany의 경우 상호참조시 오버플로우가 일어나므로 exclude 시켜주기
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String account;
private String password;
private UserStatus status;
private String email;
private String phoneNumber;
private LocalDateTime registeredAt;
private LocalDateTime unregisteredAt;
private LocalDateTime createdAt;
private String createdBy;
private LocalDateTime updatedAt;
private String updatedBy;
// User : OrderGroup = 1 : N
@OneToMany(fetch = FetchType.LAZY, mappedBy = "user") // user라는 멤버변수에 매칭시킴
private List<OrderGroup> orderGroupList; // OneToMany이므로 list 타입으로 바꿔줘야함
}
User 엔티티를 보면 현재는 OrderGroup에 대해서 어떠한 속성도 가지고 있지 않다. 하지만 User와 OrderGroup의 연관관계는 1:N의 관계를 가지고 있다. User의 입장에서는 OrderGroup을 여러 개 가지고 있을 수 있다.
@OneToMany인 경우 fetch 타입을 걸어줘야 하고, LAZY(지연 로딩)으로 걸어준다. 또한 상호참조하는 경우 롬복이 toString에 대해 서로 계속해서 찍기 때문에 오버플로우가 일어난다. 따라서 @OneToMany라든지 join을 걸어준 변수에 대해서 @ToString에 대해 exclude를 시켜줘야한다.
또한, orderGroup은 @OneToMany의 Many 부분이므로 private OrderGroup orderGroup;이 아니고, private List<OrderGroup> orderGroupList;와 같이 리스트 타입으로 지정해야 한다.
OrderGroup 엔티티
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@ToString(exclude = {"user"})
public class OrderGroup {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String status;
private OrderType orderType; // 주문의 형태 - 일괄 / 개별
private String revAddress;
private String revName;
private PaymentType paymentType; // 카드 / 현금
private BigDecimal totalPrice;
private Integer totalQuantity;
private LocalDateTime orderAt;
private LocalDateTime arrivalDate;
private LocalDateTime createdAt;
private String createdBy;
private LocalDateTime updatedAt;
private String updatedBy;
private Long userId;
// OrderGroup : User = N : 1
@ManyToOne
private User user; // 'user'는 User 클래스에 있는 mappedBy에 있는 변수명과 일치해야함
}
OrderGroup 엔티티에서 선언한 'user'는 User 클래스에 있는 mappedBy에 있는 변수명과 일치해야한다.
연관관계를 설정했으면 이제 정상적으로 동작하는지 확인해보자. User의 입장에서 확인해보겠다.
UserRepositoryTest
public class UserRepositoryTest extends StudyApplicationTests {
// Dependency Injection (DI)
@Autowired
private UserRepository userRepository;
@Test
@Transactional
public void read() {
User user = userRepository.findFirstByPhoneNumberOrderByIdDesc("010-1111-2222");
if(user != null) {
user.getOrderGroupList().stream().forEach(orderGroup -> {
System.out.println("수령인: " + orderGroup.getRevName());
System.out.println("수령지: " + orderGroup.getRevAddress());
System.out.println("총금액: " + orderGroup.getTotalPrice());
System.out.println("총수량: " + orderGroup.getTotalQuantity());
});
}
}
}
핸드폰 번호로 읽어온 user의 orderGroup에 대해 확인해보는 테스트 코드이다. NullPointerException을 방지하기 위해 user가 null이 아닌 경우에만 실행하도록 if문으로 감싸준다. (UserRepotisory에서 User를 Optional 처리해주는 것이 더 정석이긴 하다.) 이전 글대로 따라한 경우라면 프린트문으로 찍은 내용이 잘 뜰 것이다. 만약 orderGroup 안에 데이터가 여러 개 들어가 있었다면 그에 해당하는 수만큼 프린트가 됐을 것이다.
(기존에 Create를 할 때 UserId를 Long형으로 지정한 부분에서 에러가 뜰 것이다. 이제 Long형이 아닌 User 객체로 바뀌었으므로 해당 부분은 일단 주석처리를 해 놓고서 실행한다.)
2. OrderGroup : OrderDetail = 1 : N
OrderGroup 엔티티
@ToString(exclude = {"user", "orderDetailList"})
...생략...
// OrderGroup : OrderDetail = 1 : N
@OneToMany(fetch = FetchType.LAZY, mappedBy = "orderGroup")
private List<OrderDetail> orderDetailList;
OrderGroup 엔티티는 바로 위에서 수정한 게 있으므로 그 아래에 위 코드를 추가로 넣으면 된다.
orderDetail은 @OneToMany의 Many 부분이므로 private OrderDetail orderDetail;이 아니고, private List<OrderDetail> orderDetail;와 같이 리스트 타입으로 지정해야 한다. 그리고 orderDetailList도 @ToString에 exclude를 시켜줘야 한다.
OrderDetail 엔티티
@Data
@NoArgsConstructor // 기본 생성자
@AllArgsConstructor // 모든 매개변수를 가진 생성자
@Entity // 엔티티임을 명시, 자바는 카멜케이스이고 DB에 연결할 때는 스네이크케이스이므로 order_detail에 자동으로 연결
@ToString(exclude = {"orderGroup"})
public class OrderDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String status;
private LocalDateTime arrivalDate;
private Integer quantity;
private BigDecimal totalPrice;
private LocalDateTime createdAt;
private String createdBy;
private LocalDateTime updatedAt;
private String updatedBy;
private Long itemId;
private Long orderGroupId;
// OrderDetail : OrderGroup = N : 1
@ManyToOne
private OrderGroup orderGroup;
}
여기에 있는 orderGroup 변수명은 연결되고자 하는 OrderGroup 엔티티에 설정한 mappedBy의 명칭과 일치해야 한다. orderGroup을 @ToString exclude 시켜주는 것도 까먹지 말자.
이제 정상적으로 동작하는지 확인해보자. 위에서처럼 User의 입장에서 확인해보겠다.
UserRepositoryTest
public class UserRepositoryTest extends StudyApplicationTests {
// Dependency Injection (DI)
@Autowired
private UserRepository userRepository;
@Test
@Transactional
public void read() {
User user = userRepository.findFirstByPhoneNumberOrderByIdDesc("010-1111-2222");
if(user != null) {
user.getOrderGroupList().stream().forEach(orderGroup -> {
System.out.println("-------------주문묶음-------------");
System.out.println("수령인: " + orderGroup.getRevName());
System.out.println("수령지: " + orderGroup.getRevAddress());
System.out.println("총금액: " + orderGroup.getTotalPrice());
System.out.println("총수량: " + orderGroup.getTotalQuantity());
System.out.println("-------------주문상세-------------");
orderGroup.getOrderDetailList().forEach(orderDetail -> {
System.out.println("주문상태: " + orderDetail.getStatus());
System.out.println("도착예정일자: " + orderDetail.getArrivalDate());
});
});
}
}
}
위에서 작성했던 UserRepositoryTest에 위와 같이 orderDetail 부분을 추가한다.
(기존에 Create를 할 때 OrderGroupId를 Long형으로 지정한 부분에서 에러가 뜰 것이다. 이제 Long형이 아닌 OrderGroup 객체로 바뀌었으므로 해당 부분은 일단 주석처리를 해 놓고서 실행한다.)
3. OrderDetail : Item = N : 1 & Item : Partner = N : 1
OrderDetail 엔티티
@ToString(exclude = {"orderGroup", "item"})
...생략...
// OrderDetail : Item = N : 1
@ManyToOne
private Item item;
OrderDetail 엔티티는 바로 위에서 수정한 게 있으므로 그 아래에 위 코드를 추가로 넣으면 된다. 그리고 item도 @ToString에 exclude를 시켜줘야 한다.
Item 엔티티
@Data
@AllArgsConstructor // 모든 생성자
@NoArgsConstructor // 기본 생성자
@Entity // 엔티티임을 명시
@ToString(exclude = {"orderDetailList", "partner"})
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // mySQL이므로 identity로 설정
private Long id;
private ItemStatus status;
private String name;
private String title;
private String content;
private BigDecimal price;
private String brandName;
private LocalDateTime registeredAt;
private LocalDateTime unregisteredAt;
private LocalDateTime createdAt;
private String createdBy;
private LocalDateTime updatedAt;
private String updatedBy;
private Long partnerId;
// Item : OrderDetail = 1 : N
@OneToMany(fetch = FetchType.LAZY, mappedBy = "item")
private List<OrderDetail> orderDetailList;
// Item : Partner = N : 1
@ManyToOne
private Partner partner;
}
mappedBy에 설정한 "item"은 OrderDetail에 있는 변수명과 일치해야 한다. orderDetailList를 @ToString에 exclude 시켜주는 것도 까먹지 말자.
Item과 Partner의 연관관계는 N:1이므로 @ManyToOne으로 설정해둔다.
Partner 엔티티
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@ToString(exclude = {"itemList"})
public class Partner {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String status;
private String address;
private String callCenter;
private String partnerNumber;
private String businessNumber;
private String ceoName;
private LocalDateTime registeredAt;
private LocalDateTime unregisteredAt;
private LocalDateTime createdAt;
private String createdBy;
private LocalDateTime updatedAt;
private String updatedBy;
private Long categoryId;
// Partner : Item = 1 : N
@OneToMany(fetch = FetchType.LAZY, mappedBy = "partner")
private List<Item> itemList;
}
Partner와 Item의 연관관계인 1:N을 @OneToMany로 선언해주고, fetch 타입은 LAZY로, mappedBy에는 partner를 입력해준다. 여기에도 @ToString exlude에 itemList를 추가해준다. itemList 변수명과 일치해야 한다.
UserRepositoryTest
public class UserRepositoryTest extends StudyApplicationTests {
// Dependency Injection (DI)
@Autowired
private UserRepository userRepository;
@Test
@Transactional
public void read() {
User user = userRepository.findFirstByPhoneNumberOrderByIdDesc("010-1111-2222");
if(user != null) {
user.getOrderGroupList().stream().forEach(orderGroup -> {
System.out.println("-------------주문묶음-------------");
System.out.println("수령인: " + orderGroup.getRevName());
System.out.println("수령지: " + orderGroup.getRevAddress());
System.out.println("총금액: " + orderGroup.getTotalPrice());
System.out.println("총수량: " + orderGroup.getTotalQuantity());
System.out.println("-------------주문상세-------------");
orderGroup.getOrderDetailList().forEach(orderDetail -> {
System.out.println("주문상품: " + orderDetail.getItem().getName());
System.out.println("고객센터 번호: " + orderDetail.getItem().getPartner().getCallCenter());
System.out.println("주문상태: " + orderDetail.getStatus());
System.out.println("도착예정일자: " + orderDetail.getArrivalDate());
});
});
}
}
}
기존에 작성했던 UserRepositoryTest 코드를 위와 같이 조금 추가해서 데이터가 정상적으로 입력되었는지 확인해본다.
(기존에 Create를 할 때 PatrnerId를 Long형으로 지정한 부분에서 에러가 뜰 것이다. 이제 Long형이 아닌 Patrner 객체로 바뀌었으므로 해당 부분은 일단 주석처리를 해 놓고서 실행한다.)
4. Partner: Category = N : 1
Partner 엔티티
@ToString(exclude = {"itemList", "category"})
...생략...
// Patner : Category = N : 1
@ManyToOne
private Category category;
위에 입력해놓은 Partner 엔티티에 category 부분을 추가한다. @ToString에 exclude도 시켜주고, @ManyToOne 어노테이션을 달아준다.
Category 엔티티
@NoArgsConstructor // 기본 생성자
@AllArgsConstructor // 전체 생성자
@Data
@Entity
@ToString(exclude = {"partnerList"})
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 기본적으로 mySQL은 IDENTITY를 사용함
private Long id;
private String type;
private String title;
private LocalDateTime createdAt;
private String createdBy;
private LocalDateTime updatedAt;
private String updatedBy;
// Category : Partner = 1 : N
@OneToMany(fetch = FetchType.LAZY, mappedBy = "category")
private List<Partner> partnerList;
}
Partner는 @OneToMany의 Many 부분이므로 리스트 타입으로 지정해둔다.
이제 UserRepositoryTest에서 테스트 코드를 돌려서 확인해본다.
UserRepositoryTest
public class UserRepositoryTest extends StudyApplicationTests {
// Dependency Injection (DI)
@Autowired
private UserRepository userRepository;
@Test
@Transactional
public void read() {
User user = userRepository.findFirstByPhoneNumberOrderByIdDesc("010-1111-2222");
if(user != null) {
user.getOrderGroupList().stream().forEach(orderGroup -> {
System.out.println("-------------주문묶음-------------");
System.out.println("수령인: " + orderGroup.getRevName());
System.out.println("수령지: " + orderGroup.getRevAddress());
System.out.println("총금액: " + orderGroup.getTotalPrice());
System.out.println("총수량: " + orderGroup.getTotalQuantity());
System.out.println("-------------주문상세-------------");
orderGroup.getOrderDetailList().forEach(orderDetail -> {
System.out.println("파트너사 이름: " + orderDetail.getItem().getPartner().getName());
System.out.println("파트너사 카테고리: " + orderDetail.getItem().getPartner().getCategory().getTitle());
System.out.println("주문상품: " + orderDetail.getItem().getName());
System.out.println("고객센터 번호: " + orderDetail.getItem().getPartner().getCallCenter());
System.out.println("주문상태: " + orderDetail.getStatus());
System.out.println("도착예정일자: " + orderDetail.getArrivalDate());
});
});
}
}
}
위에서 만든 UserRepositoryTest에 Partner와 관련된 코드 두 줄을 추가했다. 위 테스트 코드를 Run 시켜서 데이터가 정상적으로 들어갔는지 조회해본다.
(이번에도 역시 CategoryId에서 에러가 날 것이다. 기존에 Create를 할 때 CategoryId를 Long형으로 지정한 부분에서 에러가 뜨는 것이며, 이제 Long형이 아닌 Category 객체로 바뀌었으므로 해당 부분은 일단 주석처리를 해 놓고서 실행한다.)
쿼리문을 사용하면 join을 계속 걸어서 각 항목을 가져와야하지만, JPA를 활용하면 user.getOrderGroup.getOrderDetail.getItem.getPartner.getCategory와 같은 방식으로, 마치 객체를 계속해서 타고다니면서 필요한 값을 출력할 수 있다. 따라서 JPA를 활용해서 프로그래밍을 하면 쿼리문에 대해 따로 신경쓰지 않고, 객체 형태로 여러 정보를 가져오고, 수정하고, 입력할 수 있다. 이것이 바로 JPA의 장점이다.
(출처: 패스트캠퍼스 Java & SpringBoot로 시작하는 웹 프로그래밍)
'Java' 카테고리의 다른 글
[어드민 페이지 만들기] 헤더(Header) 파일 정의하기, CRUD 인터페이스 만들기 (0) | 2022.03.14 |
---|---|
[어드민 페이지 만들기] JPA 추가기능 & @Builder, @Accessors(chain = true) (0) | 2022.03.10 |
[어드민 페이지 만들기] Repository 생성 & JUnit 테스트코드 작성 (0) | 2022.03.03 |
[어드민 페이지 만들기] ERD 설계 & Table 생성 & Entity 생성 (0) | 2022.02.27 |
[JPA] Query Method(쿼리 메소드) 사용 방법 (0) | 2022.02.25 |