단방향 연관관계
회원과 팀이 있다.
회원은 하나의 팀에만 소속될 수 있다.
회원과 팀은 다대일 관계이다.
- 객체 연관관계
- 회원 객체와 팀 객체는 단방향 관계 - 테이블 연관관계
- 회원 테이블과 팀 테이블은 양방향 관계 - 객체 연관관계와 테이블 연관관계의 가장 큰 차이
- 참조를 통한 연관관계는 언제나 단방향이다.
- 객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다.
즉, 서로 다른 단뱡향 관계 2개다.
- 반면에, 테이블은 외래 키 하나로 양방향으로 조인할 수 있다.
객체 관계 매핑
회원 엔티티
@Entity
public class Member {
@Id
@Column(name = "member_id")
private String id;
private String userName;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
팀 엔티티
@Entity
public class Team {
@Id
@Column(name = "team_id")
private String id;
}
@ManyToOne
속성 | 기능 | 기본값 |
optional | false로 설정하면 연관된 엔티티가 항상 있어야 함 | true |
fetch | 글로벌 페치 전략 설정 | @ManyToOne = FetchType.EAGER @OneToMany = FetchType.LAZY |
cascade | 영속성 전이 기능 사용 | |
targetEntity | 연관된 엔티티의 타입 정보를 설정 거의 사용 x |
@JoinColumn
속성 | 기능 | 기본값 |
name | 매핑할 외래 키 이름 | 필드명 + _ + 참조하는 테이블의 기본 키 칼럼명 |
referencedColumnName | 외래 키가 참조하는 대상 테이블의 칼럼명 | 참조하는 테이블의 기본 키 칼럼명 |
foreignKey(DDL) | 외래 키 제약조건을 직접 지정할 수 있다. 테이블 생성할 때만 사용 |
|
unique nullable insertable updatable columnDefinition table |
@Column의 속성과 같다. |
조회
- 객체 그래프 탐색
- 객체지향 쿼리 사용
객체지향 쿼리를 사용한 조회
String jpql = "select m from Member m join m.team t where t.name=:teamName";
List<Member> memberList = entityManager.createQuery(jpql, Member.class).setParameter("teamName", "팀").getResultList();
양방향 연관관계
- 데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있다. -> 데이터베이스 수정 사항이 없음
- 객체 관계는 단방향 매핑을 추가해야한다.
회원 엔티티
@Entity
public class Member {
@Id
@Column(name = "member_id")
private String id;
private String userName;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
팀 엔티티
@Entity
public class Team {
@Id
@Column(name = "team_id")
private String id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> memberList = new ArrayList<>();
}
테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리하면 된다.
JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라고 한다.
양방향 매핑의 규칙: 연관관계의 주인
연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다.
반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
- 주인은 mappedBy 속성을 사용하지 않는다.
- 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
양방향 연관관계의 주의점 및
순수한 객체까지 고려한 양방향 연관관계
양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다.
public void save(EntityManager entityManager) {
Member member = Member.builder()
.id("member")
.username("회원")
.build();
entityManager.persist(member);
Team team = Team.builder()
.id("team")
.teamName("팀")
.build();
team.getMemberList().add(member);
entityManager.persist(team);
}
다음과 같이 저장하고 조회하면 member의 팀 값은 null로 나온다.
즉, 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
양방향 매핑 순환 구조의 문제
실제로 양방향 매핑을 통해서 엔티티를 관리하다보면 무한 순환되는 문제를 발견할 수 있다.
회원 엔티티
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
@Table(name = "tbl_member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "member_name")
private String memberName;
@ManyToOne(targetEntity = Team.class, fetch = FetchType.LAZY)
@Setter
private Team team;
@Builder
public Member(String memberName, Team team) {
this.memberName = memberName;
this.team = team;
}
}
팀 엔티티
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
@Table(name = "tbl_team")
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "team_name")
private String teamName;
@OneToMany(mappedBy = "team")
@Setter
private List<Member> memberList = new ArrayList<>();
@Builder
public Team(String teamName, List<Member> memberList) {
this.teamName = teamName;
this.memberList = memberList;
}
}
테스트 코드
class 양방향매핑_Test {
@Test
void findByMemberName() {
Team team = Team.builder()
.teamName("영준 팀")
.build();
teamRepository.save(team);
Member member = Member.builder()
.memberName("영준")
.build();
member.setTeam(team);
List<Member> memberList = new ArrayList<>();
memberList.add(member);
team.setMemberList(memberList);
memberRepository.save(member);
Member savedMember = memberRepository.findByMemberName(member.getMemberName()).orElse(null);
assertNotNull(savedMember);
}
}
위와 같이 테스트코드를 작성하여 확인해보면 무한 순환으로 연결되는 것을 확인할 수 있다.
이 문제를 해결하는 방법 몇가지를 소개하겠다.
- Entity로 반환하지 않고, DTO를 적극 활용하기
- @JsonIgnore : Json으로 직렬화 할 속성에서 무시 해버리기
- @JsonManagedReference, @JsonBackReference : 직렬화 방향을 설정하여 해결
- @JsonIdentityInfo : 순환 참조 될 대상의 식별키로 구분해 더 이상 순환참조되지 않게 한다.
@JsonIgnore
@JsonIgnore
@ManyToOne(targetEntity = Team.class, fetch = FetchType.LAZY)
@Setter
private Team team;
@JsonIgnore
@OneToMany(mappedBy = "team")
@Setter
private List<Member> memberList = new ArrayList<>();
Json으로 출력해보면 아예 무시되어 나오는 것을 확인할 수 있다.
@JsonManagedReference, @JsonBackReference
@JsonManagedReference
@ManyToOne(targetEntity = Team.class, fetch = FetchType.LAZY)
@Setter
private Team team;
@JsonBackReference
@OneToMany(mappedBy = "team")
@Setter
private List<Member> memberList = new ArrayList<>();
Json으로 출력하면 정확하게 나온다.
@JsonIdentityInfo
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
@Table(name = "tbl_member")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "member_name")
private String memberName;
@ManyToOne(targetEntity = Team.class, fetch = FetchType.LAZY)
@Setter
private Team team;
@Builder
public Member(String memberName, Team team) {
this.memberName = memberName;
this.team = team;
}
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
@Table(name = "tbl_team")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "team_name")
private String teamName;
@OneToMany(mappedBy = "team")
@Setter
private List<Member> memberList = new ArrayList<>();
@Builder
public Team(String teamName, List<Member> memberList) {
this.teamName = teamName;
this.memberList = memberList;
}
}
위의 방식들과는 다르게 연관관계를 아예 단방향으로만 양방향을 만드는 방법이있다.
양방향으로 만들 엔티티 사이에 엔티티를 하나 더 만들어 간접적으로 양방향으로 만드는 방법이다.
다음과 같은 방식으로 연관관계를 맺을 수 있다.
회원 엔티티
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
@Table(name = "tbl_member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "member_name")
private String memberName;
@Builder
public Member(String memberName) {
this.memberName = memberName;
}
}
팀 엔티티
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
@Table(name = "tbl_team")
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "team_name")
private String teamName;
@Builder
public Team(String teamName) {
this.teamName = teamName;
}
}
회원 - 팀 엔티티
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
@Table(name = "tbl_member_team")
public class MemberTeam {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(targetEntity = Member.class, fetch = FetchType.LAZY)
private Member member;
@ManyToOne(targetEntity = Team.class, fetch = FetchType.LAZY)
private Team team;
@Builder
public MemberTeam(Member member, Team team) {
this.member = member;
this.team = team;
}
}
테스트 코드
class 양방향매핑_Test {
@Test
void findByMemberName() throws JsonProcessingException {
Team team = Team.builder()
.teamName("영준 팀")
.build();
teamRepository.save(team);
Member member = Member.builder()
.memberName("영준")
.build();
memberRepository.save(member);
MemberTeam memberTeam = MemberTeam.builder()
.member(member)
.team(team)
.build();
memberTeamRepository.save(memberTeam);
MemberTeam savedMemberTeam = memberTeamRepository.findById(1L).orElse(null);
assertNotNull(savedMemberTeam);
}
}
이렇게 연관관계를 맺을 경우 회원 엔티티와 팀 엔티티는 각각 테이블에서는 절대 알 수 없고, 회원-팀 엔티티를 통해서만 접근을 할 수 있다.
'Spring JPA' 카테고리의 다른 글
고급 매핑 - 10 (0) | 2021.07.12 |
---|---|
다양한 연관관계 매핑 - 9 (0) | 2021.07.09 |
엔티티 매핑 - 7 (0) | 2021.07.03 |
영속성 관리 - 6 (0) | 2021.06.29 |
영속성 관리 - 5 (0) | 2021.06.26 |
댓글