Nest.jsブログをステップバイステップで構築:コメントシステム
Takashi Yamamoto
Infrastructure Engineer · Leapcell

前回のチュートリアルでは、セッションとPassport.jsに基づいたブログの完全なユーザー認証システムを統合しました。これで、ユーザーはブログシステムに登録してログインでき、アクセスにはログインが必要な保護されたルートができました。
読者と著者を明確に区別できるようになったので、彼ら間のインタラクションのための機能を追加するのに最適な時期ではないでしょうか?
この記事では、ブログの基本的かつコアな機能であるコメントシステムを追加します。
具体的には、以下の機能を実装します。
- 各投稿の下にコメントリストを表示する。
- ログインしたユーザーがコメントを投稿できるようにする。
ステップ1:コメントのデータモデルを作成する
Post
およびUser
エンティティと同様に、Comment
エンティティにも独自のデータベーステーブルと対応するエンティティファイルが必要です。
1. データベーステーブルの作成
まず、PostgreSQLデータベースで次のSQLステートメントを実行して、comment
テーブルを作成します。このテーブルは、postId
およびuserId
を介してpost
およびuser
テーブルとのリレーションシップを確立します。
CREATE TABLE "comment" ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "content" TEXT NOT NULL, "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), "postId" UUID REFERENCES "post"("id") ON DELETE CASCADE, "userId" UUID REFERENCES "user"("id") ON DELETE CASCADE );
ヒント:
ON DELETE CASCADE
は、Post
またはUser
が削除されたときに、データの一貫性を確保するために、それに関連するすべてのコメントも自動的に削除されることを意味します。
2. Commentエンティティの作成
次に、Nest CLIを使用して、コメント関連のすべてのロジックを管理するための新しい comments
モジュールを作成します。
nest generate module comments nest generate service comments
コメントの送信と表示は投稿にアタッチされるため、現時点ではコメント用の別個のコントローラーは必要ありません。したがって、関連するルーティングロジックはPostsController
と、後で作成するCommentsController
に配置します。
src/comments
ディレクトリにcomment.entity.ts
という名前のファイルを作成します。
// src/comments/comment.entity.ts import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, ManyToOne } from 'typeorm'; import { User } from '../users/user.entity'; import { Post } from '../posts/post.entity'; @Entity() export class Comment { @PrimaryGeneratedColumn('uuid') id: string; @Column('text') content: string; @CreateDateColumn() createdAt: Date; @ManyToOne(() => User, (user) => user.comments) user: User; @ManyToOne(() => Post, (post) => post.comments) post: Post; }
3. 関連エンティティの更新
コメントからユーザーおよび投稿へのManyToOne
リレーションシップを定義しました。次に、User
およびPost
エンティティで逆のOneToMany
リレーションシップを定義する必要があります。
src/users/user.entity.ts
を更新します:
// src/users/user.entity.ts import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; import { Comment } from '../comments/comment.entity'; // Commentをインポート @Entity() export class User { @PrimaryGeneratedColumn('uuid') id: string; @Column({ unique: true }) username: string; @Column() password: string; @OneToMany(() => Comment, (comment) => comment.user) // リレーションシップを追加 comments: Comment[]; }
src/posts/post.entity.ts
を更新します:
// src/posts/post.entity.ts import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, OneToMany } from 'typeorm'; import { Comment } from '../comments/comment.entity'; // Commentをインポート @Entity() export class Post { @PrimaryGeneratedColumn('uuid') id: string; @Column() title: string; @Column('text') content: string; @CreateDateColumn() createdAt: Date; @OneToMany(() => Comment, (comment) => comment.post) // リレーションシップを追加 comments: Comment[]; }
ステップ2:CommentServiceの実装
次に、コメントの作成とクエリを処理する CommentsService
を記述します。
1. Commentエンティティの登録
src/comments/comments.module.ts
を開き、TypeOrmModule
を登録してCommentsService
をエクスポートし、他のモジュールが使用できるようにします。
// src/comments/comments.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CommentsService } from './comments.service'; import { Comment } from './comment.entity'; @Module({ imports: [TypeOrmModule.forFeature([Comment])], providers: [CommentsService], exports: [CommentsService], // サービスをエクスポート }) export class CommentsModule {}
2. Serviceロジックの記述
src/comments/comments.service.ts
ファイルを変更して、コメントの作成とクエリのためのメソッドを追加します。
// src/comments/comments.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Comment } from './comment.entity'; import { Post } from '../posts/post.entity'; import { User } from '../users/user.entity'; @Injectable() export class CommentsService { constructor( @InjectRepository(Comment) private commentsRepository: Repository<Comment> ) {} // 投稿IDでコメントをすべて検索(ユーザー情報を含む) findByPostId(postId: string): Promise<Comment[]> { return this.commentsRepository.find({ where: { post: { id: postId } }, relations: ['user'], // キー:関連するユーザーオブジェクトもロードする order: { createdAt: 'ASC', // 作成時間で昇順にソート }, }); } // コメントを作成する async create(content: string, user: User, post: Post): Promise<Comment> { const newComment = this.commentsRepository.create({ content, user, post, }); return this.commentsRepository.save(newComment); } }
TypeORMの強力な機能である
relations: ['user']
は、TypeORMにコメントをクエリするときに、外部キーを介して関連するuser
オブジェクトを自動的に取得して入力するように指示します。これにより、コメント著者のユーザー名を簡単に取得できます。
ステップ3:コメントの送信と表示の実装
次に、投稿ページにコメント機能統合します。これには、コメント送信を処理するコントローラーの作成と、投稿ページにコメントを表示するためにPostsController
の更新が含まれます。
1. CommentsController
の作成
コメント送信はPostsController
で処理できますが、関心事の分離を維持するために、専用のコントローラーを作成します。
nest generate controller comments
src/comments/comments.controller.ts
を修正します。
// src/comments/comments.controller.ts import { Controller, Post, Body, Param, Req, Res, UseGuards } from '@nestjs/common'; import { CommentsService } from './comments.service'; import { AuthenticatedGuard } from '../auth/authentic<ctrl63>src/components/Footer.js.js.html.js.jsx.tsx.tsx.tsx.tsx.jsx--2870210027.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json.json<ctrl63>meta.mdx/page.tsxio-blog-step-by-step-comment-system