연관관계 매핑(1 : N)
실제 프로젝트에 적용시켜본다.
연관관계 목차
요약
@Column(name = “registry_id)
로 Registry id 컬럼에 이름을 명시해준다.Comment
의 Registry에JoinColumn
으로 참조할 컬럼명을 지정해 준다.Comment : Registry = N : 1 관계이기 때문에, Comment 의 Registry에는
@ManyToOne
, Registry의 Comment는@OneToMany
mappedBy
: 주인이 아닌 것을 지정 (양방향시 키를 관리할 주인이 누구인지 지정)mappedBy="(주인쪽에 자신이 매핑되어 있는 필드명)"
fetch = FetchType.LAZY
: 지연로딩으로 필요에 의해서만 쿼리를 날려 조회할수 있게 설정
매핑하기
매핑할 객체는 Registry와 Comment다.
연관관계 매핑 전까지는 직접 값을 넣어서 db에 넣어줬다.
기존 코드에서 연관관계 매핑을 하면서 수정한 부분이 몇 군데 있다.(아래 코드는 수정 이후의 코드다.)
Wrapper class로 수정
기존에 idx를 제외하고는 정수는 int로 설정했었다.
이럴 경우 문제가 생기는데 값이 null로 들어오면 원시 자료형은 기본 값이 들어와버린다. (ex. int = 0)
그렇게 되면 이게 null인지 모르므로 Wrapper 클래스인 Integer로 설정하게 되면
null 값으로 떠서 에러가 뜨므로 이러한 대비를 하는게 좋다.
- mysql에서 long은 bigint로 설정하면 된다.
GenerationType 수정
auto_increment를 적용하기 위해 자연스럽게 계속 AUTO
만 사용하고 있었는데
AUTO
를 생성하면서 문제가 생겼고 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
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@Entity
@ToString
public class Registry extends Timestamped {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
@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
@Setter
@Getter
@NoArgsConstructor
@Entity
public class Comment extends Timestamped {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
@Column(name = "COMMENT_ID")
private Long idx;
@Column(nullable = false)
private String nickname;
@Column(nullable = false)
private String comment;
@Column(nullable = false)
private Long registryIdd;
@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가 주인이므로 매핑종류와 @JoinColumn을 작성해준다.
@JoinColumn의 name은 pk를 fk로 설정해서 저장해둘 이름을 작성한다.
1
2
3
@ManyToOne // 주인
@JoinColumn(name = "registry_id")
private Registry registry;
fetch : 연관관계가 있는 도메인 로딩 전략 설정 옵션 |
- EAGER[이거 로딩] : 즉시 로딩
- LAZY[레이지 로딩] : 지연 로딩
- ex)
@ManyToOne(fetch = FetchType.LAZY)
- ex)
Comment를 불러올 때 연관관계인 Registry도 같이 즉시 조회하고 싶으면 EAGER를 사용하고 LAZY는 필요로 할 때만 쓰인다.
default값
- @OneToOne, @ManyToOne: EAGER
- @OneToMany, @ManyToMany: LAZY
🐣 EAGER를 사용해 많은 데이터가 로딩되면 부하가 심해서 실무에서는 LAZY로 사용하는 것을 추천한다.
🐣 연관관계 맺은것이 많이 써먹는 메소드가 많다면 EAGER를 사용하고 연관관계 맺은 것이 써먹는 게 없으면 LAZY를 사용한다.
ex) User와 Registry의 1:N 관계에서 EAGER와 LAZY 쿼리문 차이
🐣 1:N 관계에서는 N이 주인일 수 밖에 없는 이유는 RDBMS에서 여러 개의 데이터가 들어갈 수 없기 때문이다.
자바에서 객체를 불러올 때는 가능하다.
주인이 아닌 클래스(Registry.java)에는 mappedBy 속성으로 주인을 지정한다.
일대다 혹은 다대일 매핑에서 일인 클래스에 다인 클래스를 컬렉션으로 적어준다.
또한 일대다 컬렉션 타입 필드는 “초기화” 해줘야 한다.
1
2
3
@OneToMany(mappedBy = "registry")
@JsonIgnore
private List<Comment> comments = new ArrayList<>(); // 초기화
mappedBy에서는 반대쪽 필드 명을 적는 것이다.
Comment.java의 private Registry registry;
에서 registry를 적은 것이다.
초기화
1
2
3
4
5
하이버네이트는 엔티티를 영속 상태로 만들 때, 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸서 사용한다.
컬렉션을 효율적으로 관리하기 위해서이며, 이런 특징 때문에 컬렉션을 사용할 때 다음처럼 즉시 초기화해서 사용하는 것을 권장한다.
초기화를 시키지 않으면 NullPointerException이 발생한다.
Collection
Collection은 List, Set, Map이 있다.
- Collection : 자바가 제공하는 최상위 컬렉션이다.
- List : 순서가 있고, 중복을 허용한다.
- Set : 순서가 없고, 중복은 허용하지 않는다
- Map : Key, Value로 되어있으며 키는 중복을 불허한다.
양방향 매핑시 조심해야 할 것들
- Ex: toString(), lombk , JSON 생성 라이브러리
@ToString
Comment를 문자열로 표현하기 위해 그 안에 Registry의 toString 을 실행한다.
Registry안에 Comment가 있기때문에 Comment의 toString 실행 → Comment안에 Registry가 있기때문에 Profile의 toString 실행….
이러한 무한 루프에 빠지게 되는데 이 무한 루프를 순환참조라고 한다.
따라서, 양방향 매핑 시 @ToString에서 필드를 제외 시켜줘야한다. → @ToString.Exclude
@RestController의 경우 @ResponseBody로 객체를 반환해 줄 때 JSON 형태로 변환해주는데
이때도 마찬가지로 매핑된 필드를 제외해줘야한다. → @JsonIgnore
매핑 적용하기
Comment가 Registry를 가져올수는 있는데 값을 설정해주지 않아서 가져오려면 로직 작성이 필요하다.
연관관계를 맺을 때 Registry에서는 Comment가, Comment에는 Registry 로직이 들어가야 하는데
setter 어노테이션이 해줄 수 있지만 기본 setter만 해주기 때문에 연관관계 매핑 로직이 포함 되지 않아서 작성해줘야 한다.
메소드명은 상관없지만 setter 로직이니깐 setRegistry로 작성했다.
Comment.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@ManyToOne // 주이이인
@JoinColumn(name = "registry_id")
private Registry registry;
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
8
9
10
11
12
@OneToMany(mappedBy = "registry")
@JsonIgnore
private List<Comment> comments = new ArrayList<>();
public void addComment(Comment comment) {
this.comments.add(comment);
// 무한 루프 안걸리게 하기
if(comment.getRegistry() != this) {
comment.setRegistry(this);
}
}
comments는 리스트 형태이므로 내장 함수인 add()를 활용해 더해준 것이다.
🐣 기본 setter와 getter
1
2
3
4
5
6
7
8
9
10
11
private 타입 fieldName
// Getter
public 리턴타입 getFieldName() {
return fieldName;
}
// Setter
public void setFieldName(타입 fieldName) {
this.fieldName = fieldName;
}
MySQL 같은 RDBMS에 여러 개 데이터가 들어갈 수 없어서 N이 주인이 될 수밖에 없다.
같은 이유로 여러 개 데이터가 들어갈 수 없기 때문에 add와 set 메소드 작성이 필요하다.
만약 add와 set 메소드를 작성안했다면 값을 설정할 수 없다.
ORM이 db 레코드를 객체로 바꿔주거나 객체를 레코드로 바꾸는 과정에서
add와 set과 같이 값을 지정해주는 메소드가 없다면 여러 개의 데이터가 들어갈 수 없기 때문에 null로 저장된다.
여러 개 데이터 저장 형식에는 List, Set, Map 등이 있다.
코드 작성하기
실제 비즈니스 로직에 연관관계 매핑 코드를 작성한다.
1
2
3
4
5
6
@Transactional
public Comment setComment(CommentDto commentDto) {
Comment comment = new Comment(commentDto);
commentRepository.save(comment);
return comment;
}
1
2
3
4
5
6
7
8
9
10
11
@Transactional
public Comment setComment(CommentDto commentDto) {
Comment comment = new Comment(commentDto);
// 연관관계 매핑
Registry registry = registryRepository.findById(comment.getRegistry().getIdx()).get();
comment.setRegistry(registry);
commentRepository.save(comment);
return comment;
}
test 코드 작성하기
*참고로 객체 GenerationType이 AUTO이면 아래 test가 에러가 날 것이다.(IDENTITY로 수정한다.)
자세한 내용은 아래 글을 참고한다.
Generationtype
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
32
33
34
35
36
@DisplayName("H2를 이용한 TEST")
@DataJpaTest
public class CommentandRegistryTest {
@Autowired
RegistryRepository registryRepository;
@Autowired
CommentRepository commentRepository;
@Test
void commentSave_Identity() {
Registry registry = new Registry();
registry.setNickname("coco");
registry.setTitle("안녕하세요");
registry.setMain("hi");
registryRepository.save(registry);
Comment comment = new Comment();
comment.setComment("❤️🧡💛💚💙💜🤎🖤");
comment.setNickname("우헤헤");
comment.setRegistryId(5L);
comment.setRegistryNickname("pop");
comment.setRegistry(registry);
commentRepository.save(comment);
Comment savedComment = commentRepository.findById(1L).get();
Registry savedRegistry = savedComment.getRegistry();
Assertions.assertThat("coco").isEqualTo(savedRegistry.getNickname());
Assertions.assertThat("❤️🧡💛💚💙💜🤎🖤").isEqualTo(savedComment.getComment());
}
}
참고
기존에 작성했던 Registry test 코드에서 매핑한 Comment 값이 없어 에러가 떴다.
그래서 생성자 오버로딩을 통해서 기존 값이 오류가 나지 않게 했다.
1
2
3
public Registry(long idx, String nickname, String title, String main) {
super();
}
super와 부모생성자
class가 인스턴스화 될때 생성자가 실행되면서 객체의 초기화를 한다.
그 때 자신의 생성자만 실행이 되는것이 아니고, 부모의 생성자부터 실행된다.
1
2
3
4
5
6
7
8
9
10
11
12
public class Car{
public Car(){
System.out.println("Car의 기본생성자입니다.");
}
}
public class Bus extends Car{
public Bus(){
System.out.println("Bus의 기본생성자입니다.");
}
}
1
2
3
4
5
public class BusExam{
public static void main(String args[]){
Bus b = new Bus();
}
}
- new 연산자로 Bus객체를 생성하면, Bus객체가 메모리에 올라갈때 부모인 Car도 함께 메모리에 올라간다.
- 생성자는 객체를 초기화 하는 일을한다.
- 생성자가 호출될 때 자동으로 부모의 생성자가 호출되면서 부모객체를 초기화 하게된다.
super 자신을 가리키는 키워드가 this 라면, 부모들 가리키는 키워드는 super
super()
는 부모의 생성자를 의미한다.
부모의 생성자를 임의로 호출하지 않으면, 부모 class의 기본 생성자가 자동으로 호출된다.
아래처럼 호출해보면 위에서 super()를 호출하지 않을 때와 결과가 같다.
1
2
3
4
public Bus(){
super();
System.out.println("Bus의 기본생성자입니다.");
}
- super 키워드는 자식에서 부모의 메소드나 필드를 사용할 때도 사용한다.
- 부모 클래스의 생성자는 한 번만 호출할 수 있다.
출처 super와 부모생성자