5주차: MySQL, 연관 관계 매핑

이재혁·2023년 6월 27일
post-thumbnail
  • MySQL연동
<property name="javax.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="javax.persistence.jdbc.user" value="root"/>
            <property name="javax.persistence.jdbc.password" value="0000"/>
            <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/spring?serverTimezone=Asia/Seoul&amp;useSSL=false"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/>

pom.xml파일 변경
java11 사용시 작성

<dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
<!--        MySQL 데이터베이스-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.32</version>
        </dependency>
  • 용어 이해
    • 방향(Direction): 단방향, 양방향
    • 다중성: 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해
    • 연관관계의 주인(Owner) <<가장 중요하고 어렵다. JPA계의 C언어 포인터만큼의 난이도

객체가 지향하는 패러다임과 RDB가 지향하는 패러다임이 극심히 다르다.

단방향 연관관계

연관관계가 필요한 이유

  • 객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.

*주의

~~<property name="hibernate.hbm2ddl.auto" value="create" />~~

이 pom.xml 안의 옵션 코드를 입력해야 ddl문이 날라가면서 create가 된다..

💡 @Entity: JPA에서 엔티티란 쉽게 생각하면, DB 테이블에 대응하는 하나의 클래스. @Entity가 붙은 클래스는 JPA가 관리해주며, JPA를 사용해서 DB테이블과 매핑할 클래스는 @Entity를 꼭붙여야만 매핑이 가능하다.

위와 같은 sql문이 실행된 엔티티들을 조인한 결과이다.

Hibernate: 
    /* insert hellojpa.Team
        */ insert 
        into
            Team
            (name, TEAM_ID) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (TEAM_ID, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?)
  • 관계 설정이 되지 않는다면 JPA에 계속 요청을 해야하는 객체지향 스럽지 않은 일이 발생한다
//데이터를 찾을 때 계속 JPA에게 물어봐야하는 문제가 있다
            Member findMember = em.find(Member.class, member.getId());

            Long findTeamId = findMember.getTeamId();
            Team findTeam = em.find(Team.class, findTeamId);

객체를 테이블에 맞추어 데이터중심으로 모델링하면, 협력 관계를 만들 수 없다.

  • 테이블은 외래키로 조인을 사용해서 연관된 테이블을 찾는다.
  • 객체는 참조를 사용해서 연관된 객체를 찾는다.
  • 테이블과 객체 사이에는 이런 큰 간격이 있다.

(객체가 참조를 이용해 연관된 객체를 찾는것을 위 코드에서 확인할 수 있다.)

단방향 연관관계

가장 중요하고 가장 기본이다.

member 클래스의 teamId를 team으로 바꾸고 연관관계를 설정하면 연관관계를 활용하여

연관된 테이블을 찾아 컬럼들을 바로 활용할 수 있다

Member.class

//    @Column(name = "TEAM_ID")
//    private Long teamId;

    @ManyToOne //멤버 입장에서는 Many고 Team입장에서는 One이기 떄문이다.
    @JoinColumn(name = "TEAM_ID")
    private Team team;

JpaMain.class

Member findMember = em.find(Member.class, member.getId());

            Team findTeam = findMember.getTeam();
            System.out.println("findTeam.getName() = " + findTeam.getName());

실행된 sql문

findTeam.getName() = TeamA //변수 출력 성공
Hibernate: 
    /* insert hellojpa.Team
        */ insert 
        into
            Team
            (name, TEAM_ID) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (TEAM_ID, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?)

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

양방향 매핑

  • 놀랍게도 양방향와 단방향 모두 테이블은 차이가 없다
    • Member와 Team 각각에서 TEAM_ID로 JOIN을 하면 알 수 있다.
    • 즉, 테이블의 연관관계는 외래키 하나로 양방향이 다 있는것이다.
    • 테이블의 연관관계에는 방향이라는 개념 자체가 없다.
  • 문제는 객체
  • mappedBy란?
    • 나는 뭐랑 연결되어 있지?에 대한 대답 나의 반대편 테이블에 있는 연결된 변수명과 매핑되어 있음을 나타낸다

