-> 블로그 이전

[JPA] 2. 일대다 연관관계 (@OneToMany)

2022. 7. 3. 14:37Language`/JPA

일대다 관계는 다대일 관계 반대 방향이다

일대다 관계는 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션(Collection, List, Set, Map)중에 하나를 사용해서 관리해야 한다

 

현재 포스팅에서의 도메인간 관계는 다음과 같다
- 하나의 User는 여러개의 Locker를 가질 수 있다
- 하나의 Locker는 하나의 User에 의해서만 소유될 수 있다

1. 일대다 단방향

일대다 단방향은 특이하게 일(1)쪽에서 상대(N) 엔티티의 외래키를 관리하는 구조로 이루어져 있다

보통 자신이 매핑한 테이블의 FK를 관리하는데 이 경우는 반대쪽 테이블에 있는 FK를 관리하는 특이항 구조이다

 

왜냐하면 FK는 반드시 "다"쪽에 있어야 하는데 "다"쪽인 Locker를 보면 현재 FK를 매핑할 수 있는 참조 필드가 존재하지 않는다

 

User (1 - 주인)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long id;

    private String name;

    @OneToMany
    @JoinColumn(name = "user_id")
    private List<Locker> lockerList = new ArrayList<>();
}
List<Locker> lockerList는 실제 DB상에서 Locker Table의 "user_id"를 참조해야 매핑이 되기 때문에 @JoinColumn(name = "user_id")로 매핑시켜야 한다

 

Locker (N - 주인 X)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "locker")
public class Locker {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "locker_id")
    private Long id;

    private String lockerName;
}

일대다 단방향 관계를 매핑할 때는 반드시 @JoinColumn을 명시해줘야 한다

만약 이 @JoinColumn을 매핑하지 않는다면 중간에서 연관관계를 관리하는 "JoinTable" 전략을 default로 설계되었기 때문에 JoinTable이 생성된다

 

※ @JoinColumn 매핑 O

@OneToMany
@JoinColumn(name = "user_id")
private List<Locker> lockerList = new ArrayList<>();
Hibernate: 
    create table locker (
       locker_id bigint not null auto_increment,
        lockerName varchar(255),
        user_id bigint,
        primary key (locker_id)
    ) engine=MyISAM
    
Hibernate: 
    create table user (
       user_id bigint not null auto_increment,
        name varchar(255),
        primary key (user_id)
    ) engine=MyISAM
    
# FK 제약조건 설정
Hibernate: 
    alter table locker 
       add constraint FKbh0f46ekjtm5ac0cg0vhn6d4i 
       foreign key (user_id) 
       references user (user_id)

 

※ @JoinColumn 매핑 X

@OneToMany
private List<Locker> lockerList = new ArrayList<>();
Hibernate: 
    create table locker (
       locker_id bigint not null auto_increment,
        lockerName varchar(255),
        primary key (locker_id)
    ) engine=MyISAM
    
Hibernate: 
    create table user (
       user_id bigint not null auto_increment,
        name varchar(255),
        primary key (user_id)
    ) engine=MyISAM
    
    
# JoinTable
Hibernate: 
    create table user_locker (
       User_user_id bigint not null,
        lockerList_locker_id bigint not null
    ) engine=MyISAM
    
    
# FK 제약조건 설정
Hibernate: 
    alter table user_locker 
       add constraint UK_svt7ncsflgvowhrsqky5jomq unique (lockerList_locker_id)
       
Hibernate: 
    alter table user_locker 
       add constraint FKd2c1j4mi8joaj1vmx0gvxc2a1 
       foreign key (lockerList_locker_id) 
       references locker (locker_id)
       
Hibernate: 
    alter table user_locker 
       add constraint FK21njhumvfu4grnarvr0mvvjfk 
       foreign key (User_user_id) 
       references user (user_id)

 

일대다 단방향 매핑의 단점

일대다 단방향 매핑은 엔티티가 관리하는 FK가 다른 테이블에 존재하기 때문에 "연관관계 관리"를 위해서 추가적으로 UPDATE SQL이 실행된다

Locker lockerA = new Locker("lockerA");
Locker lockerB = new Locker("lockerB");
em.persist(lockerA);
em.persist(lockerB);

User user = new User("userA");
user.getLockerList().add(lockerA);
user.getLockerList().add(lockerB);
em.persist(user);
-- 각 엔티티 persist(insert)
Hibernate: 
    /* insert OneToMany.domain.Locker
        */ insert 
        into
            locker
            (lockerName) 
        values
            (?)
            
Hibernate: 
    /* insert OneToMany.domain.Locker
        */ insert 
        into
            locker
            (lockerName) 
        values
            (?)
            
Hibernate: 
    /* insert OneToMany.domain.User
        */ insert 
        into
            user
            (name) 
        values
            (?)
            

-- update query
Hibernate: 
    /* create one-to-many row OneToMany.domain.User.lockerList */ update
        locker 
    set
        user_id=? 
    where
        locker_id=?
        
Hibernate: 
    /* create one-to-many row OneToMany.domain.User.lockerList */ update
        locker 
    set
        user_id=? 
    where
        locker_id=?

따라서 일대일 단방향 매핑보다는 "다대일 양방향 매핑"을 사용하는 것을 더 권장한다

  • 다대일 양방향이라면 insert query하나로 해결할 수 있기 때문이다

2. 일대다 양방향

"일대다 양방향"이라는 매핑은 사실 존재하지 않다

일대다 양방향은 그냥 "읽기 전용 필드"를 사용해서 양방향처럼 사용하는 것이다

 

User (1 - 주인)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long id;

    private String name;

    @OneToMany
    @JoinColumn(name = "user_id")
    private List<Locker> lockerList = new ArrayList<>();
}

Locker(N - 주인 X)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "locker")
public class Locker {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "locker_id")
    private Long id;

    private String lockerName;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", insertable = false, updatable = false)
    private User user;
}

 

사실 이렇게 매핑을 설계하는 것보다는 그냥 "다대일 양방향"을 사용하자


※ 다대일 양방향 버전

User (1)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "user")
    private List<Locker> lockerList = new ArrayList<>();
}

Locker(N - 주인)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "locker")
public class Locker {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "locker_id")
    private Long id;

    private String lockerName;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    public void registerUser(User user){
        if(this.user != null){
            this.user.getLockerList().remove(this);
        }

        this.user = user;
        user.getLockerList().add(this);
    }
 }
User user = new User("userA");
em.persist(user);

Locker lockerA = new Locker("lockerA");
Locker lockerB = new Locker("lockerB");
lockerA.registerUser(user);
lockerB.registerUser(user);
em.persist(lockerA);
em.persist(lockerB);

이 실행 결과로 도출된 query를 한번 유심히 살펴보자 (persist 제외)

Hibernate: 
    /* insert OneToMany.domain.User
        */ insert 
        into
            user
            (name) 
        values
            (?)
            
Hibernate: 
    /* insert OneToMany.domain.Locker
        */ insert 
        into
            locker
            (lockerName, user_id) 
        values
            (?, ?)
            
Hibernate: 
    /* insert OneToMany.domain.Locker
        */ insert 
        into
            locker
            (lockerName, user_id) 
        values
            (?, ?)

이전에 일대다 양방향 매핑시 insert query + "update query"가 나감으로써 연관관계 관리를 해주었다.

그러나 "다대일 양방향 매핑"에서는 추가적인 update query없이 insert query만으로 연관관계 관리를 편리하게 할 수 있다