[JPA] 엔티티 연관관계 매핑 - 2

Noah-wilson·2025년 1월 4일

JPA

목록 보기
7/10

다중성(Multiplicity)

다대일: @ManyToOne

다대일 단방향

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    }
@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;

}

Member에 다대일 매핑 관계를 걸어줌으로써 연관 관계를 매핑해줄 수 있다.

다대일 양방향

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    }
@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
}

외래 키가 있는 쪽이 연관관계의 주인이다.
다대일의 반대는 일대다이므로 반대인 Team을 mappedBy를 통해 양쪽을 서로 참조하도록 개발할 수 있다.

일대다: @OneToMany

일대다 단방향

@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;

    @OneToMany()
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<Member>();
    }
@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;
    }
            Member member = new Member();
            member.setUsername("MemberA");
            em.persist(member);
            
            Team team=new Team();
            team.setName("TeamA");
            team.getMembers().add(member);
            em.persist(team);
Hibernate: 
    /* insert for
        hellojpa.Team */insert 
    into
        Team (name, TEAM_ID) 
    values
        (?, ?)
Hibernate: 
    update
        Member 
    set
        TEAM_ID=? 
    where
        MEMBER_ID=?

정리

• 일(1)이 연관관계의 주인이다.
• 객체와 테이블의 차이 때문에 반대편 다(N) 테이블이 외래 키를 관리한다.
• @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블 방식을 사용해야한다.
• 연관관계 관리를 위해 추가로 UPDATE SQL 실행한다.
• 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는것이 좋다.

@JoinColumn을 사용하지 않은 예:

@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;

    @OneToMany()
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<Member>();
    }

Team 코드는 유지한다.

Hibernate: 
    create table Team_Member (
        Team_TEAM_ID bigint not null,
        members_MEMBER_ID bigint not null unique
    )
    Hibernate: 
    alter table if exists Team_Member 
       add constraint FKpsjmea2e134ab3x3gsdoyxepg 
       foreign key (members_MEMBER_ID) 
       references Member
Hibernate: 
    alter table if exists Team_Member 
       add constraint FK4u1npo283vgqfk8lyxclihgnl 
       foreign key (Team_TEAM_ID) 
       references Team
Hibernate: 
    /* insert for
        hellojpa.Team.members */insert 
    into
        Team_Member (Team_TEAM_ID, members_MEMBER_ID) 
    values
        (?, ?)

log를 보면 내가 계획하지 않은 Team_Member 테이블을 생성하고 insert한다. 아래 DB를 확인한 결과이다.

TEAM_TEAM_IDMEMBERS_MEMBER_ID
11

일대다 양방향

@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;

    @OneToMany()
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<Member>();
    }
@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;
    
    @ManyToOne()
    @JoinColumn(name = "",insertable = false, updatable = false)
    private Team team;

정리

일대다 양방향 매핑은 공식적으로 존재하지않고 억지로@JoinColumn(insertable=false, updatable=false)을 사용하여 양방향 매핑을 구현해야 한다.
다대일 양방향을 사용하자.

일대일: @OneToOne

*참고: 주 테이블은 관계에서 외래키를 소유하는 테이블을 의미, 대상 테이블은 외래키를 통해 연결되는 테이블을 의미한다.

일대일 관계는 그 반대도 일대일이다.
주 테이블이나 대상 테이블 중에 외래 키 선택 가능하다.
외래 키에 데이터베이스 유니크(UNI) 제약조건 추가해야 한다.(DB는 UNI 제약 조건을 설정하여야 일대일 관계 가능)

다대일 매핑과 같이 외래키가 있는 곳이 연관 관계의 주인이다.반대편은 mappedBy 적용한다.

주 테이블에 외래 키 양방향인 경우

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "LOCKER_ID",unique = true)
    private Locker locker;
    //getter setter
    }
@Entity
public class Locker {

    @Id
    @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;

    private String name;

    @OneToOne(mappedBy = "locker")
    private Member member;
	//getter setter
}

