
【第4回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる【モデルの構築】
2021.12.20
第4回~モダンなフレームワークの使い方を学びながらブログシステムを構築~

連載「Python Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる」第4回目です。
前回は、「管理者ページの整備」まで行いました。
今回は、「ブログシステムのデータベース」を解説していきます。
ブログシステムのデータベースを作ろう!
今回は、いよいよ Firestore を使って、「ブログシステムのデータベース」を作ります。
以下のポイントを押さえながら解説していきます。
- Firestore と Python の連携はどうやるの?
- ブログシステムには何が必要?
それでは、さっそく見ていきましょう!
モデルの作成
まずはモデルを扱う、 models.py を作成しましょう。
ここで、「記事」や「カテゴリ」といったモデルを作成し、「Firestore に追加していく機能」を実装していきます。
実際には「コントローラ側で、モデルをもとにデータを作成する」形になります。
まずは、試しに作ってみましょう。
Category
まずは、「カテゴリのモデル」作成から。
カテゴリは、以下の要素で構成されます。
- 主キーとなる ID
- 名前
- スラッグ
Firestore と Python の連携はどうやる?
Firestore と Python の連携は、一般的に「構築したクラスを Dict に変換して追加」という方法で行います。
したがって、この様な実装をしてみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | # models.py from datetime import datetime from init_db import db class Category(object): def __init__(self, name: str, slug: str): """ Create Category :param name: category name :param slug: category slug """ # slugの被りがあるか調査 try: if slug in get_category_slugs(): raise ValueError('Error: The slug you gave has already been existed.') except ValueError as e: print(e) exit(-1) self.name = name self.slug = slug def to_dict(self): data = self.__dict__ return data def add(self): """ Add data to Firestore """ db.collection('categories').add(self.to_dict()) |
ID の設定
ID は、動的に被りがない様にしましょう。
Cloud Firestore は、データを追加する際、「指定しなければ、ランダムに ID を割り当ててくれる」ので、これを使います。
スラッグの設定
また、ID 同様、スラッグも被りがない様にします。
以下のように、「Firestore 内データのスラッグ一覧を取得する関数」を用意しておきます。
1 2 3 4 5 6 7 8 9 10 | # models.py def get_category_slugs(): return [cat.to_dict()['slug'] for cat in db.collection('categories').stream()] def get_tag_slugs(): return [cat.to_dict()['slug'] for cat in db.collection('tags').stream()] def get_article_slugs(): return [cat.to_dict()['slug'] for cat in db.collection('articles').stream()] |
コレクションから stream() で得たデータは、そのままでは使い勝手が悪いです。
そのため、「辞書型変数」に直してから、スラッグだけのリストを作りましょう!
Tag
タグも同様です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | # models.py class Tag(object): def __init__(self, name: str, slug: str): """ Create Tag :param name: :param slug: """ # slugの被りがあるか調査 try: if slug in get_category_slugs(): raise ValueError('Error: The slug you gave has already been existed.') except ValueError as e: print(e) exit(-1) self.name = name self.slug = slug def to_dict(self): data = self.__dict__ return data def add(self): db.collection('tags').add(self.to_dict()) |
Article
記事は、以下のフィールドを持たせます。
id | 一位に定まるキー |
title | 記事タイトル |
thumbnail | サムネイル画像 |
contents | 記事内容 |
description | 詳細・記事抜粋 |
author | 著者 |
slug | スラッグ (これも一意に定まるもの) |
category | カテゴリ (1つ) |
tags | タグ (複数個可) |
released | 公開状態 |
今回、「カテゴリは1つ」「タグは複数個」持てるようにします。
その他は、先ほどの2つと大きな違いはありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | # models.py class Article(object): def __init__(self, title: str, thumbnail: str, contents: str, description: str, author: str, slug: str, category: str, tags: list, released: bool = False ): """ Create article :param title: タイトル :param thumbnail: サムネイル :param contents: 記事内容 :param description: 詳細・抜粋 :param author: 著者 :param slug: スラッグ :param category: カテゴリ :param tags: タグ :param released: 公開設定 """ # slugの被りがあるか調査 try: if slug in get_article_slugs(): raise ValueError('Error: The slug you gave has already been existed.') except ValueError as e: print(e) exit(-1) self.title = title self.thumbnail = thumbnail self.contents = contents self.description = description self.author = author self.slug = slug self.category = category self.tags = tags self.last_update = datetime.now() self.released = released def to_dict(self): data = self.__dict__ return data def add(self): db.collection('articles').add(self.to_dict()) |
サンプルデータを追加してみる
それでは、動作確認がてらデータを追加してみましょう!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | from datetime import datetime from init_db import db class Category(object): # ~~~省略~~~ # class Tag(object): # ~~~省略~~~ # class Article(object): # ~~~省略~~~ # if __name__ == '__main__': # テストコード cat1 = Category('お知らせ', 'news') cat2 = Category('技術', 'technology') cat1.add() cat2.add() tag1 = Tag('tag1', 'tag1') tag2 = Tag('tag2', 'tag2') tag1.add() tag2.add() art = Article( title='ブログを開設しました!', thumbnail='', contents='## マークダウン形式のコンテンツ', description='ブログを開設しました。ここは記事の詳細で、抜粋としても使われます。', author='rightcode', slug='create-blog', category='news', tags=['tag1', 'tag2'], ) art.add() |
マークダウン形式で書いておこう
執筆はマークダウン形式にしたいので、ここでも、マークダウン形式で記事内容を書いておきます。
今は、とりあえず適当なデータを格納しておきましょう。
実行!
実行してみると、以下の様にデータが追加されました!

