beadss / jpa-study

jpa슽터디입니다
1 stars 2 forks source link

5장 요약 #11

Open joont92 opened 5 years ago

joont92 commented 5 years ago

태아불(엔티티)이 서로 연관관계를 가질 때 드러나는 JPA와 SQL 패러다임 차이가 있다.
바로 조인참조이다.
SQL은 외래키라는 것을 통해 테이블끼리 관계를 가지고, 조인이라는 것을 통해 두 테이블의 모든 데이터에 접근 가능하다.
하지만 SQL의 경우 외래키만 있으면 어느쪽에서든 조회가 가능하다. 기본적으로 양방향이다.
하지만 객체에서는 이런 행위가 불가능하다.
엔티티간의 관계는 참조를 통해 형성된다.
클래스의 필드로 다른 클래스를 가지고 있어야하며, 한쪽으로만 접근, 즉 단방향 탐색만 가능하다.

이러한 패러다임을 풀기위해 생긴것이 방향이라는 개념이고, 여기서 단방향, 양방향의 개념이 나온다.

단방향 연관관계

기본적으로 클래스가 필드로 다른 클래스를 가질 때를 말한다.
아래는 자바가 순수하게 객체간 연관관계를 가지는 모습이다.

@Setter
@Getter
@Entity
class Mamber{
    private String id;
    private String username;
    private Team team;
}

@Setter
@Getter
@Entity
class Team{
    private String id;
    private String name;
}

psvm{
    Team team = new Team("avengers", "어벤져스");

    Member member1 = new Member("member1", "멤버1");
    Member member2 = new Member("member1", "멤버1");

    member1.setTeam(team);
    member2.setTeam(team);
}

이후 멤버에 소속된 팀을 찾으려면 아래와 같이 할 수 있다.

psvm{
    Team team = member1.getTeam();
    assertThat(team.getName(), is("어벤져스"));
}

이런식으로 참조를 통해 연관관계를 탐색하는 것을 객체 그래프 탐색 이라고 한다.

객체 관계 매핑

아래는 위의 객체듫을 JPA를 사용해서 매핑한 모습이다.

@Entity
class Member{
    private String id;
    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}
  1. @ManyToOne
    N:1의 관계라는 것을 나타내주는 어노테이션이다.
    Teamp 하나에 Member 여러개가 소속될 수 있기 때문이다.
    사용할 수 있는 옵션은 아래와 같다.
속성 기능 기본값
optional false로 설정하면 해당 엔티티가 항상 있어야한다. forien key not null과 동일한 의미가 된다. true
fetch lazy로딩, eager 로딩을 설정할 수 있다. - @ManyToOne = FetchType.EAGER
- @OneToMany = FetchType.LAZY
  1. @JoinColumn
    SQL과의 딜레마를 풀어야하기 때문에 존재해야 하는 부분이다.
    테이블에서 사용하는 외래키를 적어주면 된다.
    사용할 수 있는 옵션은 아래와 같다.
name 매핑할 외래키 이름 필드명 + _ + 참조하는 테이블의 기본 키 컬럼명
referencedColumnName 외래키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본키 컬럼명
foreignKey(DDL) 외래키 제약조건 설정 가능

연관관계 사용

저장

psvm{
    Team team = new Team("avengers", "어벤져스");
    em.persist(team);

    Member member1 = new Member("member1", "멤버1");
    Member member2 = new Member("member1", "멤버1");

    member1.setTeam(team);
    member2.setTeam(team);

    em.persist(member1);
    em.persist(member2);
}

이렇게 하면

INSERT INTO TEAM VALUES("avengers", "어벤져스");

INSERT INTO MEMBER VALUES("member1", "멤버1", "avengers");
INSERT INTO MEMBER VALUES("member2", "멤버2", "avengers");

처럼 team의 id값이 member의 외래키 값으로 세팅되어 저장된다.

엔티티 저장 시 연관된 모든 엔티티는 영속 상태여야 한다.
존재하는 엔티티라는 것이 보장되어야 하므로?

member 먼저 저장하고 team을 저장할 경우,
insert member -> insert team -> update member가 된다.. 왤까?

조회

psvm{
    Member givenMember = em.find(Member.class, "member1");
    Team givenTeam = givenMember.getTeam();
}

객체 그래프 탐색으로 매우 간단하게 찾아갈 수 있다.
(또는 JPQL로도 조회 가능하다)

수정

psvm{
    Team team = new Team("govengers", "고벤져스");

    Member givenMember = em.find(Member.class, "member1");
    givenMember.setTeam(team);
}