주테이블(member)에 외래키 양방향인 경우 주객체가 대상 객체(locker)의 참조를 가지는 것처럼 주테이블의 외래키(locker_id)를 가지고 찾을수 있다.

            Locker locker = new Locker();
            locker.setName("lockerB");
            em.persist(locker);

            Member member = new Member();
            member.setUsername("member4");
            member.setLocker(locker);
            em.persist(member);

            em.flush();
            em.clear();

            System.out.println("=====================");
            Member findMember = em.find(Member.class, 1L);
            System.out.println("findMember.getClass() = " + findMember.getClass());
            System.out.println("findMember.getId() = " + findMember.getId());
            System.out.println("findMember.getUsername() = " + findMember.getUsername());

            System.out.println("=====================");
            Locker locker1 = findMember.getLocker();
            System.out.println("locker1.getClass() = " + locker1.getClass()); 
            System.out.println("locker1.getId() = " + locker1.getName());

실행 결과:

=====================
Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.LOCKER_ID,
        m1_0.TEAM_ID,
        m1_0.USERNAME 
    from
        Member m1_0 
    where
        m1_0.MEMBER_ID=?
findMember.getClass() = class hellojpa.Member
findMember.getId() = 1
findMember.getUsername() = member4
=====================
locker1.getClass() = class hellojpa.Locker$HibernateProxy$xmWthfWJ
Hibernate: 
    select
        l1_0.LOCKER_ID,
        m1_0.MEMBER_ID,
        m1_0.TEAM_ID,
        m1_0.USERNAME,
        l1_0.name 
    from
        Locker l1_0 
    left join
        Member m1_0 
            on l1_0.LOCKER_ID=m1_0.LOCKER_ID 
    where
        l1_0.LOCKER_ID=?
locker1.getId() = lockerB
LOCKER_IDMEMBER_IDUSERNAME
11member4

주테이블에서 외래키 locker의 값이 없을 경우:

            Member findMember = em.find(Member.class, 1L);
            System.out.println("findMember.getClass() = " + findMember.getClass());
            System.out.println("findMember.getId() = " + findMember.getId());
            System.out.println("findMember.getUsername() = " + findMember.getUsername());
            if (findMember.getLocker() == null) {
                System.out.println("NPE 발생");
            }
findMember.getClass() = class hellojpa.Member
findMember.getId() = 1
findMember.getUsername() = member4
NPE 발생
LOCKER_IDMEMBER_IDUSERNAME
null1member4

정리

외래키 값이 있을 경우 프록시가 적용된것을 볼 수 있는데
이말은 주테이블(member)만 조회해도 대상테이블(locker)에 데이터가 있는지 확인이 가능하다.
그러므로 객체지향 개발자에게는 편리한 구조이다.
하지만 locker값이 없으면 null 허용이 된다.

대상 테이블에 외래키 단방향인 경우

JPA에서 지원도 안되고 연관 관계를 매핑해줄 수 있는 방법이 존재하지 않는다.

대상 테이블에 외래 키 양방향인 경우

@Entity
public class Locker {

    @Id
    @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;

    private String name;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "MEMBER_ID2", unique = true)
    private Member2 member2;
    //getter setter
    }
@Entity
public class Member2 {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID2")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @OneToOne(mappedBy = "member2")
    private Locker locker;
    //getter setter
}
            Member2 member2 = new Member2();
            member2.setUsername("memberA");
            em.persist(member2);

            Locker locker = new Locker();
            locker.setName("lockerA");
            locker.setMember2(member2);
            em.persist(locker);

            em.flush();
            em.clear();

            System.out.println("=====================");
            Member2 findMember2 = em.find(Member2.class, 1L);
            System.out.println("findMember2.getClass() = " + findMember2.getClass());
            System.out.println("findMember2.getId() = " + findMember2.getId());
            System.out.println("findMember2.getUsername() = " + findMember2.getUsername());
            System.out.println("=====================");
            Locker locker1 = findMember2.getLocker();
            System.out.println("locker1.getClass() = " + locker1.getClass()); 
            System.out.println("findMember2.getLocker().getName() = " + locker1.getName());

실행 결과:

=====================
Hibernate: 
    select
        m1_0.MEMBER_ID2,
        l1_0.LOCKER_ID,
        l1_0.name,
        m1_0.USERNAME 
    from
        Member2 m1_0 
    left join
        Locker l1_0 
            on m1_0.MEMBER_ID2=l1_0.MEMBER_ID2 
    where
        m1_0.MEMBER_ID2=?
