SPRING/JPA

[JPA] 연관관계

노트코드 2022. 2. 9. 13:40

JPA가 사용하는 객체 연관관계 VS 테이블 연관관계의 차이

  • 객체는 참조(주소)로 연관관계를 맺는다.
  • 테이블은 외래 키로 연관관계를 맺는다.

연관된 데이터를 조회할때 객체는 참조를 하지만, 테이블은 조인을 한다.

참조를 통한 연관관계는 단방향 관계이며, 양방향으로 만들어도 사실상, 단방향관계가 2개이다.

public Static void main(){
    Member member1 = new Member("member1","회원1")
    Member member2 = new Member("member2","회원2")
    Team team1 = new Team("team1","팀1");

    member1.setTeam(team1);
    member2.setTeam(team2);

    Team findTeam = member1.getTeam();

}
객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라 한다.

@JoinColumn(name="Team_ID")

  • 조인 컬럼은 외래 키를 매핑할 때 사용한다. name 속성에는 매핑할 외래 키 이름을 지정한다.

    회원과 팀 테이블은 TEAM_ID외래 키로 연관관계를 맺으므로 이 값을 지정하면 된다.

  • 키를 매핑할 때 사용한다.

주요 속성

양방향 매핑 연관관계의 주인

  • 테이블은 외래키 하나로 두테이블의 연관관계를 관리하지만 객체는 불가능하다.
  • 객체의 양방향관계는 서로 다른 단방향 관계 2개다. 즉 한쪽에서 key를 관리해야한다.
  • 연관관계의 주인이 아닌쪽은 읽기만 가능하다.
  • 기본적으로 연관관계의 주인은 one 보다는 Many쪽에서 갖는다.(외래키가 있는 곳을 주인으로)
@Entity
public class Team{
    @Id@GeneratedValue
    private Long id;
    private String name; 

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

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

연관관계의 주의점

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

Member member = new Member();
member.setName("member1");

team.getMember().add(member);
em.persist(member);

//결과 
id=1
userName=member1
Team_ID= null 

위 경우의 문제는 주인이 아닌 반대쪽에서 데이터를 입력하였고, Insert 쿼리는 발생하지만 실제로 저장되지는 않는 문제가 발생한다.(주인만 데이터입력이 가능하다)

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

Member member = new Member();
member.setName("member1");

team.getMember().add(member);
member.setTeam(team)
em.persist(member);

//결과 
id=1
userName=member1
Team_ID= 2

meber에만 Team값을 설정할시 Team.getMembers()를 통해 사이즈가 2인것을 기대하지만 실제로는 입력한 데이터가 없으니 0이 출력된다.만약 team1.getMember().add(member1) 이런식으로 코드를 추가 해주면 위 문제는 발생하지 않는다.

  • 해당 코드 보기

      public void pojoRelaction{
          //팀1
          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

    member1과 member2에는 team1에 해당하는 값을 저장하였지만, team1에는 저장된 member값이 없으므로 size는 0이다

      public void pojoRelaction{
          //팀1
          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(team1);
          team1.getMembers().add(member2);
    
          List<Member> members = team1.getMembers();
          System.out.println(members.size())
    
      }
      //결과 2

    우리가 기대한 코드의 값을 보려면 위와 같이 작성해야 한다.
    위 코드를 JPA로 완성시켜보자

      public void pojoRelaction{
          //팀1
          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")
          member2.setTeam(team1);
          team1.getMembers().add(member2); ----2
          em.persist(member2)
    
      }
    

    1,2는 주인이 아니므로 DB에 저장용으로 사용하지 않는다. (실제로 저장도 되지 않음)
    위에서 이야기한 순수한 객체 상태에서도 동작하게 하기 위해 사용하는것이다.

연관관계 편의 메소드

양방향 관계는 결국 양쪽 다 신경 써야 한다.

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

양방향 관계에서 두 코드는 하나의 것처럼 사용하는것이 좋고, 위 코드를 리팩토링하는게 좋다.

public class Member{
    private Team team;

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

만약, setTeam을 호출한 직후 이전 연관관계를 삭제하지 않으면 아래와 같은 일이 발생한다.

member1은 teamB와 새로운 연관관계가 되었지만 , teamA는 여전히 이전값을 가지고 있다.
즉, 연관관계를 삭제해주는 기능 역시 필요하다.

public void setTeam(Team team){
    if(this.team != null){
        this.team.getMember().remove(this)
    }
    this.team = team;
    team.getMembers().add(this);
}

다대다 (ManyToMany)

  1. 단방향
@Entity
@Getter
@Setter
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    @Column(name = "NAME", nullable = false, length = 10)
    private String userName;

    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT",
            joinColumns = @JoinColumn(referencedColumnName = "MEMEBER_ID"),
            inverseJoinColumns = @JoinColumn(referencedColumnName = "PRODUCT_ID"))
    private List<Product> products = new ArrayList<Product>();

}

@Entity
public class Product {

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

