노트코드
노트코드
노트코드

블로그 메뉴

  • 홈
  • 이력서
  • 이전 블로그
  • 글쓰기
  • 관리자페이지
  • 분류 전체보기 (57)
    • 코틀린 (2)
      • 실무 프로젝트로 배우는 Kotlin & Sprin.. (2)
    • JAVA (1)
      • 디자인패턴 (1)
      • 객체지향 5대원칙 (0)
    • SPRING (32)
      • JPA (11)
      • 스프링시큐리티 (1)
      • 스프링 (8)
      • QueryDsl (1)
      • 스프링배치 (11)
    • AZURE (0)
    • ETC (10)
      • MAVEN (0)
      • GIT (0)
      • ReMind (3)
      • Exception (1)
      • CS (6)
    • 책 (8)
      • 이것이 자바다 (8)

최근 글

최근 댓글

태그

  • 스프링
  • JPA
전체 방문자
오늘
어제
hELLO · Designed By 정상우.
노트코드

노트코드

SPRING/JPA

[JPA] 연관관계

2022. 2. 9. 13:40

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

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

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

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

💡 객체의 단방향 관계는 [Member.team](http://member.team) 필드를 통해서 팀을 알 수 있지만, team→ member를 접근하는 필드가 없으면 알 수가 없다. 만약 team에서도 알고 싶다면, Member라는 필드를 생성하며 Team.member를 통해 접근 할 수 있도록 양방향 관계를 맺어줘야 한다.
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외래 키로 연관관계를 맺으므로 이 값을 지정하면 된다.

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

주요 속성

💡 JoinColumn생략 다음처럼 @JoinColumn을 생략하면 외래 키를 찾을 때 기본 전략을 사용한다.

@ManyToOne
private Team team;

기본 전략: 필드명 ++ 참조하는 테이블의 컬럼명
예) 필드명(team) +
(밑줄) + 참조하는 테이블의 컬럼명(TEAM_ID) → team_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에 저장용으로 사용하지 않는다. (실제로 저장도 되지 않음)
    위에서 이야기한 순수한 객체 상태에서도 동작하게 하기 위해 사용하는것이다.

💡 주인이 아닌쪽에서 데이터를 입력해도 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;
}
  • 연관관계

'SPRING > JPA' 카테고리의 다른 글

[JPA]JPA,Hibernate, Spring Data JPA차이  (0) 2022.02.09
[JPA]객체지향쿼리  (0) 2022.02.09
[JPA]스프링 데이터 Common:커스텀 리포지토리  (0) 2022.01.21
[JPA]MappedSuperClass  (0) 2022.01.21
[JPA] 프록시  (0) 2022.01.21
    'SPRING/JPA' 카테고리의 다른 글
    • [JPA]JPA,Hibernate, Spring Data JPA차이
    • [JPA]객체지향쿼리
    • [JPA]스프링 데이터 Common:커스텀 리포지토리
    • [JPA]MappedSuperClass
    노트코드
    노트코드
    노션 블로그에서 티스토리로 이전공사중

    티스토리툴바