データが追加できた
管理者ページに反映させる
これで、データベースに記事ができました。
次は、「管理者ページ」もとい「投稿一覧ページ」に、反映させましょう。
関数を作成
まずは、 models.py に、記事を List[Dict] で取得できる関数を作っておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | # models.py from google.cloud.firestore import Query def get_articles(unreleased: bool = True): """ Get articles :param unreleased: True => all articles False => released articles :return: """ if unreleased: articles = [art.to_dict() for art in db.collection('articles').order_by( 'last_update', direction=Query.DESCENDING ).stream() ] else: # 実は以下のコードはダメ : 理由は連載後半で解説 articles = [art.to_dict() for art in db.collection('articles').where('released', '==', True).order_by( 'last_update', direction=Query.DESCENDING ).stream() ] return articles |
諸々の仕様を設定
あとで使い回せるよう、「公開済みか否か」によって、取得する記事を変更する様にしました。
また、 order_by('last_update', direction=Query.DESCENDING) で日付降順で取り出すようにしています。
ビューに投げる処理を追記
そうしたら、 controllers.py 側からこれを呼び出し、ビューに投げる処理を追記します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | # controllers.py from models import * # ~~~省略~~~ # @api.route('/admin') class Admin: async def on_get(self, req, resp): # ログインしてなければ if req.cookies.get('session') is None: api.redirect(resp, '/login') # ログイン済み else: articles = get_articles() # [New!] resp.html = api.template('admin.html', title='管理者ページ', name=req.cookies.get('username'), articles=articles) async def on_post(self, req, resp): # POSTデータを取得 data = await req.media() email = data['email'] password = data['password'] # 認証 res, session = _login(email, password, COOKIE_EXPIRES) if 'error' not in res: # クッキーにログイン情報をセット expires = datetime.now() + timedelta(COOKIE_EXPIRES) resp.set_cookie(key='session', value=session, expires=expires) resp.set_cookie(key='username', value=res['displayName'], expires=expires) resp.set_cookie(key='email', value=email, expires=expires) # 認証成功ならば管理者ページへ articles = get_articles() # New resp.html = api.template('admin.html', title='管理者ページ', name=res['displayName'], articles=articles) else: # 認証失敗ならばエラーメッセージをログイン画面に渡してリダイレクト api.redirect(resp, '/login?error={}'.format(res['error']['errors'][0]['message'])) |
ビューを変更
最後に、ビューも変更しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | <!-- templates/admin.html 管理者ページ --> {% extends "layout.html" %} {% block content %} <br> <h1>My Blog Name | 管理者ページ</h1> <p>こんにちは,{{ name }} さん</p> <div class="main-container"> <div class="admin-main-menu"> <h2>投稿一覧</h2> <table> <tr> <th style="width: 5%;">#</th> <th style="width: 35%;">タイトル</th> <th style="width: 10%;">作成者</th> <th style="width: 10%;">スラッグ</th> <th style="width: 10%;">カテゴリ</th> <th style="width: 10%;">タグ</th> <th style="width: 15%;">最終更新日</th> <th style="width: 5%;">公開</th> </tr> {% for art in articles %} <tr> <td>{{ loop.index-1 }}</td> <td>{{ art['title'] }}</td> <td>{{ art['author'] }}</td> <td>{{ art['slug'] }}</td> <td>{{ art['category'] }}</td> <td> {% for tag in art['tags'] %} {{ tag }}, {% endfor %} </td> <td>{{ art['last_update'].strftime('%Y.%m.%d') }}</td> <td> {% if art['released'] %} <a href="/category/{{ art['category'] }}/{{ art['slug'] }}">済</a> {% else %} 未 {% endif %} </td> </tr> {% endfor %} </table> </div> <div class="admin-side-menu"> <h2>Menu</h2> <ul> <li><a href="/admin/profile">プロフィール確認・編集</a></li> <li><a href="/admin/new">新規追加</a></li> <li><a href="/admin">投稿一覧</a></li> <li><a href="/admin/category">カテゴリ</a></li> <li><a href="/admin/tag">タグ</a></li> <li><a href="/logout">ログアウト</a></li> </ul> </div> </div> {% endblock %} |
ポイントは、以下の3つです。
- 公開済みの記事に直接飛べるようにする
- サイドメニューに「カテゴリ」と「タグ」の編集リンクを追加(後ほど実装します)
- テーブルのヘッダーに「公開」を追加
確認
実際に、管理者ページにアクセスしてみると、

管理者ページにデータが反映された
うまく動作しています!
第5回へつづく!
今回は、データベースにおける「Firestore と Python の連携部分」を実装してみました。
管理者ページもだいぶ整いましたが、「記事の追加」「編集」などの実装はまだです。
まだまだブログシステム完全構築には程遠いですね…。
次回は「記事個別ページ」を作成していきます。
道のりは長いですが、次回も頑張って実装していきましょう!
次回の記事はこちら
第1回はこちら
こちらの記事もオススメ!
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン
ITエンタメ7月 14, 2023【クリス・ワンストラス】GitHubが出来るまでとソフトウェアの未来