자바 ORM 표준 JPA 프로그래밍 | 김영한 | 에이콘출판- 교보ebook

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임, <b>★ 이 책에서 다루는 내용 ★</b> ■ JPA 기초 이론과 핵심 원리 ■ JPA로 도메인 모델을 설계하는 과정을 예제 중심

ebook-product.kyobobook.co.kr

소스

https://github.com/rkwhr0010/jpa

 

엔티티 연관관계 매핑 고려사항

  • 다중성
  • 단방향, 양방향
  • 연관관계의 주인

 

다중성

  • 다대일
  • 일대다
  • 일대일
  • 다대다

 

단방향, 양방향

테이블은 원래 양방향 이기 때문에 앤티티 방향을 의미

객체의 방향이란 객체 그래프 탐색이 가능한지 여부

 

연관관계의 주인

테이블은 하나의 외래 키로 테이블이 연관관계를 맺음

관리 포인트는 외래키 하나

엔티티는 방향 개로 양방향 연관관계를 맺음

관리 포인트 방향

 

차이를 보정하기 위해 하나의 엔티티에서 외래 키를 관리하도록 연관관계의 주인을 정함

주인이 아닌 반대편은 외래 키에 대해서 읽기 전용으로 동작

 

연관관계 주인이 아닌 곳은 mappedBy 속성으로 연관관계 주인 필드 이름을 명시한다.

 

다중성, 단방향, 양방향 고려한 경우의

  • 다대일
  • 일대다
  • 일대일
  • 다대다

 

다대일

DB 테이블의 다대일 관계는 항상 쪽에 외래 키가 있다.

따라서 객체 양방향 관계에서 연관관계의 주인은 항상 다쪽이다.

 

다대일 단방향

 

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        private String username;
        
        @ManyToOne
        @JoinColumn(name = "team_id")
        private Team team;
}
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Team {
        @Id
        @GeneratedValue
        @Column(name = "team_id")
        private Long id;
        private String name;
}

Member 에서 Team 탐색이 가능

 

다대일 양방향

 



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private String id;
        private String username;
        
        @ManyToOne
        @JoinColumn(name = "team_id")
        private Team team;
        
        public void setTeam(Team team) {
                // 최초 값 할당 시 null 체크
                if (this.team != null) {
                        team.getMembers().remove(this);
                }
                this.team = team;
                // 내가 이미 저장되어 있는지 (루프 체크)
                if (!team.getMembers().contains(this)) {
                        team.getMembers().add(this);
                }
        }
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Team {
        @Id
        @GeneratedValue
        @Column(name = "team_id")
        private String id;
        private String name;
        
        @OneToMany(mappedBy = "team")
        private List<Member> members = new ArrayList<>();
        
        public void addMember(Member member) {
                this.members.add(member);
                // 내가 이미 연관관계 맺고 있는지 (루프 방지)
                if (member.getTeam() != this) {
                        member.setTeam(this);;
                }
        }
}

 

  • 양방향은 외래 키가 있는 쪽이 연관관계의 주인이다.
    JPA
    외래 키를 관리할 연관관계의 주인만 사용한다.
    주인이 아닌 쪽은 조희를 위한 JPQL이나 편의 목적으로 객체 그래프 탐색할 사용한다.
  • 양방향 연관관계는 항상 서로 참조해야 한다.
    서로 참조하게 하려면 연관관계 편의 메소드를 작성하는 것이 좋다.
    편의 메소드는 한쪽/양쪽 작성에 작성하면 된다.
    양쪽에 작성할 경우 순환 호출로 루프에 빠지는 것을 주의해야 한다.

 

일대다

이상의 엔티티를 참조할 있으므로 자바 컬렉션 프레임워크를 사용해 표현한다.

Collection, List, Set, Map

 

일대다 단방향

JPA 2.0 부터 관계 표현 가능

보통 자신이 매핑한 테이블에 외래 키를 관리하는데

경우 반대편 테이블 외래 키를 관리한다.

 



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @Column(name = "member_id")
        private String id;
        private String username;
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Team {


        @Id
        @Column(name = "team_id")
        private String id;
        private String name;
        
        @OneToMany
        @JoinColumn(name = "team_id") // Member 테이블의 team_id 컬럼을 의미함
        private List<Member> members = new ArrayList<>();
}

 

일다대 단방향 관계를 매핑할 @JoinColumn 명시하지 않으면, JPA 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 사용한다.

 

 

 

일대다 단방향 매핑의 단점

