ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 연관관계 매핑 기초
    JPA 2022. 7. 27. 03:41

    해당 내용은

    자바 ORM 표준 JPA 프로그래밍 - 기본편 (https://www.inflearn.com/course/ORM-JPA-Basic/dashboard)

    강의를 듣고 정리한 포스트입니다.

     

    단방향 연관관계

     

    목표

    • 객체와 테이블 연관관계의 차이를 이해

    • 객체의 참조와 테이블의 외래 키를 매핑

     

    • 용어 이해

    • 방향(Direction): 단방향, 양방향

     

    • 다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1),

    다대다(N:M) 이해

     

    • 연관관계의 주인(Owner): 객체 양방향 연관관계는 관리 주인

     이 필요

     

     

    목차

    • 연관관계가 필요한 이유

    • 단방향 연관관계

    • 양방향 연관관계와 연관관계의 주인

    • 실전 예제 - 2. 연관관계 매핑 시작

     

    연관관계가 필요한 이유

     

    ‘객체지향 설계의 목표는 자율적인 객체들의

    협력 공동체를 만드는 것이다.

     

    –조영호(객체지향의 사실과 오해)

     

    예제 시나리오

    • 회원과 팀이 있다.

    • 회원은 하나의 팀에만 소속될 수 있다.

    • 회원과 팀은 다대일 관계다.

     

     

     

    객체를 테이블에 맞추어 모델링

    (연관관계가 없는 객체)

     

     

    객체를 테이블에 맞추어 모델링

    (참조 대신에 외래 키를 그대로 사용)

    예제는 기존 1번째 작성한 예제를 사용한다.

     

     

    객체를 테이블에 맞추어 모델링

    (식별자로 다시 조회, 객체 지향적인 방법은 아니다.)

     

    join쿼리 쓸때

    ansi 표준 쿼리문으로 쓰는게 좋다 .

    그래야 다른 데이터 베이스로 변경되어도 문제가 없다.

     

    객체를 테이블에 맞추어 데이터 중심으로 모델링하면,

    협력 관계를 만들 수 없다.

    • 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.

    • 객체는 참조를 사용해서 연관된 객체를 찾는다.

    • 테이블과 객체 사이에는 이런 큰 간격이 있다.

     

    단방향 연관관계

     

    객체 지향 모델링

    (객체 연관관계 사용)

     

     

     

    JpaMain

    package hello.jpa;


    import javax.persistence.EntityManager;
    import javax.persistence.EntityManagerFactory;
    import javax.persistence.EntityTransaction;
    import javax.persistence.Persistence;
    import java.util.Arrays;
    import java.util.List;


    public class JpaMain {
        public static void main(String[] args) {
            EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");


            EntityManager em = emf.createEntityManager();


            EntityTransaction tx = em.getTransaction();
            tx.begin();
            try {
                //저장
                Team team = new Team();
                team.setName("TeamA");
                em.persist(team);


                Member member = new Member();
                member.setName("member1");
                member.setTeam(team);
                em.persist(member);


                //1차 캐시에서 가져오기 때문에 영속성 콘텍스트를 초기야 해야 쿼리를 볼수 있다.
                em.flush();
                em.clear();


                Member findMember = em.find(Member.class,member.getId());
                Team findTeam = findMember.getTeam();
                System.out.println("findTeam = " + findTeam.getName());


                //이런식으로 하면 fk가 업데이트 된다. (100번 ID를 있다 치고)
    //            Team newTeam = em.find(Team.class, 100L);
    //            findMember.setTeam(newTeam);


                tx.commit();
            } catch (Exception e) {
                tx.rollback();
            } finally {
                //close
                em.close();
                emf.close();
            }
        }
    }

     

    Team.java

    package hello.jpa;


    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;


    @Entity
    public class Team {
        @Id
        @GeneratedValue
        @Column(name = "TEAM_ID")
        private Long id;
        private String name;


        public Long getId() {
            return id;
        }


        public void setId(Long id) {
            this.id = id;
        }


        public String getName() {
            return name;
        }


        public void setName(String name) {
            this.name = name;
        }
    }

     

    양방향 연관관계와 연관관계의 주인

    여기가 중요하다

    여기가 어려움

     

    어렵냐 ?

    객체랑 테이블의 패러다임의 차이

    객체는 참조

    테이블은 외래키 조인

    차이에서 오는 이해

     

     

    양방향 매핑

     

    테이블은 fk 넣으면 양방향으로 있다.

    객체는 양쪽으로 가려면 members라는 List 셋팅해야함

     

    양방향 매핑

    (Member 엔티티는 단방향과 동일)

    @Entity
    public class Member {


        @Id @GeneratedValue
        @Column(name="MEMBER_ID")
        private Long id;
        @Column(name = "USER_NAME")
        private String name;
        //객체 지향적이지 않은 설계
        //@Column(name="TEAM_ID")
        //private Long teamId;


        @ManyToOne //현재 객체(Member)의 입장에서 생각을 해야한다. 멤버가 여러명에 팀이 1개 이므로 ManyToOne
        @JoinColumn(name = "TEAM_ID")
        private Team team;

    양방향 매핑

    (Team 엔티티는 컬렉션 추가)

    @Entity
    public class Team {
        @Id @GeneratedValue
        @Column(name = "TEAM_ID")
        private Long id;
        private String name;
        @OneToMany(mappedBy = "team")//1대다에서 뭐랑 연결되어있지 , 반대편 사이드의 변수명을 적어줘야햔다.
        private List<Member> members= new ArrayList<Member>();

     

    JPAMain

    package hello.jpa;


    import javax.persistence.EntityManager;
    import javax.persistence.EntityManagerFactory;
    import javax.persistence.EntityTransaction;
    import javax.persistence.Persistence;
    import java.util.Arrays;
    import java.util.List;


    public class JpaMain {
        public static void main(String[] args) {
            EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");


            EntityManager em = emf.createEntityManager();


            EntityTransaction tx = em.getTransaction();
            tx.begin();
            try {
                //저장
                Team team = new Team();
                team.setName("TeamA");
                em.persist(team);


                Member member = new Member();
                member.setName("member1");
                member.setTeam(team);
                em.persist(member);


                //1차 캐시에서 가져오기 때문에 영속성 콘텍스트를 초기야 해야 쿼리를 볼수 있다.
                em.flush();
                em.clear();


                Member findMember = em.find(Member.class,member.getId());
                Team findTeam = findMember.getTeam();
                System.out.println("findTeam = " + findTeam.getName());
                // 양방향 연결 셋팅 후 start
                List<Member> members = findMember.getTeam().getMembers();


                for (Member m : members) {
                    System.out.println("m = " + m.getName());
                }
                // 양방향 연결 셋팅 후 End


                //이런식으로 하면 fk가 업데이트 된다. (100번 ID를 있다 치고)
    //            Team newTeam = em.find(Team.class, 100L);
    //            findMember.setTeam(newTeam);


                tx.commit();
            } catch (Exception e) {
                tx.rollback();
            } finally {
                //close
                em.close();
                emf.close();
            }
        }
    }

     

    JPA 스팩설계 이렇게 되었는지 이해가 필요

     

    연관관계의 주인과 mappedBy

    • mappedBy = JPA의 멘탈붕괴 난이도

    • mappedBy는 처음에는 이해하기 어렵다.

    • 객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.

     

    객체와 테이블이 관계를 맺는 차이

    • 객체 연관관계 = 2

    • 회원 -> 팀 연관관계 1개(단방향)

    • 팀 -> 회원 연관관계 1개(단방향)

     

    객체는 사실 단방향 연관관계가 2개이다.

     

    • 테이블 연관관계 = 1

    • 회원 <-> 팀의 연관관계 1개(양방향)

     

    그냥 테이블은 양방향 연관관계밖에 없다 .

    FK 모든 연관관계가 끝이난다.

     

    두가지가 차이가 있다.

     

     

    객체의 양방향 관계

    • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단

    뱡향 관계 2개다.

    • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어

    야 한다.

     

    테이블의 양방향 연관관계

    • 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리

    • MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계 가짐

    (양쪽으로 조인할 수 있다.)

    둘 중 하나로 외래 키를 관리해야 한다.

     

    둘중에 뭘로 매핑해야되는거야~~

    이상하다 .

     

    Team member 바꾸고 싶을

    Member 멤버객체인 Team 변경해야되는지

    Team members 객체를 변경해야하는 건지

    헷갈린다.

     

    DB 그냥 외래키 값만 업데이트하면

     

    어떻게해야되

     

    명확한 차이가 있다.

     

    둘중 하나로 정해야된다.

     

    둘중의 주인을 정해서 거기서 관리를해야됨

     

    연관관계의 주인(Owner)

    양방향 매핑 규칙

    • 객체의 두 관계중 하나를 연관관계의 주인으로 지정

    • 연관관계의 주인만이 외래 키를 관리(등록, 수정)

    • 주인이 아닌쪽은 읽기만 가능

    • 주인은 mappedBy 속성 사용X

    • 주인이 아니면 mappedBy 속성으로 주인 지정

     

     

    누구를 주인으로?

    • 외래 키가 있는 있는 곳을 주인으로 정해라

    • 여기서는 Member.team이 연관관계의 주인

     

    외래키가 있는 곳을 주인으로 정해라

    그러면 여러 고민거리가 해결이됨

    DB입장에서 보면 외래키가 있는 곳은 무조건 (Many)이다.

    DB N쪽이 연관관계의 주인이됨

    ManyToOne()

    DB테이블에 Many 인쪽인 연관관계 주인이다.

     

     

     

    양방향 매핑시 가장 많이 하는 실수

    (연관관계의 주인에 값을 입력하지 않음)

     

    Member 객체의 Team 연관관계의 주인

    Team Memebers 연관관계의 주인이 아닌 mappedBy 존재

    읽기 전용이므로 insert 되지 않음

     

     

    양방향 매핑시 연관관계의 주인에 값을 입력해야 한다.

    (순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야 한다.)

     

    사실 양쪽에 값을 넣어주는 맞음

     

    em.flush, clear 사용하지 않고  영속성컨텍스트  1차캐시에 저장해둔 상태라면

    1. Team 등록함 (1.Team1)
    2. Member  등록시 team 위에서 등록한 team 으로 설정함
    3. Team에서 getMembers 등록 Members 위에서 생성한 Team 셋팅함
    4. 영속성 컨텍스트에서 id 팀객체를 호출함
    5. 팀에서 멤버 객체를 가져옴 (결과 아무것도 없음)

     

    한번 더해봐야겠다.

                //저장
                Team team = new Team();
                team.setName("TeamA");
                em.persist(team);


                //1차캐시에 Team만 등록됨 | 1L | TeamA |


                Member member = new Member();
                member.setName("member1");
                member.setTeam(team);
                em.persist(member);


                //1차캐시에서 Member 등록함 | 1L | member1 | TeamA |


                //1차 캐시에서 가져오기 때문에 영속성 콘텍스트를 초기야 해야 쿼리를 볼수 있다.
    //            em.flush();
    //            em.clear();


                Team findTeam = em.find(Team.class,team.getId()); //1차 캐시에 있는 Team 객체를 가져옴 , 그러나 아직 쿼리가 안날라갔기때문에 Members에 객체가 셋팅이 안되어있는 상태
                List<Member> members = findTeam.getMembers();
                System.out.println("==========================");
                //쿼리문으로 연관관계가 맵핑되지 않은 상태인 순수 객체이므로 멤버가 나오지 않는다.
                for (Member m : members) {
                    System.out.println("m = " + m.getName());
                }
                System.out.println("==========================");




                tx.commit();

     

    양방향 연관관계 주의 - 실습

    순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자

    • 연관관계 편의 메소드를 생성하자

    Member 객체의 setTeam() [Team을셋팅시 자기자신을 team 셋팅해준다.]

        public void setTeam(Team team) {
            this.team = team;
            team.getMembers().add(this);
        }

    연관관계가 관련될 시에는 다음과같이 이름을 변경해준다.

    member.changeTeam(team);

    (getter,Setter) 관례에 의한게 아닌것을 표시

     

    연관관계 편의 메서드는 연관관계 주인에서 구현할지 아니면 대상에서 구현할지는 정해야한다 (둘중 한군데서만 하는게 맞음)

     

    • 양방향 매핑시에 무한 루프를 조심하자

    • 예: toString(), lombok, JSON 생성 라이브러리

     

    만약에 양방향 관계에서 toString  호출 Member Team 있어서 호출

    Team toString 호출 , toString 메서드안에 Members 호출 각각의

    멤버들의 toString 호출 -> 다시 멤버가 Team toString 호출한다.

    그러므로 무한루프에 빠져버림

    롬복이나

    Json생성라이브러리 사용시에도 Json 객체 같은 것을 만들

    내부의 객체를 서로 호출해서 무한루프에 빠져버림

     

    • 결론 : Lombok에서 toString 만드는거 왠만하면 쓰지마라 ,써도 저런 객체는 빼고써라
    • Json 생성라이브러리 같은경우 ,Controller에서 절대 Entity 절대 반환하지마라
    • Entity DTO 변환해서 반환해라

     

     

    양방향 매핑 정리

    • 단방향 매핑만으로도 이미 연관관계 매핑은 완료

    • 설계할때 단방향 매핑으로 설계를 완료를 해야됨
    • 양방향으로 설계해서 좋을게 없음
    • 언제 양방향으로 언제 하냐 , 실무에서 하다보면 양방향으로 하게될 때가 있음
    • 일단 기본은 단방향 매핑으로 끝난다 개발하는 단계에서 필요할때 추가해도 늦지않음

     

    • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐

    • JPQL에서 역방향으로 탐색할 일이 많음

    • 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨 (테이블에 영향을 주지 않음)

     

    연관관계의 주인을 정하는 기준

    • 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안됨

    • 연관관계의 주인은 외래 키의 위치를 기준으로 정해야함

     

     

    실전예제 - 2. 연관관계 매핑 시작

     

    단방향 연관관계를 매핑하는 것이 중요

     

    Member orders 넣는 것은 좋은 설계는 아님

    연관관계를 끊어 내는게 중요함

     

    주문이 필요하면 주문으로 부터 실행하면

     

     

     

    실전에서 JPQL 양방향연관관계를 걸게 된다.

     

    핵심을 단방향으로 웬만하면 단방향으로 짜라

    'JPA' 카테고리의 다른 글

    프록시와 연관관계 관리  (0) 2022.08.06
    고급 매핑  (0) 2022.07.30
    다양한 연관관계 매핑  (0) 2022.07.29
    엔티티 매핑  (0) 2022.07.26
    영속성 관리 - 내부 동작 방식  (0) 2022.07.26
Designed by Tistory.