優れたNest.jsブログを構築する:投稿のタグ
Min-jun Kim
Dev Intern · Leapcell

前のチュートリアルでは、ブログに訪問者追跡を追加し、各記事の人気度を視覚的に確認できるようにしました。
ブログはどう見てもかなり完成に近づいていますが、まだ何かが足りないようです。ブログにはすでに多くの記事がありますが、ユーザーはそれらの中で迷ってしまうかもしれません…。では、ユーザーは興味のあるトピックをどのように素早く見つけることができるでしょうか?
その通り、ブログにはタグ機能が必要です。
タグは、記事のテーマや内容を表示するために使用されます。記事には複数のキーワードを割り当てることができます(例:「テクニカルチュートリアル」、「Nest.js」、「データベース」)。
次の2つのチュートリアルで、ブログシステムにタグ機能を追加します。このチュートリアルでは、まず記事の作成および編集時にタグを設定するサポートを実装します。
ステップ1:データモデリングとリレーションシップの構築
タグエンティティの作成
まず、この新しい概念に対応するモジュールとエンティティを作成しましょう。
nest generate module tags nest generate service tags
src/tags
ディレクトリに tag.entity.ts
を作成します。
// src/tags/tag.entity.ts import { Entity, Column, PrimaryColumn, ManyToMany } from 'typeorm'; import { Post } from '../posts/post.entity'; @Entity() export class Tag { @PrimaryColumn({ type: 'uuid', default: () => 'gen_random_uuid()' }) id: string; @Column({ unique: true }) name: string; @ManyToMany(() => Post, (post) => post.tags) posts: Post[]; }
投稿エンティティを更新してリレーションシップを確立する
次に、投稿とタグの間の関連を確立するために src/posts/post.entity.ts
ファイルを更新する必要があります。
1つの投稿には複数のタグを持つことができ、1つのタグは複数の投稿に関連付けることができます。これは 多対多 のリレーションシップです。
// src/posts/post.entity.ts import { Entity, Column, PrimaryColumn, CreateDateColumn, ManyToOne, ManyToMany, JoinTable } from 'typeorm'; import { Tag } from '../tags/tag.entity'; // ... その他のインポート @Entity() export class Post { // ... その他のフィールド (id, title, content など) @ManyToMany(() => Tag, (tag) => tag.posts, { cascade: true, // 投稿を介した新しいタグの作成を許可します }) @JoinTable() // `@JoinTable()` はリレーションの一方の側で指定する必要があります tags: Tag[]; // ... その他のリレーション (comments, views など) }
@JoinTable()
は多対多のリレーションシップを定義するために必要なデコレーターです。Post
とTag
の関連を管理するために、結合テーブルpost_tags_tag
を作成する必要があります。
データベーステーブル構造の更新
新しいテーブルとフィールドを作成するために、次のSQLステートメントを実行してください。
-- タグテーブルの作成 CREATE TABLE "tag" ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "name" VARCHAR UNIQUE NOT NULL ); -- post_tags_tag 結合テーブルの作成 CREATE TABLE "post_tags_tag" ( "postId" UUID REFERENCES "post" ON DELETE CASCADE, "tagId" UUID REFERENCES "tag" ON DELETE CASCADE, PRIMARY KEY ("postId", "tagId") );
Leapcell でデータベースを作成した場合、
ウェブサイトのデータベース管理ページに移動し、上記のステートメントをSQLインターフェイスに貼り付けて実行するだけで、グラフィカルインターフェイスを使用してSQLステートメントを簡単に実行できます。
ステップ2:バックエンドロジックの実装
タグの作成と検索を処理するサービスを作成し、投稿を作成する際のこれらの関連を処理するように PostsService
を更新する必要があります。
TagsService の記述
このサービスのロジックは比較的単純で、主にタグの検索または作成に焦点を当てています。
src/tags/tags.module.ts
を開き、TypeOrmModule
を登録します。
// src/tags/tags.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Tag } from './tag.entity'; import { TagsService } from './tags.service'; @Module({ imports: [TypeOrmModule.forFeature([Tag])], providers: [TagsService], exports: [TagsService], // 他のモジュールが使用できるようにサービスをエクスポートします }) export class TagsModule {}
src/tags/tags.service.ts
で:
// src/tags/tags.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, In } from 'typeorm'; import { Tag } from './tag.entity'; @Injectable() export class TagsService { constructor( @InjectRepository(Tag) private tagsRepository: Repository<Tag> ) {} // タグの検索または作成 async findOrCreate(tagNames: string[]): Promise<Tag[]> { const existingTags = await this.tagsRepository.findBy({ name: In(tagNames) }); const existingTagNames = existingTags.map((tag) => tag.name); const newTagNames = tagNames.filter((name) => !existingTagNames.includes(name)); const newTags = newTagNames.map((name) => this.tagsRepository.create({ name })); await this.tagsRepository.save(newTags); return [...existingTags, ...newTags]; } }
PostsService を更新して関連を処理する
create
および findOne
メソッドを変更する必要があります。
まず、PostsModule
に TagsModule
をインポートします。
// src/posts/posts.module.ts import { TagsModule } from '../tags/tags.module'; @Module({ imports: [TypeOrmModule.forFeature([Post]), CommentsModule, TrackingModule, TagsModule], // ... }) export class PostsModule {}
次に src/posts/posts.service.ts
を更新します。
// src/posts/posts.service.ts import { Injectable } from '@nestjs/common'; import { TagsService } from '../tags/tags.service'; // ... その他のインポート @Injectable() export class PostsService { constructor( @InjectRepository(Post) private postsRepository: Repository<Post>, private readonly tagsService: TagsService // TagsService をインジェクトします ) {} // create メソッドを更新します async create(post: Omit<Partial<Post>, 'tags'> & { tags: string }): Promise<Post> { const tagNames = post.tags .split(',') .map((tag) => tag.trim()) .filter(Boolean); const tags = await this.tagsService.findOrCreate(tagNames); const newPost = this.postsRepository.create({ ...post, tags, }); return this.postsRepository.save(newPost); } // 関連データをロードするために findOne メソッドを更新します findOne(id: string): Promise<Post | null> { return this.postsRepository.findOne({ where: { id }, relations: ['tags'], // タグをロードします }); } // ... その他のメソッド }
ステップ3:フロントエンドページの統合
最終ステップは、EJSテンプレートを変更して、投稿の作成および編集時にタグを設定できるようにし、投稿詳細ページにタグを表示することです。
新規/編集投稿ページの更新
views/new-post.ejs
を開き、タグを入力するためのフォームフィールドを追加します。
<form action="/posts" method="POST" class="post-form"> <div class="form-group"> <label for="tags">Tags (comma-separated)</label> <input type="text" id="tags" name="tags" /> </div> <button type="submit">Submit</button> </form>
簡単にするため、ここでは複数のタグにコンマ区切りの入力を利用しています。実際のプロジェクトでは、ユーザーエクスペリエンスを向上させるために、専用のタグ入力コンポーネント、既存タグの自動マッチングなど、より複雑なUIコンポーネントやロジックを使用できます。
投稿詳細ページの更新
views/post.ejs
を開き、投稿のメタデータにタグを表示します。
<article class="post-detail"> <h1><%= post.title %></h1> <small> <%= new Date(post.createdAt).toLocaleDateString() %> | Views: <%= viewCount %> </small> <div class="post-content"><%- post.content %></div> <% if (post.tags && post.tags.length > 0) { %> <div class="tags-section"> <strong>Tags:</strong> <% post.tags.forEach(tag => { %> <a href="/tags/<%= tag.id %>" class="tag-item"><%= tag.name %></a> <% }) %> </div> <% } %> </article>
実行とテスト
アプリケーションを再起動します。
npm run start:dev
ブラウザを開き、 http://localhost:3000/ にアクセスします。
新しい投稿を作成すると、下部にタグ入力ボックスが表示されます。
Nest.js, Tutorial
のように、コンマで区切ってタグを入力し、送信します。
送信後、投稿詳細ページに移動すると、投稿のタグが正常に表示されていることがわかります。
これで、ブログはタグの作成と表示をサポートするようになりました。ただし、ユーザーはまだタグで記事をフィルタリングすることはできません。この機能は次のチュートリアルで実装します。
過去のチュートリアル: