Java

[어드민 페이지 만들기] 연관관계 설정하기

15호의 개발자 2022. 3. 7. 12:00
반응형

[어드민 페이지 만들기] 연관관계 설정하기

 


 

연관관계 설정하기

지난 시간 동안 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로 시작하는 웹 프로그래밍)

반응형