Skip to content

Latest commit

 

History

History
435 lines (305 loc) · 13.4 KB

4.연관관계.md

File metadata and controls

435 lines (305 loc) · 13.4 KB

1. 연관관계

  • 회원과 팀
  • 회원은 하나의 팀에만 소속될수 있다
  • 팀에는 많은 회원이 있을수 있다.

객체 테이블에 맞춘 모델링

연관관계가 필요한 이유..

image

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

@Entity
public class Team{
    @Id
    @GeneratedValue
    private Long id;
    
    @Column(name = "TEAM_NAME")
    private String name;
}
//팀 저장
Team team = new Team();
team.setName("Chelsea");
em.persist(team);

//회원 저장
Member member = new Member();
member.setName("Kane");
member.setTeamId(team.getId());
em.persist(member);
//회원의 팀 찾기
Member findMember = em.find(Member.class, member.getId());

Long findTeamId = findMember.getId();
Team findTeam = em.find(Team.class, findTeamId);
  • 위의 코드들은 연관관계 없이 그저 식별값을 통해서 거치고 거쳐서 찾아서 많은 비용이 소모됨
  • 객체 지향스럽지 못한 코드
  • 객체를 테이블에 맞춰 데이터 중심으로 모델링하면, 협력관계를 위 코드처럼 만들 수 없다
    • 테이블은 외래 키 조인을 통해 연관된 테이블을 찾음
    • 객체는 객체 참조를 사용해서 연관된 객체를 찾음

2. 단방향 연관관계

image

객체 연관관계

  • Member엔티티에는 Member.team 필드로 Team엔티티와 연관관계를 맺음
  • 회원 객체와 팀 객체는 단방향 관계
    • 회원은 Member.team을 통해 팀을 알수 있다
    • 팀은 소속 회원을 알수 없다.

테이블 연관관계

  • Member테이블은 Team테이블의 id값을 외래키로 연관관계를 맺음
  • 회원 테이블과 팀 테이블은 양방향 관계
    • 회원과 팀 테이블은 MEMBER JOIN TEAM 또는 TEAM JOIN MEMBER로 서로 연관되는 테이블의 값을 가져올수 있다.

즉, 테이블 연관관계는 외래키를 하나만 설정해줘도 양방향이 가능. 하지만 참조를 통한 객체 연관관계는 항상 단방향.

3. 다대일

-엔티티 선언

@Entity
public class Member{
    @Id @GeneratedValue
    private Long id;
    
    @Column(name = "USERNAME")
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

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

-연관관계 저장

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

//회원 저장
Member member = new Member(); 
member.setName("Kane");
member.setTeam(team); //단방향 연관관계 설정, 참조 저장 
em.persist(member);

-참조로 연관관계 조회 - 객체 그래프 탐색

//조회
Member findMember = em.find(Member.class, member.getId());
//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();

-연관관계 수정

// 새로운 팀B
Team teamB = new Team();
teamB.setName("MU");
em.persist(teamB);

// 회원1에 새로운 팀B 설정
member.setTeam(teamB);

@ManyToOne

  • 다대일 관계의 매핑 정보
  • 팀은 다수의 회원을 가질수 있다.
  • 속성
    • fetch : 패치 전략
      • Eager : 해당 엔티티를 조회할때 연관관계에 있는 엔티티도 함께 가져온다.
      • Lazy : 해당 엔티티를 조회할때 엔티티를 조회할 때 그제서야 가져온다.
    • cascade : 영속성 전이 기능을 사용
    • targetEntity : 연관된 엔티티의 타입 정보를 설정(사용x)
    • optional : false로 설정하면 연관된 엔티티가 항상 있어야한다.

@JoinColumn

  • 외래키를 매핑할 때 사용
  • 속성
    • name : 매핑할 외래 키 이름("필드명"+_+"참조 테이블 기본키")
    • referencedColumnName : 외래 키가 참조하는 대상 테이블의 컬럼명
    • freignKey(DDL) : 외래 키 제약조건을 직접 지정할수있다, 테이블을 생성할 때만 사용

4.양방향 연관관계

image

객체 연관관계

