두 Entity 사이를 연결해주는 Relative Entity의 경우 역할이 연결에만 있는 것이 아니기 때문이다.
Member
@Entity
public class Member {
...
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
...
}
Product
@Entity
public class Product {
...
@OneToMany(mappedBy = "product")
private List<MemberProduct> members = new ArrayList<>();
...
}
MemberProduct
@Entity
@Getter
@Setter
public class MemberProduct {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
}
Foreign Key를 설정하는 부분이다. name
옵션은 단순 컬럼의 이름을 지정하는 것이므로 연관 관계와는 아무 연관 없다.
@JoinColumn
내부
/**
* (Optional) The name of the column referenced by this foreign
* key column.
*
* When used with entity relationship mappings other
* than the cases described here, the referenced column is in the
* table of the target entity.
* When used with a unidirectional OneToMany foreign key
* mapping, the referenced column is in the table of the source
* entity.
*
* When used inside a <code>JoinTable</code> annotation,
* the referenced key column is in the entity table of the owning
* entity, or inverse entity if the join is part of the inverse
* join definition.
*
* When used in a <code>CollectionTable</code> mapping, the
* referenced column is in the table of the entity containing the
* collection.
*
*
* Default (only applies if single join column is being
* used): The same name as the primary key column of the
* referenced table.
*/
String referencedColumnName() default "";
한 줄 요약
referencedColumnName
속성을 생략하면 자동으로 대상 테이블의 pk 값으로 지정 된다.
mappedBy
옵션을 지정한 쪽은 ReadOnly가 돼 관계에서 테이블의 종이 된다.
반대로 지정하지 않은 쪽은 주가 된다.
외래키를 가지고 있지 않은 쪽을 종으로 설정하는 것이 좋다.
mappedBy에는 반드시 주인(자식)이 가지고 있는 객체의 이름 ( alsoCounter O / also_counter X ) 로 넣어야 한다.
양방향 맵핑을 사용할 경우 두 객체 모두 필드를 통해 서로의 객체에 접근 가능하기 때문에 두 객체 중 하나의 객체만 테이블을 관리할 수 있도록 정하는 것이 mappedBy
옵션이다.
Entity의 상태 변화를 전파시키는 옵션이다.
주종 관계에 있는 도메인에 적용할 수 있다.
종 도메인에 설정한다.
@Entity
@Getter
@NoArgsConstructor
@Table(name = "also_user")
public class AlsoUser extends BaseTimeEntity implements UserDetails {
...
@OneToMany(
mappedBy = "alsoUser",
cascade = {CascadeType.PERSIST, CascadeType.REMOVE},
orphanRemoval = true
)
@JsonManagedReference
private List<Photo> photos = new ArrayList<>();
...
}
CasecadeType.PERSIST
: record 생성 시 설정이 적용된 인스턴스가Transient
에서 Persistent
로 넘어갈 때 연관된 Entity 또한 같이 상태가 넘어가며 저장된다.
CasecadeType.REMOVE
: 동일한 알고리즘으로 인스턴스의 Record가 삭제시 연관된 Entity Record 또한 같이 삭제된다.
Transient
: 객체를 생성하고, 값을 주어도 JPA나 hibernate가 그 객체에 관해 아무것도 모르는 상태. 즉, 데이터베이스와 매핑된 것이 아무것도 없다.
Persistent
: 저장을 하고나서, JPA가 아는 상태(관리하는 상태)가 된다. 그러나 .save()를 했다고 해서, 이 순간 바로 DB에 이 객체에 대한 데이터가 들어가는 것은 아니다. JPA가 persistent 상태로 관리하고 있다가, 후에 데이터를 저장한다.(1차 캐시, Dirty Checking(변경사항 감지), Write Behind(최대한 늦게, 필요한 시점에 DB에 적용) 등의 기능을 제공한다)
Detached
: JPA가 더이상 관리하지 않는 상태. JPA가 제공해주는 기능들을 사용하고 싶다면, 다시 persistent 상태로 돌아가야한다.
Removed
: JPA가 관리하는 상태이긴 하지만, 실제 commit이 일어날 때, 삭제가 일어난다.
종 Entity의 변경이 있다면, JPA에서는 insert
-> update
-> update
-> delete
의 순서로 이어지는데 변경된 종 Entity를 insert하고, 기존 종 Entity를 Null로 업데이트한다.
이 과정에서 PK(JoinColumn) 값이 null로 변한 종 Entity는 고아 객체라고 하며 이 고아 객체를 삭제해주는 옵션이 orphanRemoval = true
이다.
클라이언트에게 객체를 응답하는 경우 객체 간 연관관계가 물고 물려서 중복된 내용이 매우 많이 생겨난다.
mappedBy
가 적용된 곳에 적용 시킨다.
mappedBy
가 적용되지 않은 곳에 적용 시킨다.
/*
* Not Mapped
*/
@ManyToOne
@JoinColumn(name = "user_id")
@JsonBackReference
private AlsoUser alsoUser;
/*
* Mapped
*/
@OneToMany(
mappedBy = "alsoUser",
cascade = {CascadeType.PERSIST, CascadeType.REMOVE},
orphanRemoval = true
)
@JsonManagedReference
private List<Photo> photos = new ArrayList<>();
순환 참조와 해결방법
[JPA] @JoinColumn 확실히 알고가기!!!
JPA orphanRemoval 용도
[JPA]엔티티 상태 & Cascade