엔티티들은 대부분 다른 엔티티와 연관관계가 있다. 하지만 객체는 참조를 사용해서 관계를 맺고 테이블은 외래 키를 사용해서 관계를 맺는다. 이 둘은 완전히 다른 특징을 가진다. 따라서 객체의 참조와 테이블의 외래 키를 매핑 해주어야 한다.
참조를 통한 연관관계는 언제나 단방향이다. 반대쪽 엔티티에 연관관계를 하나 더 만들어서 서로 참조하게 해서 양방향 연관관계를 만들 수 있지만, 정확히 이야기하자면 이것은 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다. 반면에 테이블은 외래 키 하나로 양방향으로 조인할 수 있다.
외래키를 매핑 할 때에는 @JoinColomn을 사용한다. name 속성에는 매핑할 외래 키 이름을 지정한다. 이 어노테이션은 생략할 수 있다. 생략하게 된다면 외래 키를 찾을 때 기본 전략을 사용하게 된다.
연관관계 사용
저장
public void testSave() {
// 팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
// 회원1 저장
Member member = new Member("member1", "회원1");
memeber.setTeam(team1);
em.persist(member1);
//회원 2 저장
Member member = new Member("member2", "회원2");
memeber.setTeam(team1);
em.persist(member2);
}
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다. 회원 엔티티로 팀 엔티티를 참조하고 저장했다. JPA는 참조한 팀의 식별자를 외래 키로 사용해서 적절한 등록 쿼리를 생성한다.
조회
연관관계가 있는 엔티티를 조회하는 방법은 객체 연관관계를 사용한 조회인 객체 그래프 탐색과 객체지향 쿼리(JPQL)을 사용 하는 두가지 방법이 있다.
// 객체 그래프 탐색을 이용한 연관관계 엔티티 조회
Member member = em.find(Member.class, "member1");
Team team = member.getTeam();
// 객체지향 쿼리를 이용한 연관관계 엔티티 조회
Strign jpql = "select m from Member m join m.team t where " + "t.name=:teamName"; // :로 시작하는 것은 파라미터를 바인딩받는 문법이다.
List<Member> resultList = em.createQuert(jpql, Member.class)
.setParameter("teamName", "팀1")
.getResult.List();
수정
연관관계의 수정은 단순히 불러온 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동한다. 그리고 변경사항을 데이터베이스에 자동으로 반영한다. 이것은 연관관계를 수정할 때도 같다.
// 팀1 소속이던 회원을 팀2에 소속하도록 수정
member.setTeam(Team2);
제거
기존에 설정된 연관관계를 제거하려면 연관관계를 가진 필드를 null로 설정하면 된다.
// Member 객체에 기존에 설정된 Team을 제거
member1.setTeam(null);
연관된 엔티티 삭제
연관된 엔티티를 제거하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다. 그렇지 않으면 외래 키 제약조건으로 인해 데이터베이스에서 오류가 발생한다.
// Team 객체를 삭제하기 위해 기존에 있던 연관관계를 제거하고 삭제
member1.setTeam(null);
member2.setTeam(null);
em.remove(team1);
순수한 객체까지 고려한 양방향 연관관계
양방향 관계에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다. 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다. 양쪽에 연관관계를 설정하게 되면 순수한 객체 상태에서도 동작하며, 테이블의 외래 키도 정상 입력된다.
연관관계 편의 메소드
양방향 연관관계는 결국 양쪽 다 신경 써야 한다. 서로 값을 입력해주는 함수를 각각 호출하다 보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있다. 양방향 관계에서 두 코드는 하나인 것처럼 사용하는 것이 안전하다. 또한 연관관계를 변경할 때 기존에 연관관계를 맺은 객체가 있다면 연관된 객체의 기존의 관계를 삭제하는 코드를 추가해야한다.
public void setTeam(Team team) {
// 기존 팀과 관계를 제거
if (this.team != null) {
this.team.getMembers ().remove(this);
}
this.team = team;
team.getMembers().add(this); // 반대방향의 값도 함께 입력해준다.
}
위처럼 객체에서 양방향 연관관계를 사용하려면 로직을 견고하게 작성해야 한다.
결론적으로 양방향 매핑의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것 뿐이다. 양방향 매핑은 복잡하기 때문에 우선 단방향 매핑을 사용하고 반대 방향으로 객체 그래프 탐색 기능이 필요할 때 양방향을 사용하도록 코드를 추가해도 된다.
연관관계 매핑
엔티티의 연관관계를 매핑할 때는 3가지를 고려해야 한다. 먼저 연관관계가 있는 두 엔티티가 일대일 관계인지 일대다 관계인지 다중성을 고려해야 한다. 다음으로 두 엔티티 중 한쪽만 참조하는 단방향 관계인지 서로 참조하는 양방향 관계인지 고려해야 한다. 마지막으로 양방향 관계면 연관관계의 주인을 정해야 한다.
다중성
연관관계에는 다음과 같은 다중성이 있다.
-다대일(@ManyToOne)
-일대다(@OneToMany)
-일대일(@OneToOne)
-다대다(@ManyToMany)
보통 다대일과 일대돠 관계를 가장 많이 사용하고 다대다 관계는 실무에서 거의 사용하지 않는다.
단방향, 양방향
테이블은 외래키 하나로 조인해서 사용해서 양방향으로 쿼리가 가능하므로 사실상 방향이라는 개념이 없다. 반면에 객체는 참조용 필드를 가지고 있는 객체만 연관된 객체를 조회할 수 있다. 객체 관계에서 한 쪽만 참조하는 것을 단방향 관계라 하고, 양쪽이 서로 참조하는 것을 양방향 관계라 한다.
연관관계의 주인
엔티티는 양방향 매핑하면 2곳에서 서로를 참조한다. 따라서 객체의 연관관계를 관리하는 포인트는 2곳이다. JPA는 두 객체 연관관계 중 하나를 정해서 데이터베이스 외래 키를 관리하는데 이것을 연관관계의 주인이라 한다. 외래 키를 가진 테이블과 매핑한 엔티티가 외래 키를 관리하는게 효율적이므로 보통 이곳을 연관관계의 주인으로 선택한다. 주인이 아닌 방향은 외래키를 변경할 수 없고 읽기만 가능하다. 연관관계의 주인이 아니라면 mappedBy 속성을 사용해서 연관관계의 주인 필드 이름을 값으로 입력해야한다.
다대일
데이터베이스 테이블의 다대일 관계에서 외래키는 항상 다 쪽에 있다. 따라서 객체 양방향 관계에서 연관관계의 주인은 항상 다쪽이다.
다대일 단방향
// Member 객체(다)에서 Team(일)을 참조
@ManyToOne
@JoinColumn (name = "TEAM_ID")
private Team team;
@JoinColumn을 사용해서 객체 필드를 외래 키와 매핑했다. 따라서 객체 필드로 회원 테이블의 외래키를 관리한다.
다대일 양방향
//Member(다)에서 Team(일)을 참조
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public void setTeam(Team team) {
this.team = team;
// 무한루프에 빠지지 않도록 체크
if(!team.getMembers().contains(this)){
team.getMembers().add(This);
}
}
//----------------------------------------------------------
// Team(1)에서도 Member(다)를 참조
@OneToMany(mappedBy = "team") // 관계의 주인이 아니기 때문에 mappedBy 사용
private List<Member> members = new ArrayList<Member>();
public void addMember(Member member) {
this.members.add(member);
if (member.getTeam() != this) { // 무한루프에 빠지지 않도록 체크
member.setTeam(this);
}
}
일대다와 다대일 연관관계는 항상 다(N)에 외래 키가 있으므로 연관관계의 주인이 된다. JPA는 외래키를 관리할 때 연관관계의 주인만 사용한다. 주인이 아닌 객체의 주인 객체는 조회를 위한 JPQL이나 객체 그래프를 탐색할 때 사용한다.
양방향 연관관계에서 항상 서로 참조하게 하려면 연관관계 편의 메소드를 작성하는 것이 좋은데 다(N)의 setter와 일(1)의 add 메소드가 이런 편의 메소드드들이다. 편의 메소드는 한 곳에만 작성하거나 양쪽 다 작성할 수 있는데, 양쪽에 다 작성하면 무한루프에 빠지므로 주의해야 한다.
일대다
일대다 관계는 다대일 관계의 반대 방향이다. 일대다 관계는 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션인 Collection, List, Set, Map 중에 하나를 사용해야 한다.
일대다 단방향
일대다 단방향 관계는 일 엔티티가 다 를 참조하므로 다 엔티티 쪽에서는 일을 참조하지 않으므로, 반대쪽 테이블에서 외래키를 관리한다. 외래키는 항상 다 쪽에 있지만 다 엔티티에는 외래키를 매핑할 수 있는 참조 필드가 없기 때문이다.
// Team(일)에서 Member(다)를 참조
@OneToMany
@JoinColumn(name = "TEAM_ID") // MEMBER 테이블의 외래키 TEAM_ID
private List<Member> members = new ArrayList<Member>();
일대다 단방향 매핑의 단점은 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점이다. 본인 테이블에 외래 키가 있으면 엔티티의 저장과 연관관계 처리를 INSERT SQL 한 번으로 끝낼 수 있지만, 그렇지 않다면 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 한다. 따라서 일대다 단방향 매핑 대신에 다대일 양방향 매핑을 사용하는 것이 권장된다.
일대일
일대일 관계는 양쪽이 서로 하나의 관계만 가진다. 일대일 관계는 그 반대도 일대일 관계이며, 주 테이블이나 대상 테이블 둘중 어느곳이나 외래키를 가질 수 있고 양쪽으로 조회할 수 있다.
주 테이블에 외래키를 두고 대상 테이블을 참조하는 방식은 외래키를 객체 참조와 비슷하게 사용할 수 있어서 객체지향 개발자들이 선호한다. 이 방법의 장점은 주 테이블이 외래 키를 가지고 있으므로 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다.
전통적인 데이터베이스 개발자들은 보통 대상 테이블에 외래 키를 두는 것을 선호한다. 이 방법의 장점은 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다.
주 테이블에 외래 키
객체지향 개발자들은 주 테이블에 외래키가 있는 것을 선호한다. JPA도 주 테이블에 외래 키가 있으면 좀 더 편리하게 매핑할 수 있다.
// 일대일 연관관계에서 주 테이블에 외래키가 있으며 단방향
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
//-------------------------------------------------------
// 일대일 연관관계에서 주 테이블에 외래키가 있으며 양방향이라면 추가
@OneToOne(mappedBy = "locker")
private Member member;
일대일 관계이므로 @OneToOne 을 사용했으며 이 관계는 다대일 단방향과 거의 비슷하다. 만약 반대 방향을 추가해서 양방향 관계로 만들고 싶다면 대상 객체에 주 객체 필드를 참조하고 mappedBy를 선언해서 연관관계의 주인이 아니라고 설정한다.
대상 테이블에 외래 키
일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다. 이 때는 단방향 관계를 양방향 관계로 만들고 대상 객체를 연관관계의 주인으로 설정해야 한다.
// 일대일 연관관계에서 대상 테이블에 외래키가 있으며 양방향
// 주 엔티티
@OneToOne(mappedBy = "member")
private Locker locker;
// 대상 엔티티
@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
주 엔티티 대신에 대상 엔티티를 연관관계의 주인으로 만들어서 대상 테이블이 외래키를 관리한다.
다대다
관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그래서 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다. 예시로서 [회원-주문상품-상품]이 이와 같다. 객체에서는 테이블과 다르게 객체 2개로 다대다 관계를 만들수 있다 @ManyToMany과 @JoinTable을 이용해서 연결 테이블을 바로 매핑하는 방식이다. 따라서 연결 테이블과 매핑할 엔티티를 따로 만들지 않아도 된다.
하지만 이 매핑을 실무에서 사용하기에는 한계가 있다. 위 방식으로 다대다 관계를 매핑하면 연결 테이블에는 두 개의 외래키만 가지게 되기 때문이다. 결국 연결 테이블을 매핑하는 연결 엔티티를 만들고 이곳에 추가한 컬럼들을 매핑해야한다. 회원과 상품 엔티티가 있다고 했을 때 회원상품 엔티티와 회원상품 식별자 클래스를 만들어야한다.
// 다대다 관계의 두 엔티티를 연결해주는 연결엔티티
@Entity
// IdClass를 이용해서 복합 기본 키 매핑
@IdClass (MemberProductId.class)
public class MemberProduct {
// Id와 JoinColumn을 동시에 사용하여서 기본 키 + 외래 키
@Id
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@Id
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
private int orderAmount;
...
}
회원상품 엔티티는 기본 키가 MEMBER_ID와 PRODUCT_ID로 이루어진 복합 기본 키이다. JPA에서 복합 키를 사용하려면 별도의 식별자 클래스를 만들어야 한다. 그리고 @IdClass를 사용해서 식별자 클래스를 지정하면 된다. 복합 키를 위한 식별자 클래스는 다음과 같은 특징이 있다.
-복합 키는 별도의 식별자 클래스로 만들어야 한다.
-Serializable을 구현해야 한다.
-equals와 hash 메소드를 구현해야 한다.
-기본 생성자가 있어야 한다.
-식별자 클래스는 public이어야 한다.
-@IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다.
// 연결엔티티의 식별자 클래스
public class MemberProductId implements Serializable {
private String member; // MemberProduct.member와 연결
private String product; // MemberProduct.product와 연결
//hashCode 와 equals
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {...}
}
회원상품은 회원과 상품의 기본 키를 받아서 자신의 키로 사용한다. 이렇게 부모 테이블의 기본 키를 받아서 자신의 기본 키 + 외래 키로 사용하는 것을 데이터베이스 용어로 식별 관계라 한다. 또한 식별자 클래스로 두 기본 키를 묶어서 복합 기본 키로 사용한다. 이렇게 구성한 관계를 저장할 때는 아래의 코드와 같다.
MemberProduct memberProduct = new MemberProduct();
memberProduct.setMember(member1); // 주문 회원 - 연관관계 설정
memberProduct.setProduct(productA); // 주문 상품 - 연관관계 설정
memberProduct.setOrderAmount(2); // 주문 수량
em.persist(memberProduct);
회원상품 엔티티를 만들면서 연관된 회원 엔티티와 상품 엔티티를 설정했다. 회원상품 엔티티는 데이터베이스에 저장될 때 연관된 회원의 식별자와 상품의 식별자를 가져와서 자신의 기본 키 값으로 사용한다.
// 복합키를 이용해서 연결 객체와 두 객체 조회
//기본 키 값 생성
MemberProductId memberProductId = new MemberProductId();
memberProductId.setMember("member1");
memberProductId.setProduct("productA");
memberProduct memberProduct = em.find(MemberProduct.class, memberProductId);
Member member = memberProduct.getMember();
Product product = memberProduct.getProduct();
단순한 기본 키는 객체를 사용할 필요가 없지만 복합키가 되면 항상 식별자 클래스를 만들어야한다. 복합 키를 사용하는 방법은 컬럼 하나만 기본 키로 사용하는 것과 비교해서 복잡하고 ORM 매핑에서 처리할 일이 상당히 많아진다. 따라서 복합 키를 사용하지 않고 간단히 다대다 관계를 구성하는 방법을 알아보자. 추천하는 기본 키 생성 전략은 데이터베이스에서 자동 생성해주는 대리 키를 Long 값으로 사용하는 것이다. 이 방법을 사용한다면 ORM 매핑 시에 복합 키를 만들지 않아도 되므로 간단히 매핑을 완성할 수 있다. 이것의 장점은 간편하고 거의 영구히 쓸 수 있으며 비즈니스에 의존하지 않는다. 또한 ORM 매핑 시에 복합 키를 만들지 않아도 되므로 간단히 매핑할 수 있다.
@Entity
public class Order { // 주문상품에서 주문으로 이름변경
/* 기본키로 대리키 사용 */
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
/** 기본 키 + 외래키가 아닌 외래키로만 사용 **/
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member meber;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
private int orderAmount;
...
}
대리키를 사용함으로써 식별 관계에 복합 키를 사용하는 것보다 매핑이 단순하고 이해하기 쉽다. 이전처럼 받아온 식별자를 기본 키 + 외래 키로 사용하는 것을 식별 관계라고 하고, 바뀐 코드와 같이 받아온 식별자는 외래키로만 사용하고 새로운 식별자를 추가하는 것을 비식별 관계라고 한다. 객체 입장에서 보면 2번처럼 비식별 관계를 사용하는 것이 복합 키를 위한 식별자 클래스를 만들지 않아도 되므로 단순하고 편리하게 ORM 매핑을 할 수 있다.
'JPA' 카테고리의 다른 글
[JPA] 프록시와 연관관계 관리 (0) | 2020.09.22 |
---|---|
[JPA] 고급 매핑 (0) | 2020.09.15 |
[JPA] 엔티티 매핑 (0) | 2020.09.04 |
[JPA] 영속성 관리 (0) | 2020.08.31 |
[JPA] JPA 소개 및 시작 (0) | 2020.08.30 |