여기서는 Post와 Attachment의 관계를 다룬다. 우리 시나리오에서는 게시글 하나에 첨부파일은 하나만 가능하다고 하자. 따라서 Post와 Attachment는 1:1의 관계가 된다. 1:1 관계의 특징과 처리 방법을 살펴보자.
일반적으로 DB에서 1:1 관계를 처리하기 위해서는 보조테이블(Attachment)에 F.K를 설정하고 관계의 소유자로 만든다. 보조테이블(Attachment)에는 데이터가 없을 수 있지만 주테이블(Post)에는 데이터가 없을 수 있기 때문이다. 아무튼 F.K를 이용하면 join을 이용해서 서로를 탐색할 수 있다.
JPA에서는 @OneToOne을 이용해서 1:1 관계를 처리한다. @OneToOne도 기본적으로는 단방향이다. 따라서 양방향을 설정하기 위해서는 양쪽 엔티티 모두에게 @OneToOne 설정이 필요하다.
mappedBy: 양방향 관계에서 관계 비소유 엔티티에서 관계 소유 엔티티의 연결 필드를 설정한다.
optional: false로 설정하면 inner join으로 동작하므로 연관된 데이터가 양쪽에 모두 있을 경우만 조회된다. 기본은 true이므로 left outer join이다.
그럼 Post와 Attachment에 대해 1:1 관계를 설정해보자.
@OneToOne을 이용한 단방향 관계 처리
Attachment 수정
Attachment는 Post에 대한 참조를 갖는다. 여기에 @OneToOne을 추가해주면 관계 처리는 끝이다.
package com.doding.board.post.model.entity;
import com.doding.board.common.entity.BaseEnttiy;
import jakarta.persistence.*;
import lombok.*;
import lombok.experimental.SuperBuilder;
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString(callSuper = true)
@Entity
public class Attachment extends BaseEnttiy {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long ano;
// private Long pno;
private String file;
@OneToOne
@JoinColumn(name="pno")
@ToString.Exclude
private Post post;
}
테스트를 통한 단방향 관계 확인
위 관계를 그대로 엔티티로 가져오려면 Attachment에 @OneToOne으로 Post를 참조하게 하면 된다. 아래 테스트는 Attachment를 통해서 Post를 조회하는 단방향은 가능하지만 반대 방향은 불가하다.
@Test
public void oneToOneTest() {
Attachment attachment = arepo.findById(1L).get();
Post post = attachment.getPost(); // Attachment를 통해 Post 조회: 단방향
Assertions.assertEquals(post.getTitle(), "title");
}
이때 생성된 DDL을 보면 attachment에서 pno을 이용해 post와 관계를 맺고 있음을 알 수 있다. 또한 unique로 설정되어 동일한 pno에는 하나의 attachment만 가능하다.
Hibernate:
create table attachment (
ano bigint generated by default as identity,
create_time timestamp(6),
pno bigint unique, # unique로 설정됨
update_time timestamp(6),
file varchar(255),
primary key (ano)
)
Hibernate:
alter table if exists attachment add constraint FKtyftxc9bkxgsudkk32ynpw0r
foreign key (pno) references post
@OneToOne을 이용한 양방향 관계 처리
양방향 관계의 추가
양방향 관계를 위해서 Post에 Attachment와의 관계를 추가해주자. 관계의 주인인 Attachment에서 Post post로 참조하고 있으므로 Post 엔티티의 @OneToOne에는 mappedBy="post"가 필요하다.
@Entity
public class Post extends BaseEnttiy {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long pno;
. . .
@OneToOne(mappedBy = "post")
@ToString.Exclude
private Attachment attachment;
}
연관관계 helper 메서드
필요한 에너테이션 추가와 함께 데이터베이스와의 일관성을 유지하기 위해 연관관계 helper 메서드의 작성도 잊지 말자. 우리는 주로 Post에 Attachment를 추가하므로 Post에 다음과 같이 setAttachment를 추가해보자.
public class Post extends BaseEnttiy {
. . .
public void setAttachment(Attachment attachment) {
// 1단계
if(this.attachment != null) {
this.attachment.setPost(null); // 기존 관계 제거
}
// 2단계
this.attachment = attachment;// post에 새로운 attachment 추가
// 3단계
if(this.attachment != null) {
this.attachment.setPost(this); // 양방향 관계 설정
}
}
}
양방향 동작의 확인
이제 원활한 관계가 맺어졌다면 다음 테스트 처럼 양방향 탐색이 가능해진다.
@Test
public void oneToOneTest() {
Attachment attachment = atrepo.findById(1L).get();
Post post = attachment.getPost();
// then
Assertions.assertEquals(post.getTitle(), "title_001");
Assertions.assertEquals(post.getAttachment(), attachment);
}
또한 연관관계 helper 메서드를 통해서 엔티티간의 연관관계도 손쉽게 관리가 가능하다.
@Test
@DisplayName("@OneToOne은 unique 제약사항을 만들므로 동일한 post 번호로 attachment가 있을 수 없다. 따라서 먼저 지우고 insert 해야 함.")
public void oneToOneModifyTest(){
Post post = prepo.findById(1L).get();
Assertions.assertNotNull(post.getAttachment());
// 새로운 attachment 생성
Attachment attachment = Attachment.builder().file("myfile").build();
atrepo.delete(post.getAttachment());
atrepo.flush(); // insert 되기 전에 delete 되어야 하기 때문에 명시적 호출
post.setAttachment(attachment);
atrepo.saveAndFlush(attachment); // 키를 가져와야 하니까 insert가 자동으로 호출됨
Assertions.assertEquals(post.getAttachment().getFile(), "myfile");
}
auto increment로 @ID를 사용하는 경우 insert query가 flush 호출 전에 호출된다는 점을 기억하자. unique 제약사항을 피하기 위해서 먼저 기존 attachment를 insert 보다 먼저 실행해야 한다. 따라서 save()전에 flush() 호출이 필요하다.