데이터베이스, ORM/TypeORM

[TypeORM] @OneToMany, @ManyToOne - 옵션: cascade

SparkIT 2024. 11. 23. 05:24
[TypeORM] @OneToMany, @ManyToOne - 옵션: cascade
[TypeORM] @OneToMany, @ManyToOne - 옵션: eager / lazy
[TypeORM] @OneToMany, @ManyToOne - 옵션: onDelete / onUpdate

 

typeorm 사용해서 1:N 관계, N:1 관계를 구현할 때 사용되는 cascade 옵션에 대한 설명입니다.

 


cascade

설명에 앞서 설명에서 사용될 용어들을 미리 설명하고 가겠습니다.

  • 부모(엔티티)
    1:N 관계에서 1 쪽 엔티티를 의미. 게시물과 댓글 관계에서 게시물이 부모 역할을 한다.
  • 자식(엔티티)
    1:N 관계에서 N 쪽 엔티티를 의미. 게시물과 댓글 관계에서 댓글이 자식 역할을 한다.

 

 정의

부모 엔티티 작업을 자식 엔티티에 전파하는 옵션입니다. 더 자세하게 설명하자면 부모 자체의 작업이 아니라 부모 레벨에서 자식에 대한 삭제, 수정, 추가 작업을 진행했을 때 실제 자식도 삭제, 수정, 추가되게 할지말지 정하는 옵션이라고 생각하면 됩니다. 이는 단뱡향 옵션으로 부모에서 자식으로 일방적으로 전파되는 구조입니다. 이는 save 메서드를 통해서만 동작합니다.( 더 정확한 예시는 아래 예제에서 )

❗️여기서 주의할 점은 바로 save 메서드를 통해서만 동작한다는 것입니다. 만약 새로운 부모 엔티티를 create 메서드로 생성 후 insert 메서드로 추가한다면 해당 부모 엔티티에 추가한 자식 엔티티는 생성되지 않습니다. 부모 엔티티만 insert 메서드를 통해 데이터베이스에 추가되고, 부모 엔티티에 설정한 자식 엔티티는 데이터베이스에 추가되지 않습니다. save 메서드를 이용해야한 자식 엔티티 데이터도 데이터베이스에 추가됩니다.

 

📄 설정값

  • cascade
    • true : 모든 작업을 자식 엔티티에 전파함
    • false : 모든 작업을 자식 엔티티에 전파하지 않음
    • insert : 삽입 작업을 자식 엔티티에 전판함
    • update : 수정 작업을 자식 엔티티에 전파함
    • remove : 삭제 작업을 자식 엔티티에 전파함
    • soft-remove : soft delete 작업을 자식 엔티티에 전파함( @DeleteDateColumn, softRemove 메서드 활용 )
    • recover : soft delete에 대한 복구 작업을 자식 엔티티에 전파함( @DeleteDateColumn, recover 메서드 활용 )

기본적으로 부모 엔티티 작업을 자식 엔티티에 전파하는 옵션이므로 부모 엔티티 상의 @OneToMany() 에만 적용 가능한 옵션입니다.

 

🔍  예제

# 게시물

@Entity()
export class Post {
  @OneToMany(() => Comment, (comment) => comment.post, { cascade: true })
  comments: Comment[];
}
# 댓글 

@Entity()
export class Comment {
  @ManyToOne(() => Post, (post) => post.comments)
  post: Post;
}

게시물(Post) 엔티티에서 @OneToMany 설정한 댓글 변수(comments)에 { cascade: true } 옵션을 적용한 예제입니다.

이 경우 부모 엔티티에 cascade: true 옵션을 적용했으므로 부모(게시물)에서 댓글(자식)을 삽입/수정/삭제한다면 실제로 자식(댓글) 정보도 모두 삽입/수정/삭제됩니다.

# 서비스 코드 중 일부

@Injectable()
export class PostService {
	constructor(
		@InjectRepository(Post)
		private readonly postRepository: Repository<Post>,
		@InjectRepository(PostComment)
		private readonly postCommentRepository: Repository<PostComment>,
	) {}

	async create(createPostDto: CreatePostDto) {
		const postComment = this.postCommentRepository.create({
			content: 'testContent',
		});
		const post = this.postRepository.create({
			content: 'testContent',
			tag: 'testTag',
			title: 'testTitle',
			topic: 'testTopic',
			postComments: [postComment],
		});
		await this.postRepository.save(post);
		return 'This action adds a new post';
	}
}

위 코드를 보면 postComment, post 객체를 만들고 post 객체에 postComment 객체를 삽입합니다. 그리고 post 객체만 save 메서드로 저장합니다. 하지만 해당 코드를 실행하면 결과적으로 post 데이터뿐만 아니라 postComment 데이터도 추가되는 모습을 확인할 수 있습니다. 아래는 비슷한 예시들입니다.

async update(id: number, updatePostDto: UpdatePostDto) {
    const post = await this.postRepository.findOne({
        where: { id },
        relations: ['postComments'],
    });
    post.postComments[0].content = '업데이트 전파 확인';
    await this.postRepository.save(post);
    return `This action updates a #${id} post`;
}

위 코드 실행 시 실제 자식 엔티티에 해당하는 postComment의 content 부분이 '업데이트 전파 확인'이라는 값으로 수정됨을 확인할 수 있습니다.

async remove(id: number) {
    const post = await this.postRepository.findOne({
        where: { id },
        relations: ['postComments'],
    });
    post.postComments = [];
    await this.postRepository.save(post);
    return `This action removes a #${id} post`;
}

위 코드 실행 시 실제 자식 엔티티에 해당하는 postComment의 post_id(FK)가 null 처리됨을 확인할 수 있습니다.

 

🧐 예제에서 사용되는 실제 쿼리

위 예제에서 사용되는 실제 쿼리를 보면서 typeorm 동작 방식에 대해 더 알아봅시다

1. { cascade: true } 데이터 추가

// code
async create() {
    const postComment = this.postCommentRepository.create({
        content: 'testComment',
    });
    const post = this.postRepository.create({
        content: 'test',
        tag: 'test',
        title: 'test',
        topic: 'test',
        postComments: [postComment],
    });
    await this.postRepository.save(post);
    return 'This action adds a new post';
}

// query
query: START TRANSACTION
query: INSERT INTO `post`(`id`, `topic`, `title`, `tag`, `content`, `createdAt`) VALUES (DEFAULT, ?, ?, ?, ?, DEFAULT) -- PARAMETERS: ["test","test","test","test"]
query: SELECT `Post`.`id` AS `Post_id`, `Post`.`createdAt` AS `Post_createdAt` FROM `post` `Post` WHERE `Post`.`id` = ? -- PARAMETERS: [7]
query: INSERT INTO `post_comment`(`id`, `content`, `postId`) VALUES (DEFAULT, ?, ?) -- PARAMETERS: ["testComment",7]
query: COMMIT

위와 같이 트랜잭션으로 처리되는 모습을 확인할 수 있었습니다. 트랜잭션에 포함된 쿼리는 post 데이터 추가, post 데이터 id 조회, 해당 post 데이터 id를 통해서 post comment 데이터 추가로 총 3 가지입니다.