JPA 객체 매핑- 연관 관계
1. 겍체와 테이블의 연관관계 방식
객체 연관관계로 따지면 Member 객체는 Member.team 필드로 Team 객체와 참조(주소)로 단방향 연관관계를 맺으며 member는 team필드로 Team을 알 수 있지만 Team은 Member을 알 수 없다.
테이블로 따지면 양방향 연관관계이다(단반향이 없다). MEMBER 테이블의 TEAM_ID 외래 키를 통해 TEAM과 MEMBER은 서로 조인 할 수 있기 때문이다.
2. 연관 관계 매핑
JPA를 사용해서 객체방식과 테이블 방식을 매핑한다.
Member 객체에 아래처럼 작성한다.
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
Member.team 과 MEMBER.TEAM_ID 를 매핑하는 것이 연관관계 매핑이다.
@ManyToOne 는 다대일 관계라는 매핑 정보이다.
@OneToMany, @OneToOne 도 있다.
@JoinColumn 은 외래키를 매핑할 때 사용한다. name속성에 외래 키 이름을 지정한다. 이 어노테이션은 생략가능하다.
@Column의 속성처럼 unique, nullable, insertable, updatable, columnDefinition, table 속성들이 있다.
Member 객체를 완성할 때 Team객체를 미리 만들고 Member 객체에다 생성자를 쓰든 set함수를 쓰든 집어넣으면 된다.
이렇게 하면 Member 엔티티는 Team 엔티티를 참조하고 저장한다. JPA는 참조한 Team 식별자를 외래키로 사용해서 적절한 등록 쿼리를 생성한다.
Member 객체에서 Team 객체를 객체지향적인 방식이나 테이블방식의 join을 통해 저장된 값을 불러올 수 있다. 테이블 방식은 JPQL을 사용하면 된다.
연관된 엔티티를 삭제할 때 주의 할점은 연관관계를 먼저 제거하고 연관된 엔티티를 삭제해야 한다는 점이다. 아래는 간단한 예시이다.
member.setTeam(null);
em.remove(Team);
또한 객체에서 양방향 연관관계를 가질 수 있다는 특징이 있다. 이는 Team에서 Member를 가져올 수 있다는 말이다.
아래는 Team 객체에서의 예시이다.
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
Member 가 여러개 일 수 있으므로 List로 받는다.
mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다.
Member 객체에선 team을 값으로 주었기에 mappedBy는 team으로 설정한다.
3. 연관관계의 주인
OneToMany를 사용해 객체에서 양방향 연관관계를 가졌다고 말했지만 엄밀히 말하면 객체에서 양방향 연관관계라는 것은 없다. 서로 다른 단방향 연관관계 2개를 로직으로 묶어 양뱡향인 것처럼 보이게 할 뿐이다.
물론 데이터베이스에서는 외래키 하나만으로 양방향 연관관계를 맺는다. OneToMany를 사용하지 않고도 JPQL을 이용하면 join을 통해 Team객체를 통해 Member객체를 불러올 수가 있다는 것이다. 이미 Member 객체에서 외래키로 TEAM_ID를 지정했기 때문이다.
만약 OneToMany를 Team 객체에서 사용한다면 두 번의 참고가 발생할 것인데 외래키는 Member 객체에서 사용한 하나다. 그렇기에 둘 사이의 차이가 발생하므로 두 객체 연관관계중에서 하나를 연관관계의 주인으로 설정하여 외래키를 관리하게 한다. 주인은 연관관계와 매핑되고 외래 키를 관리할 수 있지만 다른 한 쪽은 읽기만 할 수 있다.
여기서 주인을 정하기 위해 mappedBy속성이 이용된다.
mappedBy속성을 사용하면 연관관계의 주인이 아니게 되고 연관된 객체 내에 이 속성의 값으로 지정된 필드가 연관관계의 주인을 지정하면 된다.
연관관계의 주인은 외래 키가 있는 곳으로 정해야 하며 위의 예시에서는 Member.teamId를 주인으로 설정해야 하는 것이다. 그 이유는 Team 객체에서는 Member객체는 전혀 상관 없는 것인데, 주인으로 지정하기도 애매해서이다.
즉, 다대일, 일대다 관계에서는 항상 다 쪽이(@ManyToOne를 지정하는 쪽이) 외래 키를 가지며 연관관계의 주인으로 설정해야 하는것이다.
주의할 점은 주인이 하나여도 양쪽의 값을 모두 집어넣야 한다는 것이다.
단방향 연관관계라면 ManyToOne나 OneToMany나 어노테이션을 붙인 쪽이 주인이 되지만 외래 키는 항상 다쪽 테이블에 생성된다.
4. 일대일, 다대다 관계
@OneToOne을 사용하면 일대일 관계를 가진다. 그리고 어느 테이블이든 외래 키를 가지고 연관관계의 주인을 가질 수 있다.
주 테이블에 두던지 대상 테이블에 주던지 둘다 특징이 있다
주 테이블에 외래 키를 둘 경우에는 객체지향방식에서 편리하게 매핑할 수 있게 해준다. 위의 방식처럼 단방향, 양방향을 설정하면 된다.
대상 테이블에 외래 키를 둘 경우에는 데이터베이스 개발자들은 이를 더 선호한다고 한다. 하지만 JPA에서는 일대다 단방향 관계에서는 대상 테이블에 외래 키가 매핑되는 것은 허용하지만 일대일 관계에서는 이런 매핑을 허용하지 않는다. 그렇기에 양방향에서만 가능하다.
객체는 다대다 관계가 가능하지만 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그렇기에 다대다 관계를 일대다, 다대일 관계로 풀어서 연결한다. 이는 중간에 연결테이블을 추가해야 한다는 것을 의미한다.
@ManyToMany 를 통해 매핑한다.
아래는 예시 코드이다. 회원과 상품의 다대다 관계에서 단방향만으로 나타낸것이고 아래는 회원 엔티티이다.
@ManyToMany
@JoinTable(name= "MEMBER_PRODUCT", // 연결 테이블
joinCloumns = @JoinColumn(name = "MEMBER_ID"), // 자신의 매핑할 조인 컴럼 정보
inverseJoinColumns = @JoinColumn(name = PRODUCT_ID)) // 상대 엔티티의 매핑할 조인 컬럼 정보
private List<Product> products = new ArrayList<Priduct>();
JPA를 통해 객체를 생성하고 flush() 할 때 각 객체에 대한 테이블뿐만이 아니라 연결 테이블인 MEMBER_PRODUCT 도 INSERT 하게 된다.
양방향 관계에서 연관관계의 주인은 둘중 원하는 곳에 지정한다.
하지만 다대다 관계로 인한 연결테이블을 엔티티로 직접 만들어 사용하는 방식이 더 유용하다고 한다.
MEMBER, PRODUCT 테이블이 연결테이블과 각각 일대다, 연결테이블은 각각의 테이블에 다대일이 되도록 설정하고 연결테이블에서는 두개의 foreignkey 와 1개의 primarykey를 가지게 하는 방식(비식별 관계)과 두개의 foreignkey중 하나를 primarykey로 하는 방식(식별 관계)이 있다.
이런방식들로 구현하면 연결테이블에 다른 추가적인 속성들을 붙일 수 있다. 참고로 비식별 관계가 더 추천되어진다고 한다.
참고
자바 ORM 표준 JPA 프로그래밍 - 김영한