JPA 사용시, 엔티티 클래스를 만들어줘야 한다. 엔티티에 자주 사용하는, 꼭 써야하는 어노테이션들은 무엇인지 알아보자.
예시로 사용할 엔티티 클래스는 다음과 같다.
보통 나는 엔티티에는 빌더패턴을 쓰기 때문에 @Builder
도 같이 쓴다.
하지만 @Builder
를 쓸 때 '생성자에 쓰는 것'이 권장된다.
자세한 건 https://velog.io/@mooh2jj/올바른-엔티티-Builder-사용법
JPA를 사용할려면 '디폴트 생성자' 가 필요하다. 그래서 @NoArgsConstructor
를 써주는 것이다.
옵션으로 준access = AccessLevel.PROTECTED
은 자바 개발자라면 다들 배웠을 접근제한자랑 같은 의미다.
PROTECTED
는 다른 외부 패키지에 소속된 클래스가 접근하지 못하게 하는 것이다.
??) access = AccessLevel.PRIVATE
는 사용하면 안되나?
?? access = AccessLevel
Proxy 객체와 관련되어 있다. JPA 지연로딩시, Proxy 객체를 사용하기에 외부에서 이 엔티티 클래스를 사용하면 오류가 나서 안정성이 안좋다.
👀 각 연관관계의 default 속성은 다음과 같다.
@ManyToOne
: EAGER@OneToOne
: EAGER@ManyToMany
: LAZY@OneToMany
: LAZY
👀 이래서 XXXToOne(FetchType.EAGER)
부분 즉, 자식테이블
쪽에서 Lazy
로 바꿔주어야 한다.
@Getter
// @Setter // 실무에선 자제
@NoArgsConstructor(access = AccessLevel.PROTECTED) // entity를 만들기 위해서는 기본생성자 필요
@AllArgsConstructor // 권장 x => 안쓰는 게 나음!
@Builder // 권장x => 생성자에
@Entity
@ToString(exclude = "team")
// @ToString(of = {"id", "username", "age"})
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
public Member(String username) {
this(username, 0);
}
public Member(String username, int age) {
this(username, age, null);
}
public Member(String username, int age, Team team) {
this.username = username;
this.age = age;
if (team != null) {
changeTeam(team);
}
}
private void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@ToString(exclude = "members")
public class Team {
@Id
@GeneratedValue
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany(mappedBy = "team") // mappedBy에서 쓰이는 변수명은 @ManyToOne Member 엔티티의 Team 변수명과 같아야 한다!
private List<Member> members = new ArrayList<>();
public Team(String name) {
this.name = name;
}
}
하지만 실제 실무에선 쓰지 않는 방식이다. 이런 조인 방식에서 만든 매핑테이블에는 추가 컬럼을 만들 수도 없기 때문이다.
다른 방법으로 직접 매핑테이블을 만들고 ManyToOne으로 만드는 방식으로 한다.
// Category.class
@ManyToMany
@JoinTable(name = "category_item",
joinColumns = @JoinColumn(name = "category_id"),
inverseJoinColumns = @JoinColumn(name = "item_id"))
private List<Item> items = new ArrayList<>();
// springboot_board_example
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name="role_id", referencedColumnName = "id"))
private Set<Role> roles;
✅ 직접 중간매핑 만들기(실제 현장에선 이렇게 만듦)
단방향매핑(@ManyToOne
) 이 2개 만들면 된다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "cart_item")
public class CartItem {
// cart - cart_item - item 중간 매핑 테이블
// 1 : N _ M : 1
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cart_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "cart_id")
@Setter
private Cart cart;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
@Setter
private Item item;
}
ManyToOne - 연관관계의 주인(외래키를 가진 곳) 설정, 아래 글 참고
OneToMany - 외래키로 참조한 자식 테이블을 보통 List로 담을 때 사용, 필수는 아니다.
cascade = CascadeType.ALL
은 영속성 전이
를 해주는 옵션이다. 이 옵션을 설정해주면 외래키로 참조한 자식 테이블도 같이 지워줄 수 있다.
추가로 고아객체
도 추가설정할 수 있다.
// Team.class : 부모테이블
// Team.members는 읽기용도일뿐! 주인 아님!
// 컬렉션일 때 cascade 처리(영속성 전이)
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL
, orphanRemoval = true)
private List<Member> members = new ArrayList<>();
고아 객체
: 부모 엔티티와 연관관계가 끊어져서 버려진 자식 엔티티
영속성 전이 기능과 같이 사용하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
고아객체 제거 기능은 참조하는 곳이 하나일 때만 사용
OrderItem 엔티티를 Order 엔티티가 아닌 다른 곳에서 사용하고 있다면 이 기능을 사용하면 안됨
@OneToOne
, @OneToMany
어노테이션에서 orphanRemoval = true
옵션 추가할 수 있음.
https://velog.io/@conatuseus/연관관계-매핑-기초-2-양방향-연관관계와-연관관계의-주인
엔티티를 양방향 연관관계로 설정한다면, 객체의 참조는 둘인데 외래 키는 하나
입니다.
따라서 DB 테이블과 다르게 차이가 발생
합니다.
그렇다면 둘 중 어떤 관계를 사용해서 외래 키를 관리해야 할까요?
이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리하게 하는데 이것을 연관관계의 주인
이라 합니다.
// Member.class : 자식 테이블
// 외래키를 가지고 있는 곳인 연관관계의 주인이다! Member.team이 주인임
// 즉, 자식테이블의 부모테이블 필드가 연관관계의 주인!
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id") // @JoinColumn 로 외래키 걺
private Team team;
양방향 연관관계 매핑 시 지켜야할 규칙이 있는데 두 연관관계 중 하나를 연관관계의 주인으로 정해야 합니다.
연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있습니다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있습니다.
어떤 연관관계를 주인으로 정할지는 mappedBy
속성을 사용하면 됩니다.
// Team.class : 부모테이블
// Team.members는 읽기용도일뿐! 주인 아님!
// mappedBy = "team" => Member.team 이 연관관계 주인 설정
// OneToMany쪽에서 설정
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL
, orphanRemoval = true)
private List<Member> members = new ArrayList<>();
mappedBy 설정
은 @OneToMany에서 설정하는 것이다.- mappedBy에서 쓰이는 변수명은
@ManyToOne Member 엔티티의 Team 변수명과 같아야 한다!
@ManyToOne
이 항상 연관관계의 주인이 된다.- 연관관계의 주인은
외래 키가 있는 곳이다.
연관관계의 주인은 테이블에 외래 키가 있는 곳
으로 정해야 합니다. 여기서는 회원 테이블이 외래 키를 가지고 있으므로
Member.team
이 주인이 됩니다.
주인이 아닌 Team.members에는mappedBy="team"
속성을 사용해서 주인이 아님을 설정해야 합니다.
여기서 mappedBy의 값으로 사용된 team은 연관관계의 주인인 Member 엔티티의 team 필드를 말합니다.
https://velog.io/@conatuseus/연관관계-매핑-기초-2-양방향-연관관계와-연관관계의-주인
✨ 연관관계의 주인만이 외래 키의 값을 변경할 수 있습니다!
public class Member {
...
private Team team;
public void setTeam(Team team){
this.team = team;
member.getTeam().add(member);
}
setTeam()
메서드 하나로 양방향 관계를 모두 설정하도록 변경했습니다.
이렇게 수정한 메서드를 사용하는 코드를 보겠습니다.
Test
public void test() {
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1);
em.persist(member1);
Member member2 = new Member("member2", "회원1");
member2.setTeam(team1);
em.persist(member2);
}
이렇게 한 번에 양방향 관계를 설정하는 메서드를 연관관계 편의 메서드라 합니다.