연관관계의 주인과 mappedBy

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

  • 객체 연관관계 = 2개
    • 회원 → 팀 연관관계 1개(단방향)
    • 팀 → 회원 연관관계 1개(단방향)
  • 테이블 연관관계 = 1개
    • 회원 ↔ 팀의 연관관계 1개(양방향)
      • 사실 방향이 없는 무방향이다.

객체의 양방향 관계

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

테이블의 양방향 연관관계

  • 양쪽 조인이 가능하므로 양방향 연관관계를 가진다

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

외래 키 값을 관리하기 위해 딜레마에 빠진다.

이를 해결하기 위해 연관관계의 주인을 설정한다.

연관관계의 주인(Owner)

양방향 매핑 규칙

  • 객체의 두 관계중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정)
  • 주인이 아닌 쪽은 읽기만 가능
  • 주인은 mappedBy 속성 사용X
  • 주인이 아니면 mappedBy 속성으로 주인을 지정

누구를 주인으로?

Member.class

@ManyToOne //멤버 입장에서는 Many고 Team입장에서는 One이기 떄문이다.
    @JoinColumn(name = "TEAM_ID")
    private Team team;

Team.class

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

⇒ 주인이 team 이다. 따라서, 중요한 점은

team.getMembers하면 연관된 team을 조회하는 것은 되는데,

값을 변경(update, insert) 시에는 private Team team 만 참조한다.

  • 외래키가 있는 곳을 주인으로 정해라
  • 여기서는 Member.team이 연관관계의 주인

  • 외래키가 있는 곳을 주인으로 딱 정하면 된다.
  • DB의 N쪽이 무조건 주인이 된다.(N쪽이 FK이므로)
    • 다 쪽이 연관관계의 주인이 된다.

**https://www.youtube.com/watch?v=brE0tYOV9jQ - 해당 문제 해결해보기**

소감: hibernate 발음하는 아기가 굉장히 귀엽다.

정답: Book이 n이 되고, 연관관계의 주인이 된다. Bookstore는 참조되는 pk를 갖고 있고

mappedBy가 사용되어 읽기만 가능한 상태라 아무리 Bookstore에 Book을 꽂아도 저장이나 수정 삭제가 되지 않는다.

mappedBy 시 FK가 생성되므로 이 시점부터 FK를 수정해야(관계의 주인) 데이터베이스가 수정됨을 명심해야 한다.

Bookstore.add(book 객체) 시에 연관관계의 주인인 book 객체의 bookstore를 수정하는 코드를 Bookstore의 add 메소드에 추가해야 한다.

⇒객체(Books List)에 단순히 집어 넣는게 아닌 관계형 데이터베이스(Book)에 데이터를 집어 넣어주는 행위를 한다.

public void add(Member member) {
member.setTeam(this); // RDB에 데이터추가
this.members.add(member); // 객체의 배열에 단순히 추가
}

Team.class

public class Team {

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

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

    public void add(Member member) { 
        member.setTeam(this);  // 이 코드 한 줄이 없으면 fk에 null이 뜬다.
        this.members.add(member); // Team 객체에 멤버를 배열에 추가한다.
    }

}

Member.class

package hellojpa;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Getter @Setter
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    @Column(name = "USERNAME")
    private String username;

    //    @Column(name = "TEAM_ID")
//    private Long teamId;

    @ManyToOne //멤버 입장에서는 Many고 Team입장에서는 One이기 떄문이다.
    @JoinColumn(name = "TEAM_ID")
    private Team team;

}

JpaMain.class

try {

            //저장
            Team team = new Team();

            team.setName("TeamA");
            em.persist(team);

            Member member = new Member();
            member.setUsername("member2");

            team.add(member); //새로 만든 팀 객체에 새로 만든 멤버를 추가하려고 한다.

            em.persist(member);

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

            List<Member> members = team.getMembers();
            System.out.println("members = " + members); //Team 클래스의 배열 저장 코드가 없으면 null

            tx.commit();
}

mappedBy가 없으면

@OneToMany

@ManyToOne

이 두개가 각각의 Book, BookStore 클래스에 존재하는데

이 경우 의미는 단방향 2개가 존재하는 상황이라는 의미다.

