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)
- 단방향
@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로 매핑한 덕분에 다대다 관계를 사용 할 때는 이 연결 테이블을 신경 쓰지 않아도 된다.
- 양방향
@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)
}
}
- 다대다 매핑의 한계
@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 |