일쪽 엔티티가 자기가 매핑된 테이블이 아닌 다른 테이블에 외래 키를 관리한다는 것에서 발생한다.

본인 엔티티를 저장하는 본인 엔티티 INSERT SQL 만으로 끝나는게 아니라

외래 키가 있으면 연관관계 처리를 위한 UPDATE SQL 추가로 실행해야 한다.



        private static void save() {
                logic(em -> {
                        Member m1 = new Member();
                        m1.setUsername("m1");
                        Member m2 = new Member();
                        m2.setUsername("m2");
                        
                        Team t1 = new Team();
                        t1.getMembers().add(m1);
                        t1.getMembers().add(m2);
                        
                        em.persist(m1);
                        em.persist(m2);
                        em.persist(t1); // 문제 지점
                });
        }

 

 

Member 엔티티는 Team 엔티티를 모른다.

그러나 연관관계에 대한 정보는 Team.members 관리한다.

따라서 Member 엔티티를 저장할 때는 외래 정보가 빠지고 저장한다.

나중에 Team 엔티티를 저장할 Team.members 참조 값을 확인하고 Member 테이블의 외래 정보를 업데이트한다.

 

 

일대다 양방향

일대다 양방향 매핑은 없다.

양방향 매핑에서 @OneToMany 연관관계의 주인이 없다.

RDB 특성상 일대다 관계에선 외래 키는 항상 쪽에 있다.

 

 

 

일대다 : 다대일 관계에서 항상 다대일 쪽이 연관관계 주인일 밖에 없다.

그런 이유로 ManyToOne mappedBy 속성이 없다.

 

억지로 사용할려면 사용할 있다.



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        private String username;
        
        @ManyToOne
        @JoinColumn(name = "team_id",
                insertable = false, updatable = false)
        private Team team;
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Team {


        @Id
        @GeneratedValue
        @Column(name = "team_id")
        private Long id;
        private String name;
        
        @OneToMany
        @JoinColumn(name = "team_id") // Member 테이블의 team_id 컬럼을 의미함
        private List<Member> members = new ArrayList<>();
}

 

일대다 단방향 매핑에 반대편에 다대일 단방향 매핑을 "읽기전용"으로 추가했다.

 

일대일

양쪽이 하나의 관계만 가진다.

 

테이블 관계에서 일대일은 테이블, 대상 테이블 어느 곳이나 외래 키를 가질 있다. 따라서 어느 테이블이 외래 키를 가질지 선택해야 한다.

 

테이블에 외래

객체지향적 방법

테이블에 외래 키를 두고 대상 테이블을 참조한다.

방법은 테이블이 외래 키를 가지고 있으므로 테이블만 확인해도 대상 테이블과 연관관계가 있는지 있다.

 

대상 테이블에 외래

일반적인 DB 관계 사용법

대상 테이블에 외래 키를

방법은 테이블 관계를 일대일에서 일대다로 변경할 테이블 구조를 그대로 유지할 있다.

 

테이블에 외래

테이블에 외래 키에 있어 JPA 쉽게 매핑할 있다.

객체지향에선 객체가 대상 객체의 참조를 가지고 있어야 탐색할 있다.

 

단방향

일대일 관계라 외래키에 유니크 제약조건까지 추가했다.

 



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        @OneToOne
        @JoinColumn(name = "locker_id")
        private Locker locker;
        private String username;
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Locker {
        @Id
        @GeneratedValue
        @Column(name = "locker_id")
        private Long id;
        private String name;
}

 

양방향



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Locker {
        @Id
        @GeneratedValue
        @Column(name = "locker_id")
        private Long id;
        private String name;
        @OneToOne(mappedBy = "locker")
        private Member member;
}

 

연관관계의 주인만 정해주면 된다.

 

대상 테이블에 외래

단방향

일대일 관계 엔티티에서 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다.

매핑할 있는 방법이 없다. 하려면 양방향으로 바꿔야 한다.

 

 

양방향

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        @OneToOne(mappedBy = "member")
        private Locker locker;
        private String username;
}
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Locker {
        @Id
        @GeneratedValue
        @Column(name = "locker_id")
        private Long id;
        private String name;
        @OneToOne
        @JoinColumn(name = "member_id")
        private Member member;
}

일대일 매핑에서 대상 테이블에 외래 키를 두고 싶으면, 양방향으로 매핑해야 한다.