  • Member엔티티에서 Member.team필드를 통해 연관관계를 맺음
    • Member.team을 통해 Team을 찾을수 있다
  • Team엔티티에서 Team.members.get()를 통해 소속된 Member를 찾을수 있다.
  • 서로 연관된 엔티티에서 서로를 찾을수 있음으로 양방향 연관관계

image

테이블 연관관계

  • 두 테이블 다 JOIN을 통해 서로를 찾을수 있다.
  • 데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할수 있다.

양방향 연관관계 매핑

-엔티티 선언

@Entity
public class Member{
    @Id
    @Column(name = "MEMBER_ID")
    private string id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    public void setTeam(Team team){
        this.team = team;
    }
}


@Entiy
public class Team{
    @Id
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
}

-일대다 컬렉션 조회

// member1: (id=1, username="회원1", team="team1")
// member2: (id=2, username="회원2", team="team1")

Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers(); // (팀 -> 회원) 객체 그래프 탐색

for (Member member : members) {
System.out.println("member.username = " + member.getUsername());
}

// === 결과 ===
// member.username = 회원1
// member.username = 회원2

5.연관관계 주인

  • 위의 코드에서 Member.getTeam()과 Team.getMembers()를 통해 양방향 연관관계를 확인했다.
  • 하지만 객체에는 양방향 연관관계라는 개념이 존재하지 않는다.
  • 서로 다른 단방향 연관관계 2개를 로직으로 묶어 양방향처럼 보이게 한것 뿐이다.(회원 -> 팀, 팀 -> 회원)
  • 하지만 데이터베이스 테이블은 외래키 하나로 양방향 연관관계를 맺는다.
  • 엔티티를 양방향 연관관계로 설정함으로써 객체의 참조는 둘인데, 외래 키는 하나라서 차이가 발생한다.
  • 그렇기 떄문에 하나의 엔티티에서 테이블의 외래키를 관리해 줘야한다. 즉, 객체를 관리해 주는 객체를 연관관계의 주인이라고 한다.

양방향 매핑의 규칙 : 연관관계의 주인

양방향 연관관계를 매핑할때 꼭 지켜줘야할 규칙이 있다. 테이블의 외래키를 관리해줄 연관관계의 주인을 두 연관관계 중 하나의 엔티티로 정해야하는 것이다.

연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(CRUD)할수 있다. 반대로 주인이 아닌 엔티티는 오직 읽을수만 있습니다.

주인의 여부는 mappedby속성의 여부로 결정된다.

주인은 외래키를 가지고 있는 엔티티 즉, Team을 가지고 있는 Member가 주인이 되고 Team은 참조하고 있는 members에 mappedBy속성을 설정해주면 된다.

쉽게 얘기하면 다대일(N:1)에서 N인 엔티티가 주인이다.(@OneToMany에는 mappedBy 속성이 있지만, @ManyToOne에는 mappedBy속성이 없다.)

image

image

  • 여기서는 Member.team이 주인, Team.members가 주인이 아니게된다.
  • Team.members에 mappedBy="team"속성을 사용해서 주인이 아님을 알려준다.
  • mappedBy의 team은 연관관계의 주인인 Member엔티티의 team필드

6.양방향 연관관계 저장

-팀과 회원 저장

public void save(){
    //팀1 저장
    Team team1 = new Team("team1", "팀1");
    em.persist(team1);
    
    //회원1 저장
    Member member1 = new Member("member1", "회원1");
    member1.setTeam(team1);
    em.persist(member1);
    
    //회원2 저장
    Member member2 = new Member("member2","회원2");
    member2.setTeam(member2);
    em.persist(member2);
}
  • 주인인 Member엔티티에서는 연관관계인 Team엔티티를 추가시켜주면 데이터베이스에 추가가된다.

  • 주인이 아닌 Team엔티티에는 값을 설정하지 않아도 데이터베이스에 값을 설정하지 않아도 데이터베이스에 외래 키 값이 정상 입력된다.

  • team1.getMembers().add(member1); //무시(연관관계의 주인이 아님)
    team1.getmembers().add(member2); //무시(연관관계의 주인이 아님)

