본문 바로가기
JPA

[JPA] 다대일 연관관계 (양방향)

by 2nyong 2023. 4. 29.

목차


테이블 설계

다대일 연관관계를 고객이 주문한 음식(Food) 고객(Member)을 예시로 설명하겠습니다.

  • 여러가지의 음식(Food)은 한 명의 고객(Member)에게 주문될 수 있다.
  • 음식(Food) : 고객(Member) = N : 1

 

양방향 연관관계를 위한 추가 조건은 다음과 같습니다.

  • Food Entity는 Food.member 필드를 통해 음식을 주문한 고객(Member Entity)에 접근할 수 있다.
  • Member Entity는 Member.foodList 필드를 통해서 주문한 음식 목록(List<Food>)에 접근할 수 있다.


Entity 관계 매핑

- Food Entity

  • 다대일에서 다(Many)의 관계를 갖습니다.
@Entity
public class Food {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;
    
    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;
    
    // constructor, getter, setter, ...
}

 

- Member Entity

  • 다대일에서 일(One)의 관계를 갖습니다.
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "member")
    List<Food> foodList = new ArrayList<>();

    public void addFoodList(Food food) {
        this.foodList.add(food);
        food.setMember(this); // 외래 키의 주인을 통해 업데이트
    }
        
    // constructor, getter, setter, ...
}

 

  • 다대일 단방향 매핑에서도 단방향 매핑과 같이 다(Many)쪽에서 일(One)의 Entity를 참조 필드로 갖습니다.
  • 외래 키 주인 Entity의 참조 필드 위에 @ManyToOne 애노테이션과 @JoinColumn(name = "{foreign key name}") 애노테이션을 추가로 작성합니다.
  • One에 해당하는 Entity(Member Entity)에서는 Many에 해당하는 Entity(Food Entity)를 Collection 프레임워크로 감싼 형태의 참조 필드로 작성합니다.
  • 이 때 일(One)에 해당하는 Entity의 참조 필드 위에 @OneToMany(mappedBy = "{다(Many) Entity가 참조하는 필드 이름}") 애노테이션을 추가로 작성합니다.
  • mappedBy 속성은 양방향 연관관계에서 사용하며, 이 연관관계에 대해 외래 키의 주인이 참조하고 있는 필드 명을 넣어줍니다.
  • 위의 Entity 관계를 적용하여 테이블을 생성하였을 때, 실행되는 SQL은  아래와 같습니다.
create table food (
    id bigint not null auto_increment,
    name varchar(255),
    price float(53) not null,
    member_id bigint,
    primary key (id)
);

create table member (
    id bigint not null auto_increment,
    name varchar(255),
    primary key (id)
);

alter table food 
    add constraint FKnyfi1wxbgo0pqliou7xkfe4lw 
    foreign key (member_id) 
    references member (id)

Entity 관계 사용법

- 저장

@Transactional
@SpringBootTest
public class ManyToOneTest {

    @Autowired FoodRepository foodRepository;
    @Autowired MemberRepository memberRepository;

    @Test
    @Rollback(value = false)
    @DisplayName("N대1 양방향 저장 테스트")
    void TwoWaySaveTest() {
        // 고객(Member)1
        Member member = new Member("Inyong");
        memberRepository.save(member);

        // 음식(Food)1
        Food food1 = new Food("후라이드 치킨", 15000);
        foodRepository.save(food1);
        member.addFoodList(food1) // Member Entity에서 Food Entity 참조

        // 음식(Food)2
        Food food2 = new Food("양념 치킨", 20000);
        foodRepository.save(food2);
        member.addFoodList(food2) // Member Entity에서 Food Entity 참조
    }
}

 

  • 위 코드 실행 시, 적용되는 SQL은 다음과 같습니다.
  • JPA의 변경감지와 Hibernate의 업데이트 기본 전략에 의해 UPDATE 문이 실행됩니다.
INSERT INTO MEMBER (NAME) VALUES ('Inyong');
INSERT INTO FOOD (MEMBER_ID, NAME, PRICE) VALUES (NULL, "후라이드 치킨", 15000);
INSERT INTO FOOD (MEMBER_ID, NAME, PRICE) VALUES (NULL, "양념 치킨", 20000);
UPDATE FOOD SET (MEMBER_ID=1, NAME="후라이드 치킨", PRICE=15000) WHERE FOOD_ID=1;
UPDATE FOOD SET (MEMBER_ID=1, NAME="양념 치킨", PRICE=20000) WHERE FOOD_ID=2;

 

- 조회

@Transactional
@SpringBootTest
public class ManyToOneTest {

    @Autowired FoodRepository foodRepository;
    @Autowired MemberRepository memberRepository;

    @Test
    @DisplayName("N대1 양방향 조회")
    void TwoWayViewTest() {
        System.out.println("[Food -> Member]");
        Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new);
        System.out.println("food.getName() = " + food.getName());
        System.out.println("food.getMember().getName() = " + food.getMember().getName());

        System.out.println("[Member -> Food"]);
        Member member = memberRepository.findById(1L).orElseThrow(NullPointerException::new);
        System.out.println("member.getName() = " + member.getName());

        List<Food> foodList = member.getFoodList();
        for (Food food1 : foodList) {
            System.out.println("food1.getName() = " + food1.getName());
            System.out.println("food1.getPrice() = " + food1.getPrice());
        }
    }
}

// 출력 결과 
// [Food -> Member]
// food.getName() = 후라이드 치킨
// food.getMember().getName() = Inyong
// [Member -> Food]
// member.getName() = Inyong
// food1.getName() = 후라이드 치킨
// food1.getPrice() = 15000.0
// food1.getName() = 양념 치킨
// food1.getPrice() = 20000.0

 

  • 위 코드 실행 시, 적용되는 SQL은 다음과 같습니다.
/* Food -> Member */
SELECT f.ID, m.ID, m.NAME, f.NAME, f.PRICE FROM FOOD f
    LEFT JOIN MEMBER m ON m.ID = f.MEMBER_ID
WHERE f.ID = 1

/* Member -> Food */
SELECT f.MEMBER_ID, f.ID, f.NAME, f.PRICE FROM FOOD f
WHERE f.ID = 1

 

댓글