엔티티인 Member 엔티티 대신 대상 엔티티 Locker 연관관계의 주인으로 만들어 Locker 외래 member_id 관리하게 한다.

 

 

다대다

RDB 정규화된 테이블로 다대다 관계를 표현할 없다.

그래서 중간에 연결 테이블을 추가해 일대다-다대일, 일대다-다대일로 해소한다.

 

 

 

다대다 관계는 테이블에서는 불가능하지만, 객체 관계에서는 가능하다.

양쪽에서 컬렉션을 사용하면 그만이다.

 

다대다 단방향



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        private String username;
        
        @ManyToMany
        @JoinTable(name = "member_product",
                        joinColumns = @JoinColumn(name = "member_id"),
                        inverseJoinColumns = @JoinColumn(name = "product_id"))
        private List<Product> products = new ArrayList<>();
}

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
        @Id
        @GeneratedValue
        @Column(name = "product_id")
        private Long id;
        private String name;
}

 



public class Main {
        static EntityManagerFactory emf = Persistence.createEntityManagerFactory("studyjpa");
        
        public static void main(String[] args) {
                save();
                find();
                emf.close();
        }
        


        private static void find() {
                logic(em -> {
                        Member m1 = em.find(Member.class, 1L);
                        System.out.println(m1);
                });
        }


        private static void save() {
                logic(em -> {
                        Product p1 = new Product();
                        p1.setName("제품1");
                        em.persist(p1);
                        Product p2 = new Product();
                        p2.setName("제품2");
                        em.persist(p2);
                        
                        Member m1 = new Member();
                        m1.setUsername("멤버1");
                        m1.getProducts().add(p1);
                        m1.getProducts().add(p2);
                        em.persist(m1);
                });
        }




        static void logic(Consumer<EntityManager> logic) {
                EntityManager em = emf.createEntityManager();
                EntityTransaction tx = em.getTransaction();
                
                try {
                        tx.begin();
                        
                        logic.accept(em);
                        
                        tx.commit();
                } catch (Exception e) {
                        e.printStackTrace();
                        tx.rollback();
                } finally {
                        em.close();
                }
        }
}

 

저장

 

조회

 

 

 

 

다대다 양방향



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        private String username;
        
        @ManyToMany
        @JoinTable(name = "member_product",
                        joinColumns = @JoinColumn(name = "member_id"),
                        inverseJoinColumns = @JoinColumn(name = "product_id"))
        private List<Product> products = new ArrayList<>();
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
        @Id
        @GeneratedValue
        @Column(name = "product_id")
        private Long id;
        private String name;
        
        @ManyToMany(mappedBy = "products")
        @ToString.Exclude // toString 무한 루프 방지
        private List<Member> members;
}

 

다대다 매핑의 한계와 극복, 연결 엔티티 사용

@ManyToMany 연결 테이블을 자동으로 처리할 있지만, 실무에서 사용하기엔 한계가 있다.

실무라면, 연결 테이블에 멤버ID, 상품ID 끝나지 않는다.

추가 정보가 담긴다.

 

@ManyToMany 로는 추가 정보를 담을 없다.

따로 연결 테이블을 매핑하는 연결 엔티티를 만들고, 거기에 추가 컬럼을 매핑해야 한다.

사실 지금 DDL AUTO 기능은 실무에선 쓰지 못한다.

신규 기능 개발할 때나 독립적으로 사용한다.

 



@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        private String username;
        
        @OneToMany(mappedBy = "member")
        private List<MemberProduct> memberProducts = new ArrayList<>();
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
        @Id
        @GeneratedValue
        @Column(name = "product_id")
        private Long id;
        private String name;
}


@Entity
@IdClass(MemberProductId.class)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MemberProduct {
        
        @Id
        @ManyToOne
        @JoinColumn(name = "member_id")
        private Member member;
        
        @Id
        @ManyToOne
        @JoinColumn(name = "product_id")
        private Product product;
        
        private int orderAmount;
        private LocalDateTime orderDate;
}
@EqualsAndHashCode
public class MemberProductId implements Serializable{
        //IDE 자동 생성 기능
        private static final long serialVersionUID = 938018879231292890L;
        private Long member;
        private Long product;
}

 

 

 

JPA에서 복합 키를 사용하려면 별도 식별자 클래스 만들어야 한다.

@IdClass 식별자 클래스를 지정한다.

 

복합키를 위한 식별자 클래스 특징

  • 복합 키는 별도 식별자 클래스 필요
  • Serializable 구현
  • EqualsAndHashCode 구현
  • 기본 생성자 필수
  • public 클래스일
  • @IdClass @EmbeddedId 가능

 