변경감지가 동일하게 동작하여 update문이 발생하게 된다.

연관관계 제거

psvm{
    Member givenMember = em.find(Member.class, "member1");
    givenMember.setTeam(null);
}

위처럼 null로 세팅해 연관관계를 제거해줄 수도 있다.
fk인 team_id가 null로 세팅된다.

삭제

psvm{
    member1.setTeam(null);
    member2.setTeam(null);

    em.remove(team);
}

연관관계를 제거해주지 않고 삭제할 경우 외래키 제약조건에 걸리므로, 관계를 제거를 선행해줘야 한다.

양방향 연관관계

현재는 Member -> Team의 관계만 형성되어있는데(객체지향 관점에서)
Team -> Member의 관계까지 추가하면 양방향 연관관계가 성립된다.
Member -> Team이 N:1 관계였으므로, Team -> Member는 1:N의 관계를 가진다.

관계는 반대편 관계에 달려있다. 반대편이 1:N 관계일 경우 N:1, 1:1일 경우 1:1 관계를 가진다.

자바에서 1:N의 관계를 표현하려면,
즉 여러건의 Member를 저장하려면 배열을 사용해야하는데,
JPA에서는 여기서 Collection을 사용한다.(List, Set, Map 등)

@Setter
@Entity
class Team{
    private String id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> memberList;
}

psvm{
    Team givenTeam = em.find(Team.class, "team");

    List<Member> givenMembers = givenTeam.getMembers();

    // do something...
}

근데 여기서 mappedBy라는 속성은 무엇일까?

연관관계의 주인

SQL의 경우 기본적으로 양방향 연관관계를 가지지만, 객체지향의 경우 양방향 연관관계라는 것이 애초에 없다.
단방향 연관관계 2개를 로직으로 잘 묶어서 양방향 연관관계처럼 보이게 하는 것 일 뿐이다.

SQL의 경우 외래키 하나로 연관관계 관리가 가능하지만,
객체지향의 경우 양방향 연관관계를 형성할 경우 양쪽에 참조를 명시해줘야 하므로, 관리해야 할 포인트가 2개이다.
결국 둘 중에 하나를 선택해줘야 하고, 이 하나를 연관관계의 주인 이라고 한다.

이 연관관계의 주인을 지정해주는 것이 아까 위에서 봤던 mappedBy 속성이다.
위에서 봤듯이 mappedBy 속성 지정에는 아래의 룰이 존재한다.

이 행위가 정확히 어떤일을 일어나게 해주는걸까...

연관관계의 주인은 외래키가 있는곳이다.

이 연관관계의 주인만이 연관관계와 매핑되는 외래키를 관리(등록, 수정, 삭제)할 수 있고,
주인이 아닌 쪽은 읽기만 가능하다.

기본적으로는 이렇고, cascade나 orphanRemoval 옵션을 사용하면 주인이 아니어도 컨트롤 할 수 있다(orphanRemoval은 좀 알아봐야함)

psvm{
    team.getMembers().add(member1); // 무시됨
    team.getMembers().add(member2); // 무시됨

    member.setTeam(team); // 설정됨
}

위의 현상에 주의해서 연관관계를 지정해줘야 한다.
setTeam을 호출하지 않으면 연관관게는 저장되지 않는다(Member 테이블의 team_id가 null로 들어갈 것이다)

연관관계 편의 메서드

사실상 연관관계 주인한테만 연관관계를 설정하는 위의 행위는 순수한 객체지향은 아니다.

save{
    member.setTeam(team);
    em.save(member);
}

get{
    List<Member> members = team.getMembers();
}

Team 내에 있는 List 에는 아무도 값을 넣어준적이 없는데,
get 메서드 처럼 값을 탐색해보면 List가 세팅된 채로 반환된다.
이는 hibernate라는 애가 중간에 있기 때문인데, 이는 순수 객체지향을 사용하고자 ORM을 사용하는 우리의 의도와는 어느정도 벗어나고, 실제로도 ORM Framework라는 애가 중간에 없으면 심각한 오류를 뿜뿜할 수 있는 코드들이다.

그러므로 안전하게 양쪽에 연관관계 편의 메서드들을 생성해주는 것이 좋다.

// member
setTeam(Team team){
    this.team = team;
    team.getMembers().add(this);
}

// team
addMembers(Member member){
    this.members.add(member);
    member.setTeam(this);
}

setMembers(List<Member> members){
    this.members = members;
    member.forEach(m -> m.setTeam(this));
}