findMember2.getClass() = class hellojpa.Member2
findMember2.getId() = 1
findMember2.getUsername() = memberA
=====================
locker1.getClass() = class hellojpa.Locker
findMember2.getLocker().getName() = lockerA
LOCKER_IDMEMBER_ID2NAME
11lockerA

locker 조회 예제:

            System.out.println("=====================");
            Locker findLocker = em.find(Locker.class, 1L);
            System.out.println("findLocker.getClass() = " + findLocker.getClass());
            System.out.println("findLocker.getName() = " + findLocker.getName());

            System.out.println("=====================");

            Member2 findLockerMember = findLocker.getMember2();
            System.out.println("findLockerMember.getClass() = " + findLockerMember.getClass());
            System.out.println("findLockerMember.getUsername() = " + findLockerMember.getUsername());

실행 결과:

=====================
Hibernate: 
    select
        l1_0.LOCKER_ID,
        l1_0.MEMBER_ID2,
        l1_0.name 
    from
        Locker l1_0 
    where
        l1_0.LOCKER_ID=?
findLocker.getClass() = class hellojpa.Locker
findLocker.getName() = lockerA
=====================
findLockerMember.getClass() = class hellojpa.Member2$HibernateProxy$frn8HIqN
Hibernate: 
    select
        m1_0.MEMBER_ID2,
        l1_0.LOCKER_ID,
        l1_0.name,
        m1_0.USERNAME 
    from
        Member2 m1_0 
    left join
        Locker l1_0 
            on m1_0.MEMBER_ID2=l1_0.MEMBER_ID2 
    where
        m1_0.MEMBER_ID2=?
findLockerMember.getUsername() = memberA

정리

member를 조회시 대상테이블(locker)에 외래키가 존재하므로 지연 로딩으로 설정해도 항상 즉시 로딩된다.(프록시 x)
반대로 locker를 조회하면 정상적으로 설정된 지연로딩으로 동작할것이다.(member가 외래키를 가지고 있는 상황에서도 locker를 먼저 조회하면 당연히 즉시 로딩 됨)

다대다: @ManyToMany

다대다 단방향

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT")
    private List<Product> products = new ArrayList<Product>();
    }
@Entity
public class Product {

    @Id @GeneratedValue
    private Long id;

    private String name;
}

다대다 양방향

Member 코드는 유지.

@Entity
public class Product {

    @Id @GeneratedValue
    private Long id;

    private String name;
    
       @ManyToMany(mappedBy = "products")
    private List<Member> members=new ArrayList<>();

}

정리

편리해 보이지만 실무에서 사용하지 않는다. 실무에서는 연결 테이블이 단순히 연결만 하고 끝나지 않기 때문이다.(ex: 주문시간, 수량 같은 데이터가 들어올 수 있다.)

실무에서는 @ManyToMany는 @OneToMany, @ManyToOne로 풀어서 해결할 수 있다.

@ManyToMany -> @OneToMany, @ManyToOne로 풀어보기

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> MemberProducts = new ArrayList<MemberProduct>();
}
@Entity
public class MemberProduct {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;
    
    private int count;
    private int price;
    private LocalDateTime orderDateTime;
}

@Entity
public class Product {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "product")
    private List<MemberProduct> memberProducts=new ArrayList<>();
    
}
            Member member=new Member();
            member.setUsername("memberA");
            em.persist(member);
            
            Product product = new Product();
            product.setName("ProductA");
            em.persist(product);
            
            MemberProduct memberProduct=new MemberProduct();
            memberProduct.setProduct(product);
            memberProduct.setMember(member);
            em.persist(memberProduct);

실행 결과:

Hibernate: 
    /* insert for
        hellojpa.Member */insert 
    into
        Member (USERNAME, MEMBER_ID) 
    values
        (?, ?)
Hibernate: 
    /* insert for
        hellojpa.Product */insert 
    into
        Product (count, name, orderDateTime, price, id) 
    values
        (?, ?, ?, ?, ?)
Hibernate: 
    /* insert for
        hellojpa.MemberProduct */insert 
    into
        MemberProduct (MEMBER_ID, PRODUCT_ID, id) 
    values
        (?, ?, ?)

0개의 댓글