식별 관계

MemberProduct Member Product 기본 키를 받아서 자신의 기본 키로 사용한다.

부모 테이블의 기본 키를 받아서 자신의 기본 + 외래 키로 사용하는 것을 DB 용어로 식별 관계라 한다.

 

 

 



        private static void save() {
                logic(em -> {
                        Member m1 = new Member();
                        m1.setUsername("멤버1");
                        em.persist(m1);
                        
                        Product p1 = new Product();
                        p1.setName("상품1");
                        em.persist(p1);
                        
                        MemberProduct memberProduct = new MemberProduct();
                        memberProduct.setMember(m1);
                        memberProduct.setProduct(p1);
                        memberProduct.setOrderAmount(10);
                        memberProduct.setOrderDate(LocalDateTime.now());
                        em.persist(memberProduct);
                        
                });
        }

 

 



        private static void find() {
                logic(em -> {
                        MemberProductId memberProductId = new MemberProductId();
                        memberProductId.setMember(1L);
                        memberProductId.setProduct(1L);
                        
                        MemberProduct memberProduct = em.find(MemberProduct.class, memberProductId);
                        Member member = memberProduct.getMember();
                        Product product = memberProduct.getProduct();
                        
                        System.out.println(member.getUsername());
                        System.out.println(product.getName());
                });
        }

 

 

복합키로 조회하려면 항상 식별자 클래스를 만들어야 한다.

기본키로 조회하는 것보다 번거롭다.

 

다대다 새로운 기본 사용

복합키를 사용하지 않고 간단히 다대다 관계를 구성하는 방법

 

새로운 기본 생성 전략은 DB에서 자동으로 생성해주는 대리 키는 사용하는 것을 추천

어떠한 비즈니스에 의존하지 않으며, 간편하고 거의 영구적이다.

ORDER_ID 키본 키로 잡고, MEMBER_ID, PRODUCT_ID 외래 키로만 사용한다.

참고, Mysql 예약어 문제로 Orders 끝에 s 붙였다.

 

 

 



@Entity
@Table(name = "Orders")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
        
        @Id
        @GeneratedValue
        @Column(name = "order_id")
        private Long id;
        
        @ManyToOne
        @JoinColumn(name = "member_id")
        private Member member;
        
        @ManyToOne
        @JoinColumn(name = "product_id")
        private Product product;
        
        private int orderAmount;
        private LocalDateTime orderDate;
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        private String username;
        
        @OneToMany(mappedBy = "member")
        private List<Order> orders = new ArrayList<>();
}


@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
        @Id
        @GeneratedValue
        @Column(name = "product_id")
        private Long id;
        private String name;
}


        private static void find() {
                logic(em -> {
                        Order memberProduct = em.find(Order.class, 1L);
                        Member member = memberProduct.getMember();
                        Product product = memberProduct.getProduct();
                        
                        System.out.println(member.getUsername());
                        System.out.println(product.getName());
                });
        }


        private static void save() {
                logic(em -> {
                        Member m1 = new Member();
                        m1.setUsername("멤버1");
                        em.persist(m1);
                        
                        Product p1 = new Product();
                        p1.setName("상품1");
                        em.persist(p1);
                        
                        Order memberProduct = new Order();
                        memberProduct.setMember(m1);
                        memberProduct.setProduct(p1);
                        memberProduct.setOrderAmount(10);
                        memberProduct.setOrderDate(LocalDateTime.now());
                        em.persist(memberProduct);
                        
                });
        }

복합 키를 사용하는 것보다, 엔티티 저장 조회가 간결해 졌다.

 

 

 

다대다 연관관계 정리

다대다 관계를 일대다 다대일 관계로 풀기 위한 연결 테이블을 만들 식별자를 어떻게 가져갈지 결정해야 한다.

  • 식별 관계
    받아온
    식별자를 기본 + 외래 키로 사용
  • 비식별 관계 (추천)
    받아온 식별자는 외래 키로만 사용, 새로운 식별자 추가

'개발 > JPA' 카테고리의 다른 글

08 프록시와 연관관계 관리  (1) 2024.05.13
07 고급 매핑  (0) 2024.05.06
05 연관관계 매핑 기초  (1) 2024.03.22
04 엔티티 매핑  (1) 2024.02.26
03 영속성 관리  (1) 2024.02.19

+ Recent posts