    private String name;
}
  • JoinTable.name : 연결 테이블을 지정한다. 여기서는 MEMBER_PRODUCT 테이블을 선택했다.
  • JoinTable.inverseJoinColumns: 반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정한다.
  • JoinTable.joinColumns: 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정한다.(MEMBER_ID)

MEMBER_PRODUCT 테이블은 다대다 관계를, 다대일 관계로 풀어내기 위해 필요한 연결 테이블이다.
@ManyToMany로 매핑한 덕분에 다대다 관계를 사용 할 때는 이 연결 테이블을 신경 쓰지 않아도 된다.

  1. 양방향
@Entity
public class Product {

    @Id
    private String id;

    @ManyToMany(mappedBy = "products")
    private List<Member> members;
}
//양방향 연관관계 설정 방법
member.getProducts().add(product);
product.getMembers().add(member);

public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    @Column(name = "NAME", nullable = false, length = 10)
    private String userName;

    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT",
            joinColumns = @JoinColumn(referencedColumnName = "MEMEBER_ID"),
            inverseJoinColumns = @JoinColumn(referencedColumnName = "PRODUCT_ID"))
    private List<Product> products = new ArrayList<Product>();

        //편의 메소드
        public void addProduct(Product product){
            products.add(product);
            product.getMembers().add(this)
        }
}
  1. 다대다 매핑의 한계

@ManyToMany를 사용하면 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 여러 가지로 편리하나, 테이블의 자유도(필요한 컬럼을 추가하기 어려움)가 떨어져 사용하기에 어려움이 있다.

결국 필요한 컬럼이 있다면, 아래와 같이 다대일 관계로 풀어야한다.

@Entity
public class Product {

    @Id
        @Column(name="PRODUCT_ID")
    private String id;

}

public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts; 

}

@Entity
@IdClass(MemberProductId.class)
public class MemberProduct{
    @Id
    @ManyToOne
    @JoinColumn(name="MEMBER_ID")
    private Member member; //MemberProductId.member와 연결

    @Id
    @ManyToOne
    @JoinColumn(name="PRODUCT_ID")
    private Product product; // MemberProductId.product와 연결
    private int orderAmount;
}

public class MemberProductId impplements Serializable{
    private String member;
    private String product;

    // hashcode and equals
}

*복합 기본 키 (@IdClass)

회원상품 엔티티는 기본키가 MEMBER_ID와 PRODUCT_ID로 이루어진 복합 기본키다.
JPA에서 복합 키를 사용하려면 별도의 식별자 클래스를 만들어야 한다.
그리고 엔티티에 @IdClass를 사용해서 식별자 클래스를 지정하면 된다.

복합 키를 위한 식별자 클래스는 다음과 같은 특징이 있다.

  • 복합 키는 별도의 식별자 클래스로 만들어야한다.
  • Serializable을 구현해야 한다.
  • equals와 hashCode메소드를 구현해야 한다.
  • 기본 생성자가 있어야한다.
  • 식별자 클래스는 public이어야 한다.
  • IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다.

*새로운 기본 키 사용

추천하는 기본 키 생성 전략은 데이터베이스에서 자동으로 생성해주는 대리 키를 Long값으로 사용하는것이다. 영구히 쓸 수 있으며, 비즈니스에 의존하지 않는다.

@Entity
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;
}
  • 연관관계