以下では、前回に作成したToDoリストアプリをデータベースを利用するよう改変したものを見ていこう。実際にはWebページの構成を少し変更して、次のような画面を持たせるようにした。
チェックボックスのオン/オフで、その項目が完了したかどうかを更新するようになっている。これに従って、項目を登録するフォームと、チェックボックスのオン/オフでPOSTを実行(して、完了している項目の状態を更新し、その結果を再描画)するフォームの2つのフォームを持たせてある。
前回に作成したToDoItemクラスとToDoListクラスのうち、データベースに保存するのはToDoItemクラスだけであり、そのコードは実は既に見た通りだ。ToDoListクラスは、データベースに対するToDoItemクラスのインスタンスの追加や削除、全項目の取得などを行うインタフェースとなっている。コードは次の通りだ。
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def init_db(app):
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///db/todoitems.db"
db.init_app(app)
class ToDoItem(db.Model):
__tablename__ = "todoitems"
item_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False, default=False)
done = db.Column(db.Boolean, nullable=False)
class ToDoList:
def add(self, title):
item = ToDoItem(title=title, done=False)
db.session.add(item)
db.session.commit()
def delete(self, item_id):
item = ToDoItem.query.filter_by(item_id=item_id).first()
db.session.delete(item)
db.session.commit()
def get_all(self):
items = ToDoItem.query.all()
return items
def delete_doneitem(self):
ToDoItem.query.filter_by(done=True).delete()
db.session.commit()
def update_done(self, items):
for item in self.get_all():
if item.item_id in items:
item.done = True
else:
item.done = False
db.session.commit()
ToDoListクラスでは、前回に見たコードを、前ページで紹介したメソッドを使ってデータベースを操作するように素直に書き下しただけとなっている(ただし、Webページ上ではチェックボックスを使って各項目が完了したかどうかを示すようにしたために、コードが変更されている。これについては詳しくは取り上げない。後で簡単に説明しよう)。
重要なのは冒頭の部分だ。前ページでは、「db = SQLAlchemy(app)」のようにして、SQLAlchemyクラスのコンストラクタにappオブジェクトを渡していたが、今回はapp.pyファイルとtodo.pyファイルでアプリが構成されている。そのため、app.pyファイル(appモジュール)の側でFlaskアプリを構築してから、その情報をtodo.pyファイルに渡すような初期化処理を行っている。
# app.py
from flask import Flask, render_template, redirect, request
from todo import ToDoList, init_db
app = Flask(__name__)
init_db(app)
# …… 省略 ……
# todo.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def init_db(app):
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///db/todoitems.db"
db.init_app(app)
# …… 省略 ……
このような場合には、(仮想環境を有効にした)ターミナルを開いて、pythonコマンドでREPL環境を起動し、「from py import db」「db.create_all()」行を実行しても思った通りの動作とはならない。簡単には「ここでしている方法で初期化を行うにはアプリのコンテキストが必要になる」ということだ。そして、前ページで見た「flask shell」コマンドは「アプリのコンテキストで動作するREPL環境を起動」してくれる。app.pyファイルのコードを見た後で、実際にこれを行って、データベース(とテーブル)を作成しよう。なお、ここではデータベースファイルをtodoitems.dbとしてある。
app.pyファイルのコードは前回とほぼ同様だ(todo.pyファイルと同様、WebページのUIを変更したことによる変更点はある)。
from flask import Flask, render_template, redirect, request
from todo import ToDoList, init_db
app = Flask(__name__)
db = init_db(app)
todolist = ToDoList()
@app.route("/")
def show_todolist():
return render_template("showtodo.html", todolist=todolist.get_all())
@app.route("/additem", methods=["POST"])
def add_item():
title = request.form["title"]
if not title:
return redirect("/")
todolist.add(title)
return redirect("/")
@app.route("/deleteitem/<int:item_id>")
def delete_todoitem(item_id):
todolist.delete(item_id)
return redirect("/")
@app.route("/deletealldoneitems")
def delete_alldoneitems():
todolist.delete_doneitem()
return redirect("/")
@app.route("/updatedone", methods=["POST"])
def update_done():
keys = request.form.keys()
items = [int(x) for x in keys]
todolist.update_done(items)
return redirect("/")
このページで冒頭でも述べたが、チェックボックスのオン/オフによるPOSTリクエストにより、上のリストの末尾にあるupdate_done関数が呼び出される。このとき、チェックボックスがオン(完了を意味する)になっているもののitem_id値をrequest.form.keysで取得できるように、テンプレートには記述してある(チェックボックスのname属性)。そこで、これを取得して、整数値のリストに変換してから、ToDoListクラスのインスタンスのupdate_doneメソッドを渡すようにしている。update_doneメソッドではデータの全項目に対して、その情報を基に各項目のdoneメンバを更新している(雑な処理をしているので、項目数が多くなるとひどいことになると予想される)。
ここまで見たところで、データベース(とテーブル)を作成しておこう。といっても、既に紹介したように「環境変数FLASK_APPを設定して、flask shellコマンドを実行したら、dbを(ただし今度はtodoモジュールから)インポートし、db.create_allメソッドを呼び出す」だけだ(「だけ」という割に長かった)。
ここで気にしてほしいのが、REPL環境での「db」の評価結果だ(赤枠内)。engineにURIで指定したファイルが設定されているのが分かる。ちなみに、単にpythonコマンドでREPL環境を起動して同様な処理を行った場合は次のようになる。
これはtodo.pyファイルにあるinit_db関数での初期化後にdbオブジェクトを返送するようにして、それをapp.py側で受け取るように修正しても変わらない。
# app.py
from flask import Flask, render_template, redirect, request
from todo import ToDoList, init_db
app = Flask(__name__)
db = init_db(app)
# …… 省略 ……
# todo.pyのinit_db関数
def init_db(app):
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///db/todoitems.db"
db.init_app(app)
return db
というわけで、データベースを作成するときには「flask shell」コマンドを使うようにしよう。
Webページの描画に使用するテンプレートは次の通りだ。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>To Do List sample app</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}" />
</head>
<body>
<h1>ToDo List</h1>
<form action="/additem" method="post" name="registerform">
<p>
something to do:
<input type="text" name="title">
<a href="javascript:document.registerform.submit()" class="button">submit</a>
</p>
</form>
{% if todolist %}
<p>here your todos</p>
<form action="/updatedone" method="post" name="updatedoneform">
<ul style="list-style-type: none; padding-left: 10px">
{% for item in todolist %}
<li {% if item.done %} style="color: lightgray" {% endif %}>
<input type="checkbox" {% if item.done %} checked="checked" {% endif %}
name="{{ item.item_id }}" value="{{ item.title }}"
onchange="javascript:document.updatedoneform.submit()">
{{ item.title }}
<a href="/deleteitem/{{ item.item_id }}">[delete]</a>
</li>
{% endfor %}
</ul>
<a href="/deletealldoneitems" class="button">delete all done items</a>
</form>
{% else %}
<p>you can register something to do.</p>
{% endif %}
</body>
</html>
2つのフォームがあり、1つは項目の登録用で(registerform)、もう1つは完了状態を表すチェックボックスに変更があるたびに送信される(updatedoneform)。使っているJinja2テンプレートの構文自体は前回と同様なので、詳しくは説明する必要はないだろう。
スタイルシートは前回と同様なので割愛する。
この状態でアプリを実行すると、次のようになる(デバッグ実行の手順については「VS CodeとFlaskによるWebアプリ開発『最初の一歩』」を参照のこと)。
今回はFlask-SQLAlchemyを利用して、ToDoリストアプリにデータベースを組み込んでみた。次回は、これをWeb API化してみることにしよう。
Copyright© Digital Advantage Corp. All Rights Reserved.