본문 바로가기
JPA

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

by 2nyong 2023. 4. 29.

목차


    테이블 설계

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

    • 하나의 음식(Food)는 여러명의 고객(Member)에게 주문될 수 있다.
    • 음식(Food) : 고객(Member) = 1 : N

     

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

    • One에 해당하는 Entity(Food Entity)가 외래 키를 관리하며, 외래 키의 주인입니다.
    • Food Entity는 Food.memberList 필드를 통해서 음식을 주문한 고객 목록(List<Member>)에 접근할 수 있습니다.
    • Member Entity는 주문한 음식 Entity인 Food Entity에 접근할 수 없습니다.
    • 일대다 연관관계는 표준 스펙이서 지원하고 있지만, 실무에서는 권장하지 않는 연관관계입니다.

    • 이 연관관계는 One 관계인 Food Entity가 외래키를 관리하고, Many 관계인 ManyMember Entity가 외래키를 소유하고 있는 특이한 구조를 갖습니다.

     

    Entity 관계 매핑

    - Food Entity

    • 일대다에서 일(One)의 관계를 갖습니다.
    @Entity
    public class Food {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
        private double price;
        
        @OneToMany
        @JoinColumn(name = "food_id") // merber 테이블에 food_id 컬럼
        private List<Member> memberList = new ArrayList<>();
    
        public void addMemberList(Member member) {
            this.memberList.add(member);
        }
            
        // constructor, getter, setter, ...
    }

     

    - Member Entity

    • 일대다에서 다(Many)의 관계를 갖습니다.
    @Entity
    public class Member {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
        
        // constructor, getter, setter, ...
    }

     

    • 다대일 단방향 매핑에서는 One에 해당하는 Entity(Food Entity)에서는 Many에 해당하는 Entity(Member Entity)를 Collection 프레임워크로 감싼 형태의 참조 필드로 작성합니다.
    • 참조 필드 위에 @OneToMany 애노테이션과 @JoinColumn(name = "{foreign key name}") 애노테이션을 추가로 작성합니다.
    • 위의 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),
        food_id bigint,
        primary key (id)
    );
    
    alter table member 
        add constraint FKnyfi1wxbgo0pqliou7xkfe4lw 
        foreign key (food_id) 
        references food (id)

     

    Entity 관계 사용법

    - 저장

    @Transactional
    @SpringBootTest
    public class OneToManyTest {
    
        @Autowired FoodRepository foodRepository;
        @Autowired MemberRepository memberRepository;
    
        @Test
        @Rollback(value = false)
        @DisplayName("1대N 단방향 저장 테스트")
        void OneWaySaveTest() {
            // 음식(Food)1
            Food food = new Food("후라이드 치킨", 15000);
            foodRepository.save(food);
    
            // 고객(Member)1
            Member member1 = new Member("Inyong");
            memberRepository.save(member1);
            food.addMemberList(member1);
    
            // 고객(Member)2
            Member member2 = new Member("Robert");
            memberRepository.save(member2);
            food.addMemberList(member2);
        }
    }

     

    • 위 코드 실행 시, 적용되는 SQL은 다음과 같습니다.
    • 외래키의 주인과 외래키를 소유한 Entity가 서로 다르기 때문에, INSERT 이후 UPDATE를 사용하여 외래키를 입력해줘야 합니다.
    • 추가로 발생하는 UPDATE SQL로 인한 성능적 이슈구조적 복잡함@OneToMany 단방향 연관관계의 단점입니다.
    INSERT INTO FOOD (MEMBER_ID, NAME, PRICE) VALUES (NULL, "후라이드 치킨", 15000);
    INSERT INTO MEMBER (NAME) VALUES ('Inyong');
    INSERT INTO MEMBER (NAME) VALUES ('Robbert');
    UPDATE MEMBER SET (MEMBER_ID=1, NAME="Inyong", FOOD_ID=1) WHERE MEMBER_ID=1;
    UPDATE MEMBER SET (MEMBER_ID=1, NAME="Robbert", FOOD_ID=1) WHERE MEMBER_ID=1;

     

    - 조회

    @Transactional
    @SpringBootTest
    public class ManyToOneTest {
    
        @Autowired FoodRepository foodRepository;
        @Autowired MemberRepository memberRepository;
    
        @Test
        @DisplayName("1대N 단방향 조회 테스트")
        void OneWayViewTest() {
            // Food -> Member
            System.out.println("Food Entity 조회")
            Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new);
            System.out.println("food.getName() = " + food.getName());
    
            System.out.println("Member Entity 조회")
            List<Member> userList = food.getMemberList();
            for (Member member : userList) {
                System.out.println("member.getName() = " + member.getName());
            }
        }
    }
    
    // 출력 결과 
    // Food Entity 조회
    // food.getName() = 후라이드 치킨
    // Member Entity 조회
    // member.getName() = Inyong
    // member.getName() = Robert

     

    • 위 코드 실행 시, 적용되는 SQL은 다음과 같습니다.
    /* Food Table 조회 */
    SELECT f.ID, f.NAME, f.PRICE FROM FOOD f
    WHERE f.ID = 1
    
    /* Member Table 조회 */
    SELECT m.FOOD_ID, m.ID, m.NAME FROM MEMBER m
    WHERE m.FOOD_ID = 1

    댓글