
【第2回】Responderを使ってDjangoチュートリアルをやってみた【データベース・モデル構築編】
2021.12.20
第2回~Responderを使ってDjangoチュートリアルをやってみた~
今回も、Responder(レスポンダー)を使って「Djangoのチュートリアル」をやってみたいと思います。
Responderで追う形になりますので、多少内容が異なる部分がありますが、成果物はできるだけ同じモノになるよう作る予定です。
今回は、Django のチュートリアル「はじめての Django アプリ作成、その2」をご参考下さい。
【はじめての Django アプリ作成、その2】
https://docs.djangoproject.com/ja/2.2/intro/tutorial02/
第1回はこちら
データベースの構築
Responder単体では、Djangoのように単体で簡単にデータベース作成はできません。
そこで、「SQLAlchemy(エスキューエルアルケミー)」と呼ばれる、Pythonでデータベース管理ができるライブラリの力を借りることにしました。
他にも、データベース管理のためのPythonライブラリはあるようですが、情報がたくさんあるSQLAlchemyを使うことにします。
データベースを構築
では、早速データベースを構築していきましょう!
今回は、SQLite3を使ってデータベースを構築していきます。
以下は、データベースの設定ファイル db.py です。
作成場所は、プロジェクトのルートで構いません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | """ db.py データベース設定用 """ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker Base = declarative_base() RDB_PATH = 'sqlite:///db.sqlite3' ECHO_LOG = True engine = create_engine( RDB_PATH, echo=ECHO_LOG ) Session = sessionmaker(bind=engine) session = Session() |
現段階では、SQLAlchemyを触ったことのない人にとっては何が書いてあるか分からないかもしれません。
でも、今の段階では深く理解する必要はありません。
「ああ、SQLite3っていうデータベースを使うんだな」くらいに思っていただければOKです。
モデルの定義
今回、必要なテーブル(モデル)は、以下の通りとなります。
Question テーブル | id(主キー)、question_text(質問事項)、pub_date(公開日時) |
Choice テーブル | id(主キー)、question(紐づけられた質問)、choice_text(選択肢テキスト)、votes(投票数) |
User テーブル | id(主キー)、username(ユーザ名)、password(ハッシュ化されたパスワード) |
これらは、SQLAlchemyにおいてクラスで定義可能です。
models.py
これらのモデル定義をするために、 models.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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | """ models.py モデルの定義 """ import os from datetime import datetime from db import Base from db import engine from sqlalchemy import Column, String, DateTime, ForeignKey from sqlalchemy.sql.functions import current_timestamp from sqlalchemy.dialects.mysql import INTEGER import hashlib SQLITE3_NAME = "./db.sqlite3" class Question(Base): """ Questionテーブル id : 主キー question_text : 質問事項 pub_date : 公開日 """ __tablename__ = 'question' id = Column( 'id', INTEGER(unsigned=True), primary_key=True, autoincrement=True ) question_text = Column('question_text', String(256)) pub_date = Column( 'pub_date', DateTime, default=datetime.now(), nullable=False, server_default=current_timestamp() ) def __init__(self, question_text, pub_date=datetime.now()): self.question_text = question_text self.pub_date = pub_date def __str__(self): return str(self.id) + ':' + self.question_text + ' - ' + self.pub_date.strftime('%Y/%m/%d - %H:%M:%S') class Choice(Base): """ Choiceテーブル id : 主キー question : 紐づけられた質問 (外部キー) choice_text : 選択肢のテキスト votes : 投票数 """ __tablename__ = 'choice' id = Column( 'id', INTEGER(unsigned=True), primary_key=True, autoincrement=True ) question = Column('question', ForeignKey('question.id')) choice_text = Column('choice_text', String(256)) votes = Column('votes', INTEGER(unsigned=True), nullable=False) def __init__(self, question, choice_text, vote=0): self.question = question self.choice_text = choice_text self.votes = vote def __str__(self): return str(self.id) + ':' + self.choice_text + ' - ' + self.votes class User(Base): """ Userテーブル id : 主キー username : ユーザネーム password : パスワード """ __tablename__ = 'user' id = Column( 'id', INTEGER(unsigned=True), primary_key=True, autoincrement=True ) username = Column('username', ForeignKey('question.id')) password = Column('password', String(256)) def __init__(self, username, password): self.username = username # パスワードはハッシュ化して保存 self.password = hashlib.md5(password.encode()).hexdigest() def __str__(self): return str(self.id) + ':' + self.username if __name__ == "__main__": path = SQLITE3_NAME if not os.path.isfile(path): # テーブルを作成する Base.metadata.create_all(engine) |
コードを見てわかるように、各クラスのメンバ変数が各テーブルのレコードの要素となります。
このとき、各モデルのインスタンスを print() できるように def __str__() も実装しておきましょう!
実行してみよう!
さて、実際に models.py を実行してみてください。
db.sqlite3 というファイルが生成されたかと思います。
では、実際にモデルの定義ができたか確認してみましょう!
1 | hoge@user:responder$ sqlite3 db.sqlite3 |
で作成したデータベースにアクセスします。
1 2 | sqlite> .table choice question user |
しっかりテーブルが作成できました!
サンプルのデータを挿入してみる
それでは、適当なデータを挿入してみましょう。
このとき、Userテーブルには管理者ユーザ(admin)を追加することにします。
早速、 sample_insert.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 | """ sample_insert.py サンプルデータをデータベースに格納してみる """ import models import db if __name__ == '__main__': # サンプル質問 question = models.Question('What\'s up?') db.session.add(question) db.session.commit() # サンプル選択肢 choices = list() choice_1 = models.Choice(question.id, 'Fine.') choice_2 = models.Choice(question.id, 'Not bad') choice_3 = models.Choice(question.id, 'Bad...') choices.append(choice_1) choices.append(choice_2) choices.append(choice_3) db.session.add_all(choices) db.session.commit() # adminユーザを作成 admin = models.User('admin', 'responder') db.session.add(admin) db.session.commit() db.session.close() |
SQLAlchemyでは、このようにデータをデータベースに追加が可能です。
データベースをいじる際には、 import models と import db を忘れないようにしましょう。
simple_insert.py を実行してみる!
では、 simple_insert.py を実行してみましょう!
すると、 models.py で定義された各モデルのコンストラクタ __init__()に、引数にとったデータが渡され、各テーブルに db.session.commit() で挿入されます。
確認
うまく挿入できたか確認してみましょう!
1 2 3 4 5 6 7 8 9 10 | sqlite> select * from user; 1|admin|6295474448cef4d8b762397447b77beb sqlite> select * from question; 1|What's up?|2019.08.02 - 12:17:36 sqlite> select * from choice; 1|1|Fine.|0 2|1|Not bad|0 3|1|Bad...|0 |
うまく挿入できていそうです!
adminサイトに入る前に
それでは、adminページに入りたいところですが、Djangoと違ってデフォルトで用意されていないので、管理者ページを作ります。
ということで、今から作成するのは、この3つになります。
- 管理者(admin)のログイン機能
- 管理者ページ(administrator.html)
- ログアウト機能
少々ややこしい部分も出てきますが頑張っていきましょう!
管理者(admin)のログイン機能
それでは、前回作成した admin.html からpostメソッドで受け取る /ad_login を作っていきます。
ルーティング作業ですので、 urls.py に加筆していきます。
ログイン機能
ログイン機能には、様々な機能が必要です。
- usernameとpasswordがデータベースに存在するか
- usernameとpasswordが一致するか
- そもそも入力フォームにそれぞれが入力されているか
- 以上3点が守られていない場合エラーメッセージを返す
- OKならば入力されたusernameでセッションを開始する
と、最低限必要な機能を挙げるだけでも、これだけあります。
また、getで /ad_login にアクセスされた場合に、余計な処理をせずにログインページへリダイレクトさせる必要もあります。
これらを実装していく前に、「1.usernameとpasswordがデータベースに存在するか」と「2.usernameとpasswordが一致するか」は、一つの関数として最初に定義してしまいましょう。
新しく認証用の関数を定義する auth.py を作成し、認証ができたか否かを返す関数 is_auth() を実装します。
また、「今現在ログイン状態にあるかないかを判定し、ログインしていなければログイン画面へリダイレクトさせる」機能をもつ authorized() も一緒に実装してしまおうかと思います。
auth.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 | """ auth.py 認証系の関数群 """ from models import User import db def is_auth(username, password): """ Userテーブルに存在するか否かを返す """ users = db.session.query(User.username, User.password) db.session.close() for user in users: if user.username == username and user.password == password: return True return False def authorized(req, resp, api): """ cookieにusernameが存在しない場合ログインページにリダイレクトする (ログインしていない場合) """ if 'username' not in req.cookies: api.redirect(resp, '/admin') |
is_auth() では、まずデータベースのUserテーブルから、「username」と「password」をセットでリストとして持ってきます。
users = db.session.query(User.username, User.password)
そのリスト内に、引数にとったusernameとpasswordに対して一致するものがあればTrueを返し、なければFalseを返すシンプルな関数です。
authorized() もシンプルで、cookieに「username」キーがなければリダイレクト、としているだけです。
urls.py
では、また urls.py に戻ります。
先ほど挙げた機能を urls.py に実装してみたいと思います。
新しく加筆した部分には # New! をつけていますので参考にしてください。
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 | """ urls.py ルーティング用ファイル """ import responder from auth import is_auth, authorized # New! import hashlib # New! from models import User, Question, Choice # New! import db # New! api = responder.API() # -- 省略-- # @api.route('/ad_login') # New! class AdLogin: async def on_get(self, req, resp): # getならリダイレクト resp.content = api.template('admin.html') async def on_post(self, req, resp): data = await req.media() error_messages = [] if data.get('username') is None or data.get('password') is None: error_messages.append('ユーザ名またはパスワードが入力されていません。') resp.content = api.template('admin.html', error_messages=error_messages) return username = data.get('username') password = hashlib.md5(data.get('password').encode()).hexdigest() if not is_auth(username, password): # 認証失敗 error_messages.append('ユーザ名かパスワードに誤りがあります。') resp.content = api.template('admin.html', error_messages=error_messages) return # 認証成功した場合sessionにユーザを追加しリダイレクト resp.set_cookie(key='username', value=username, expires=None, max_age=None) api.redirect(resp, '/admin_top') |
加筆したコード量は多いですが、一つ一つ丁寧にみていけば、どのような処理を行っているかはなんとなく理解できると思います。
Responderでは、今回実装した @api.route('/ad_login') のように処理をクラスでも記述することができます class AdLogin 。
- async def on_get(self, req, resp) でget処理
- async def on_post(self, req, resp) でpost処理
そうすると、 async def on_get(self, req, resp) でget処理、 async def on_post(self, req, resp) でpost処理を書き分けることができますので、覚えておきましょう!
ログイン/ログアウト
また、Responderのログイン/ログアウトについては、 resp.set_cookie(key='名前', value='実際の値', expires='いつ破棄されるか', max_age='何秒後に破棄されるか')を使用します。
- ログインは、 expire=None max_age=None
- ログアウトは、 expire=0 max_age=0
ログインでは、 expire=None max_age=None としておき、ログアウトでは expire=0 max_age=0 とすれば機能が実現できます。
他にも、 resp.set_cookie()には引数は存在しますが、今回は簡略化のためスルーします。
また、まだ /admin_top のルーティングは終わってないので、このログイン機能は完全には機能しません。
管理者ページ(administrator.html)
次に、先ほどの /admin_top のルーティングと、そのビュー administrator.html を作成していきます。
管理者ページでは、データベースの中身を視覚的に確認できるようにします。
最終的には、このページからデータベースへの追加や削除もできるようにしますが、とりあえずは表示のみを目指します。
/admin_top のルーティング
早速、 urls.py に /admin_top のルーティングを行なっていきます。
また、その際にQuestionテーブルとChoiceテーブルのデータを取得しておき、 administrator.html に渡す作業も実装していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @api.route('/admin_top') async def on_session(req, resp): """ 管理者ページ """ authorized(req, resp, api) # ログインユーザ名を取得 auth_user = req.cookies.get('username') # データベースから質問一覧と選択肢を全て取得 questions = db.session.query(Question).all() choices = db.session.query(Choice).all() db.session.close() # 各データを管理者ページに渡す resp.content = api.template('administrator.html', auth_user=auth_user, questions=questions, choices=choices) |
やっていることは先の is_auth() と少し似ていますね。
関数の冒頭で、cookieにユーザが存在しない(=ログインしていない)ときに、ログインページへリダイレクト authorized() させています。
こうすることで、 /admin_top に直接アクセスできなくさせています。
ビューを作る
それでは、ビューを作っていきましょう。
ビューは、前回のように作っていけば良いので説明は省略しますが、受け取ったデータの展開の仕方だけコードを見て理解しておきましょう。
今更ですが、ビューでの動的ページは、Jinja2と呼ばれるエンジンに従ったコーディングを行なっています。
{% %} の中に構文、 {{ }} の中には出力させたい変数を入れます。
詳細は、コードを見ながら覚えられると思います。
ビューの実装
ビューのコードは以下のように実装しました。
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 66 67 68 69 70 71 72 73 | {% extends "layout.html" %} {% block content %} <br> <div class="row"> <div class="col-md-10"> <h1>Polls Application Administrator</h1> </div> <div class="col-md-2"> <form action="/logout" method="post"> <input type="hidden" name="token" value="{{auth_user}}"> <button type="submit" class="btn btn-primary">Logout</button> </form> </div> </div> <hr> <p>Hi, {{auth_user}}.</p> <br> <br> <h4>Question <a href="/add_Question" class="btn-warning btn-sm">Add</a></h4> <table class="table"> <thead class="thead-dark"> <tr> <th>id</th> <th scope="col">question_text</th> <th scope="col">pub_date</th> <th scope="col"></th> </tr> </thead> <tbody> {% for question in questions%} <tr> <th scope="row">{{question['id']}}</th> <td>{{question['question_text']}}</td> <td>{{question['pub_date']}}</td> <td> <a href="/change/question/{{question['id']}}" class="btn-secondary btn-sm">Change</a> <a href="/delete/question/{{question['id']}}" class="btn-danger btn-sm">Delete</a> </td> </tr> {% endfor %} </tbody> </table> <br> <h4>Choice <a href="/add_Choice" class="btn-warning btn-sm">Add</a></h4> <table class="table"> <thead class="thead-dark"> <tr> <th>id</th> <th scope="col">question</th> <th scope="col">choice_text</th> <th scope="col">votes</th> <th scope="col"></th> </tr> </thead> <tbody> {% for choice in choices%} <tr> <th scope="row">{{choice['id']}}</th> <td>{{choice['question']}}</td> <td>{{choice['choice_text']}}</td> <td>{{choice['votes']}}</td> <td> <a href="/change/choice/{{choice['id']}}" class="btn-secondary btn-sm">Change</a> <a href="/delete/choice/{{choice['id']}}" class="btn-danger btn-sm">Delete</a> </td> </tr> {% endfor %} </tbody> </table> {% endblock %} |
テーブルでデータベースの情報を表示する形をとりました。
「Add」ボタン、「Change」ボタン、「Delete」ボタンは、まだ実装していないので機能しません。
右上に表示される「ログアウトボタン」については、今から実装していきたいと思います。
ログアウト機能
さて、長くなりましたが、やっとで本記事最後のコーディングになります。
ログアウト機能は、とてもシンプルです。
セッションから抜けて、ログイン画面にリダイレクトさせるだけなので、以下のように urls.py に加筆します。
ログアウトは、先ほど言ったようにcookieを破棄するまでの時間を0とすればOKです。
1 2 3 4 5 | @api.route('/logout') # New! async def logout(req, resp): # クッキーを削除 resp.set_cookie(key='username', value='', expires=0, max_age=0) api.redirect(resp, '/admin') |
ちなみに、cookieの中身を見たい場合は print(req.cookies) で見ることができます。
( resp ではなく req です!)
もし、うまくいかない場合は、デバッグ用で活用してくださいね。
adminサイトに入る
ここにきて、やっとでチュートリアルに戻ってきました!
まずは、サーバを起動して、http://127.0.0.1:5042/adminにアクセスしてみます。
ログイン成功
次に、ログイン画面を確認してみたいと思います。
それぞれに「admin」「responder」と入力して、
ログインしてみましょう!
うまくデータベースの中身が表示できました!
ログイン失敗
それでは、わざとユーザ名やパスワードを間違えて、エラーメッセージが出るかも確認してみます。
【error1】
【error2】
問題なさそうですね!
あとは、管理者ページからデータベースの操作ができれば管理者ページは完成になります。
こちらは、次回に続きます!
第3回へつづく!
今回は、「データベースの構築」と「モデルの定義」と、ボリュームのある回となりました。
ですが、ここを乗り越えると、Webアプリケーション初心者でも、なんとなく勝手が分かってくるのではないでしょうか?
次回も、「データベースとの連携」を含んだ話が続きますが、あともう少し頑張っていきましょう!
第3回の記事はこちら
【全編まとめ】Responderを使ってDjangoチュートリアルをやってみた
こちらの記事もオススメ!
urls.py
最後に、前回から更新(加筆)されたurls.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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | """ urls.py ルーティング用ファイル """ import responder from auth import is_auth, authorized # New! import hashlib # New! from models import User, Question, Choice # New! import db # New! api = responder.API() @api.route('/') def index(req, resp): resp.content = api.template("index.html") @api.route('/admin') def admin(req, resp): resp.content = api.template("admin.html") @api.route('/ad_login') # New! class AdLogin: async def on_get(self, req, resp): # getならリダイレクト resp.content = api.template('admin.html') async def on_post(self, req, resp): data = await req.media() error_messages = [] if data.get('username') is None or data.get('password') is None: error_messages.append('ユーザ名またはパスワードが入力されていません。') resp.content = api.template('admin.html', error_messages=error_messages) return username = data.get('username') password = hashlib.md5(data.get('password').encode()).hexdigest() if username != 'admin': # 少々強引ですがadmin以外はログインできないようにする。 error_messages.append('管理者ユーザではありません。') resp.content = api.template('admin.html', error_messages=error_messages) return if not is_auth(username, password): # 認証失敗 error_messages.append('ユーザ名かパスワードに誤りがあります。') resp.content = api.template('admin.html', error_messages=error_messages) return # 認証成功した場合cookieにユーザを追加しリダイレクト resp.set_cookie(key='username', value=username, expires=None, max_age=None) api.redirect(resp, '/admin_top') @api.route('/admin_top') # New! async def on_session(req, resp): """ 管理者ページ """ authorized() # ログインユーザ名を取得 auth_user = req.cookies.get('username') # データベースから質問一覧と選択肢を全て取得 questions = db.session.query(Question.id, Question.question_text, Question.pub_date) choices = db.session.query(Choice.id, Choice.question, Choice.choice_text, Choice.votes) db.session.close() # 各データを管理者ページに渡す resp.content = api.template('administrator.html', auth_user=auth_user, questions=questions, choices=choices) @api.route('/logout') # New! async def logout(req, resp): # クッキーを削除 resp.set_cookie(key='username', value='', expires=0, max_age=0) api.redirect(resp, '/admin') |
ディレクトリ構成
あとは、現在のディレクトリ構成も載せておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | .responder/ ├── __pycache__ # なくてもよい ├── auth.py ├── db.py ├── db.sqlite3 ├── models.py ├── run.py ├── sample_insert.py ├── static ├── templates │ ├── admin.html │ ├── administrator.html │ ├── index.html │ └── layout.html └── urls.py |
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の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が出来るまでとソフトウェアの未来