    위의 코드도 있어야 할거같지만 Team.members는 연관관계의 주인이 아니기때문에 주인이 아닌 곳에서 입력된 값은 외래키에 영향을 주지 않는다.

-데이터베이스

Member USERNAME TEAM_ID
member1 회원1 team1
member2 회원2 team1

-객체 그래프 탐색

//회원에서 팀 찾기
Member findMember = em.find(Member.class,"member1");
Team findTeam = findMember.getTeam();
System.out.println(findTeam.getId()); //team1

//팀에서 회원 찾기
Team findTeam = em.find(Team.class,"team1");
List<Member> findMembers = findTeam.getMembers();
for(Member m:findMembers){
    System.out.println(m.getId()); //member1, member2
}

7. 순수한 객체까지 고려한 양방향 연관관계

위의 코드에서 주인이 아닌 Team엔티티의 members에 값을 넣어줘도 영향을 받지 않기때문에 사용하지 않아도 된다고 생각한다.

하지만 우리는 객체지향코드를 작성해야하기 때문에 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 안전하다.

양쪽 방향에 값을 모두 입력해주지 않는다면 JPA에 영향을 받지 않는 순수한 객체에서 문제가 발생할수 있다.

public void test(){
    Team team1 = new Team("team1","팀1");
    Member member1 = new Member("member1","회원1");
    Member member2 = new Member("member2","회원2");
    
    member1.setTeam(team1);
    member2.setTeam(team1);
    
    List<Member> members = team1.getMembers();
    System.out.println(members.size()); // 0
}
  • 위의 코드에서 보시는 바와 같이 연관관계의 주인인 member에게만 team1을 선언해주었다.
  • JPA에 영향을 받지않는 순수한 객체의 관점에서 봤었을 때는 주인에게만 연관관계를 설정해줘도 반대 방향은 연관관계를 알수 없게되는 것이다.
public void test(){
    Team team1 = new Team("team1","팀1");
    Member member1 = new Member("member1","회원1");
    Member member2 = new Member("member2","회원2");
    
    member1.setTeam(team1);
    team1.getMembers().add(member1);
    
    member2.setTeam(team2);
    team2.getMembers().add(member2);
    
    List<Member> members = team1.getMembers();
    System.out.println(members.size()); // 2
}
  • 이렇게 주인이지 않는 엔티티에도 연관관계를 설정해준다면 우리가 원하는 양방향 연관관계가 된다.

-JPA를 사용한 예제

public void test(){
    Team team1 = new Team("team1","팀1");
    em.persist(team1);
    
    Member member1 = new Member("member1","회원1");
    member1.setTeam(team1);
    team1.getMembers().add(member1);//1
    em.persist(member1);
    
    Member member2 = new Member("member2","회원2");
    memeber2.setTeam(team1);
    team1.getmembers().add(member2);//2
    em.persist(member2);
}
  • 이렇게 설정해준다면 순수한 객체 상태에서도 동작을하고 테이블 외래 키도 정상 입력된다.
  • 명심할것은 1,2 코드를 작성하지 않아도 테이블 외래키는 정상 입력된다는 것이다.

8.편의메소드

편의 메소드란 양방향일때 결국 순수 객체와 테이블 외래키 양쪽 다 신경써야 한다.

member.setTeam(team)과 team.setMembers().add(member)를 각각 호출하다보면 실수로 둘 중 하나만 호출해서 양방향이 깨질수도 있다.

그렇기 떄문에 연관관계의 주인인 Member엔티티에서 setTeam()메서드를 리팩토링 해봐야한다.

public class Member{
    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;
    
    public void setTeam(Team team){
        if(this.team != null){
            this.team.getMembers().remove(this);//1
        }
        this.team = team;//2
        team.getMembers().add(this);//3
    }
}
  1. 해당 Member에 Team이 있다는 것은 해당 Member는 이미 속해있는 팀이 있다는 뜻이기 때문에 새로운 팀과 연관관계를 맺기 위해서는 이미 관계가 맺어져있는 팀과 연관관계를 끊어야한다.
  2. 새로운 관계를 맺고자하는 팀을 선언해준다.
  3. 새로 관계를 맺은 팀과 순수한 객체의 양방향 연관관계를 지켜주기 위해 해당 회원(this)을 추가해준다.