연관관계 적용
연관관계 매핑(1 : N)
실제 프로젝트에 적용시켜본다.
연관관계 목차
요약
1. @Column(name = "registry_id") 로 Registry id 컬럼에 이름을 명시해준다.
2. Comment 의 Registry에 @JoinColumn으로 참조할 컬럼명을 지정해 준다.
3. Comment : Registry = N : 1 관계이기 때문에
- Comment의 Registry에는
@ManyToOne, Registry의 Comment에는@OneToMany
4. mappedBy : 주인이 아닌 것을 지정 (양방향 시 키를 관리할 주인을 지정)
mappedBy="주인쪽에 자신이 매핑되어 있는 필드명"
5. fetch = FetchType.LAZY : 지연 로딩으로 필요할 때만 쿼리를 날려 조회
매핑하기
매핑할 객체는 Registry와 Comment다.
연관관계 매핑 전까지는 직접 값을 넣어서 DB에 저장했다.
기존 코드에서 연관관계 매핑을 하면서 수정한 부분이 몇 군데 있다.
(아래 코드는 수정 이후의 코드다.)
Wrapper class로 수정
기존에 idx를 제외한 정수 타입을 int로 사용했다.
이 경우 null 값이 들어오면 기본값(0)이 들어가면서 오류인지 모르는 문제가 발생한다.
Wrapper 클래스(Integer, Long)를 사용하면 null 그대로 유지되어 오류를 바로 확인할 수 있다.
*MySQL에서 Long → BIGINT로 변경
GenerationType 수정
AUTO를 사용했으나 auto_increment 환경에서 문제가 발생해 IDENTITY로 변경했다.
자세한 내용은 Generationtype을 참고한다.
Registry.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@Entity
@ToString
public class Registry {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "registry_id")
private Long idx;
@Column(nullable = false)
private String nickname;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String main;
public Registry(RegistryDto registryDto) {
this.title = registryDto.getTitle();
this.main = registryDto.getMain();
this.nickname = registryDto.getNickname();
}
}
Comment.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Setter
@Getter
@NoArgsConstructor
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "comment_id")
private Long idx;
@Column(nullable = false)
private String nickname;
@Column(nullable = false)
private String comment;
@Column(nullable = false)
private Long registryId;
@Column(nullable = false)
private String registryNickname;
public Comment(CommentDto commentDto) {
this.nickname = commentDto.getNickname();
this.comment = commentDto.getComment();
this.registryId = commentDto.getRegistryId();
this.registryNickname = commentDto.getRegistryNickname();
}
}
관계
Registry와 Comment는 일대다 관계다.
(게시글 하나당 여러 개의 댓글)
매핑 방향
Registry와 Comment가 서로 참조하는 양방향 매핑으로 진행한다.
주인
1 : N 관계에서는 N이 주인이다.
따라서 Comment가 주인이며 @ManyToOne과 @JoinColumn을 작성한다.
1
2
3
@ManyToOne // 주인
@JoinColumn(name = "registry_id")
private Registry registry;
🐣 1:N 관계에서는 N이 주인일 수 밖에 없는 이유는 RDBMS에서 여러 개의 데이터가 들어갈 수 없기 때문이다.
fetch 옵션
- EAGER : 즉시 로딩
- LAZY : 지연 로딩
@ManyToOne(fetch = FetchType.LAZY)
기본값
@ManyToOne,@OneToOne→ EAGER@OneToMany,@ManyToMany→ LAZY
🐣 실무에서는 대부분 LAZY 사용을 권장
mappedBy
주인이 아닌 클래스(Registry)에 mappedBy를 지정한다.
일대다 관계에서는 컬렉션 타입 필드를 사용하며 반드시 초기화해야 한다.
1
2
3
4
// Registry
@OneToMany(mappedBy = "registry")
@JsonIgnore
private List<Comment> comments = new ArrayList<>();
mappedBy = "registry"는 Comment의 private Registry registry 필드를 의미한다.
컬렉션 초기화 이유
하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 내부 컬렉션으로 감싸서 관리한다.
초기화하지 않으면 NullPointerException이 발생할 수 있다.
양방향 매핑 시 주의사항
toString()- Lombok
- JSON 직렬화
양방향 매핑 시 순환 참조가 발생할 수 있다.
따라서 다음 처리가 필요하다.
@ToString.Exclude@JsonIgnore
연관관계 편의 메서드
Comment.java
1
2
3
4
5
6
7
8
9
10
public void setRegistry(Registry registry) {
if (this.registry != null) {
this.registry.getComments().remove(this);
}
this.registry = registry;
if (!registry.getComments().contains(this)) {
registry.addComment(this);
}
}
registry.addComment(this); : Registry에서 addComment()를 작성해 줬기 때문에 이를 활용하여 작성한 것
만약 작성하지 않았다면 registry.getComments().add(this); 로 작성해야 할 것
Registry.java
1
2
3
4
5
6
7
public void addComment(Comment comment) {
this.comments.add(comment);
if (comment.getRegistry() != this) {
comment.setRegistry(this);
}
}
comments는 리스트 형태이므로 내장 함수인 add()를 활용해 더해준 것
비즈니스 로직 적용
1
2
3
4
5
6
7
8
9
10
11
12
@Transactional
public Comment setComment(CommentDto commentDto) {
Comment comment = new Comment(commentDto);
Registry registry =
registryRepository.findById(commentDto.getRegistryId()).get();
comment.setRegistry(registry);
commentRepository.save(comment);
return comment;
}
Test 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@DisplayName("H2를 이용한 TEST")
@DataJpaTest
public class CommentAndRegistryTest {
@Autowired
RegistryRepository registryRepository;
@Autowired
CommentRepository commentRepository;
@Test
void comment_save_test() {
Registry registry = new Registry();
registry.setNickname("coco");
registry.setTitle("안녕하세요");
registry.setMain("hi");
registryRepository.save(registry);
Comment comment = new Comment();
comment.setNickname("우헤헤");
comment.setComment("❤️🧡💛💚💙💜");
comment.setRegistry(registry);
commentRepository.save(comment);
Comment savedComment = commentRepository.findById(1L).get();
Assertions.assertThat(savedComment.getRegistry().getNickname())
.isEqualTo("coco");
}
}
super 와 부모 생성자
기존에 작성했던 Registry test 코드에서 매핑한 Comment 값이 없어 에러가 떴다.
그래서 생성자 오버로딩을 통해서 기존 값이 오류가 나지 않게 했다.
1
2
3
public Registry(long idx, String nickname, String title, String main) {
super();
}
1
2
3
4
5
public class Parent {
public Parent() {
System.out.println("Parent 생성자 호출");
}
}
1
2
3
4
5
6
public class Child extends Parent {
public Child() {
super();
System.out.println("Child 생성자 호출");
}
}
1
2
3
4
5
public class Main {
public static void main(String[] args) {
Child child = new Child();
}
}
- 객체 생성 시 부모 생성자가 먼저 호출된다.
super()는 부모 생성자를 의미한다. (자신을 가리키는 키워드는 this)- 부모 생성자는 한 번만 호출된다.
reference
super와 부모생성자