⇒ 스키마가 전혀 다르다. bookstore, books의 조인 테이블이 새로 생긴다.

mappedBy를 함으로써 양방향 관계가 되는 것이다.

⇒ 조인 테이블이 새로 생기지 않고 book(연관관계의 주인) 쪽에 FK가 생성된다.

단, 양방향 관계는 mappedBy만 쓰는 방법만 있는 것이 아니다.

강의와 내 코드가 달랐던 점.. JoinColumn의 생략

[@JoinColumn의 생략]

만약 @JoinColumn을 생략한다면, 외래 키를 찾을 때 다음 기본 전략을 사용합니다.

필드명_[참조하는 테이블의 기본 키 컬럼명]

[예시]

@ManyToOne
private Team team;

Joincolumn의 name속성은 그저 member테이블의 조인컬럼의 컬럼명을 지정하는것뿐 team과 매핑하는것과는 관련이없다.

member와 team을 연관관계맺는것은 referencedColumnName속성이 하지만 이것을 생략하면 자동으로 team의 pk값으로 연관관계를 맺어줘서 생략한다.

@Getter, @Setter ⇒ 롬복 설치

Getter, Setter 애노테이션으로 긴 코드 대체 가능

다대일 [N:1]

JPA에서 가장 많이 사용하고 기초가 되는 다대일

  • 가장 많이 사용하는 관계

일대다 [1:N]

일대다 단방향의 문제점

  • 여러 테이블이 존재 시 수정하기가 어렵다.
  • 양방향으로 만드는 것이 유지보수가 더 낫다

일대다 단방향 정리

  • 일대다 단방향은 1이 연관관계의 주인
  • 테이블 일대다 관계는 항상 다 쪽에 외래 키가 있다
  • 객체와 테이블의 패러다임 차이때문에 반대편 테이블 FK를 관리하는 특이한 구조
  • @JoinColumn을 꼭 사용해야 한다.
  • 일대다 단방향 매핑의 단점
    • 엔티티가 관리하는 외래 키가 다른 테이블에 있음
    • 연관관계 관리를 위해 추가로 UPDATE SQL 실행
  • 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자

일대다 양방향

@JoinColumn(name = “연결될 컬럼명”, insertable = “false”, updatable=”false”)

⇒읽기 전용 매핑

  • 이런 매핑은 공식적으로 존재X
  • 읽기 전용 필드를 사용해서 양방향처럼 사용하는 방법
  • 다대일 양방향을 사용하자

일대일 [1:1]

  • 일대일 관계는 그 반대도 일대일
  • 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
    • 주 테이블에 외래 키
    • 대상 테이블에 외래 키
  • 외래 키에 데이터베이스 유니크 제약조건 추가

일대일: 주 테이블에 FK 양방향

한쪽에 mappedBy를 걸어주면 되서 다대일 양방향이랑 비슷하다.

  • 가장 Simple하다

일대일: 대상 테이블에 외래 키 단방향

  • 단방향 관계는 JPA지원 X
  • 양방향 관계는 지원

주 테이블에 FK 단방향

  • Member에 FK가 있어서 따로 Join할 필요 없이 주테이블만으로 데이터를 볼 수 있어 효율적

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

다대다 [N:M]

실무에서 쓰면 안된다.

  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다
  • 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야한다

객체는 다대다가 된다

다대다 매핑의 한계

  • 연결 테이블이 단순히 연결만 하고 끝나지 않는다
  • 주문시간, 수량 같은 데이터가 들어올 수 있다

중간테이블이 숨겨져 있기 때문에 예측하지 못한 쿼리문이 나갈 수도 있다

다대다 한계 극복

  • 중간 테이블을 하나 더 생성해서 @OneToMany @ManyToOne 설정

이렇게 되면 중간테이블(Orders)에 원하는 필드를 맘껏 추가할 수 있다

ex) price, count…

  • 왠만하면 PK는 비즈니스적 의미 없는 값을 써야한다.
    • 2개의 컬럼이 묶여서 pk를 이루면 의미가 생긴다.⇒ 변경 시 대규모 업데이트 필요
profile
서비스기업 가고 싶은 대학생

0개의 댓글