FastAPIで完璧なブログを構築:投稿のためのタグ機能
Ethan Miller
Product Engineer · Leapcell

前回の記事では、ブログに訪問者トラッキングを追加し、各投稿の人気度を視覚的に確認できるようになりました。
ブログはかなり完成度が高く見えますが、まだ何かが欠けているようです。ブログにはすでに多くの投稿があり、ユーザーは迷ってしまうかもしれません…では、ユーザーが興味のあるトピックを素早く見つけるにはどうすればよいでしょうか?
その通り、ブログにはタグ機能が必要になりました。
タグは、コンテンツを整理・分類するための古典的な方法です。各投稿にキーワード(例:「FastAPI」、「Python」、「Database」)を割り当てることで、読者は特定のトピックに関連するすべての記事を簡単に見つけることができます。
次の2つのチュートリアルで、ブログシステムに完全なタグ機能を追加します。このチュートリアルでは、まず基本的な部分、つまり投稿作成時のタグ設定と、投稿ページでのタグ表示をサポートします。
ステップ1:タグのデータモデルを作成する
タグ機能を実装するには、新しいTag
モデルが必要で、Post
とTag
の間にリレーションシップを確立する必要があります。1つの投稿に複数のタグを付けることができ、1つのタグは複数の投稿に関連付けることができます。これは典型的な多対多リレーションシップです。
SQLModel(およびほとんどのORM)では、多対多リレーションシップを実装するには、Post
とTag
のペアリングリレーションシップを格納するための追加の「リンクテーブル」が必要です。
1. モデルファイルを更新する
models.py
ファイルを開き、Tag
とPostTagLink
モデルを追加し、Post
モデルを更新します。
# models.py import uuid from datetime import datetime from typing import Optional, List from sqlmodel import Field, SQLModel, Relationship # ... User, PageView, Comment クラス ... class PostTagLink(SQLModel, table=True): post_id: Optional[uuid.UUID] = Field( default=None, foreign_key="post.id", primary_key=True ) tag_id: Optional[uuid.UUID] = Field( default=None, foreign_key="tag.id", primary_key=True ) class Tag(SQLModel, table=True): id: Optional[uuid.UUID] = Field(default_factory=uuid.uuid4, primary_key=True) name: str = Field(unique=True, index=True) posts: List["Post"] = Relationship(back_populates="tags", link_model=PostTagLink) class Post(SQLModel, table=True): id: Optional[uuid.UUID] = Field(default_factory=uuid.uuid4, primary_key=True) title: str content: str createdAt: datetime = Field(default_factory=datetime.utcnow, nullable=False) comments: List["Comment"] = Relationship(back_populates="post") page_views: List["PageView"] = Relationship(back_populates="post") # Tag との多対多リレーションシップを追加 tags: List["Tag"] = Relationship(back_populates="posts", link_model=PostTagLink)
コードの説明:
- 一意の
name
フィールドを持つTag
モデルを作成しました。 post_id
とtag_id
の2つのフィールドのみを含むPostTagLink
モデルを作成しました。これらの2つのフィールドは、それぞれpost
テーブルとtag
テーブルを参照する外部キーとして機能し、 together primary key を形成します。Post
モデルとTag
モデルの両方に、Relationship
フィールド(tags
とposts
)を追加しました。鍵となるのはlink_model=PostTagLink
パラメーターで、SQLModelにこれらの2つのモデル間のリレーションシップがPostTagLink
中間テーブルを通じて維持されることを伝えています。
2. データベーステーブルを作成する
2つの新しいモデルを追加したため、データベースに対応するテーブルを作成する必要があります。PostgreSQLデータベースで以下のSQLステートメントを実行します。
-- タブテーブルを作成 CREATE TABLE "tag" ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "name" VARCHAR UNIQUE NOT NULL ); -- リンクテーブル post_tag_link を作成 CREATE TABLE "posttaglink" ( "post_id" UUID REFERENCES "post"("id") ON DELETE CASCADE, "tag_id" UUID REFERENCES "tag"("id") ON DELETE CASCADE, PRIMARY KEY ("post_id", "tag_id") );
注意: SQLModelはPostTagLink
をposttaglink
テーブルに自動的にマッピングします。テーブル名が正しいことを確認してください。
Leapcellでデータベースが作成された場合、
GUIを使用してSQLステートメントを簡単に実行できます。Webサイトのデータベース管理ページにアクセスし、上記のステートメントをSQLインターフェイスに貼り付けて実行するだけです。
ステップ2:タグのビジネスロジックを実装する
コードをクリーンに保つため、タグ機能用の新しいサービスファイルを作成します。
プロジェクトのルートディレクトリに新しいファイルtags_service.py
を作成します。
# tags_service.py import uuid from typing import List from sqlmodel import Session, select from models import Tag def find_or_create_tags(tag_names: List[str], session: Session) -> List[Tag]: """ タグ名のリストに基づいて、タグエンティティを検索または作成します。 """ tags = [] if not tag_names: return tags # 既存のタグをクエリ statement = select(Tag).where(Tag.name.in_(tag_names)) existing_tags = session.exec(statement).all() tags.extend(existing_tags) existing_tag_names = {tag.name for tag in existing_tags} # 作成が必要なタグを特定 new_tag_names = [name for name in tag_names if name not in existing_tag_names] # 新しいタグを作成 for name in new_tag_names: new_tag = Tag(name=name) session.add(new_tag) tags.append(new_tag) # 効率のために一度コミット session.commit() # 新しく作成されたタグをリフレッシュしてIDを取得 for tag in tags: if tag.id is None: session.refresh(tag) return tags
このfind_or_create_tags
関数は、タグ名のリストを受け取り、データベースをクエリし、既存のタグのエンティティを返し、存在しないタグの新しいレコードを作成します。
ステップ3:投稿ルートにタグロジックを統合する
次に、投稿作成ルートを変更して、タグデータを受け入れて処理できるようにする必要があります。
routers/posts.py
を開き、新しいサービスをインポートし、create_post
ルートを更新します。
# routers/posts.py # ... 他のインポート import tags_service # タグサービスをインポート # ... @router.post("/posts", response_class=HTMLResponse) def create_post( title: str = Form(...), content: str = Form(...), tags: str = Form(""), # 新しいtagsフォームフィールドを追加 session: Session = Depends(get_session), user: dict = Depends(login_required) ): # 1. Postオブジェクトを作成 new_post = Post(title=title, content=content) # 2. タグを処理 if tags: # カンマ区切りの文字列をタグ名のリストに解析 tag_names = [name.strip() for name in tags.split(',') if name.strip()] # タグエンティティを検索または作成 tag_objects = tags_service.find_or_create_tags(tag_names, session) # タグを投稿に関連付け new_post.tags = tag_objects # 3. 投稿を保存 session.add(new_post) session.commit() return RedirectResponse(url="/posts", status_code=302) # ...
create_post
関数にtags
パラメーターを追加しました。これはフォームからカンマ区切りのタグ文字列を受け取ります。この文字列を解析し、tags_service
を呼び出してTag
オブジェクトのリストを取得し、new_post.tags
に割り当てます。SQLModelのRelationship
のおかげで、new_post
をコミットするとき、SQLModelはposttaglink
リンクテーブルのレコードを自動的に処理します。
ステップ4:フロントエンド表示
最後のステップは、ユーザーがタグを入力し、投稿ページでタグを表示できるようにテンプレートファイルを変更することです。
新規投稿ページ
templates/new-post.html
を開き、コンテンツテキストエリアの下にタグ入力フィールドを追加します。
{% include "_header.html" %} <form action="/posts" method="POST" class="post-form"> <div class="form-group"> <label for="content">Content</label> <textarea id="content" name="content" rows="10" required></textarea> </div> <div class="form-group"> <label for="tags">Tags (comma-separated)</label> <input type="text" id="tags" name="tags" placeholder="e.g. fastapi, python, tutorial" /> </div> <button type="submit">Submit</button> </form> {% include "_footer.html" %}
投稿詳細ページ
templates/post.html
を開き、投稿コンテンツのすぐ下にタグを表示するセクションを追加します。
<article class="post-detail"> <h1>{{ post.title }}</h1> <small>{{ post.createdAt.strftime('%Y-%m-%d') }} | Views: {{ view_count }}</small> <div class="post-content">{{ post.content | safe }}</div> {% if post.tags %} <div class="tags-section"> <strong>Tags:</strong> {% for tag in post.tags %} <a href="/tags/{{ tag.id }}" class="tag-item">{{ tag.name }}</a> {% endfor %} </div> {% endif %} </article>
投稿にタグがあるかどうかを確認するために{% if post.tags %}
を使用します。タグがある場合、post.tags
リストを反復処理し、各タグをリンクとしてレンダリングします。このリンクはまだクリックできません。次の記事で実装します。
実行とテスト
アプリケーションを再起動します。
uvicorn main:app --reload
ログイン後、「新規投稿」ページにアクセスします。新しいタグ入力フィールドが表示されます。
カンマ区切りでいくつかのタグを入力します。例:「Python, Tutorial」、次に送信します。
送信後、投稿の詳細ページにアクセスすると、投稿のタグが正常に表示されていることがわかります。
これで、ブログはタグの作成と表示をサポートするようになりました。ただし、ユーザーはまだタグで記事をフィルタリングできません。この機能は次のチュートリアルで実装します。