Blow Up by Black Swan

Python-Flask で作る Web アプリケーション(チュートリアル編2)

チュートリアル2です。

参考サイト: Flask – Tutorial(公式ドキュメント)

2. アプリケーションセットアップ

Flaskアプリケーションは、Flaskクラスのインスタンスです。設定やURLなどのアプリケーションに関わるあらゆることが、このクラスに登録されます。Flaskアプリケーションを生成する最も簡単な方法は、コードの最上層に直接、グローバルなFlaskインスタンスを作成することです。この方法はシンプルで使い勝手の良い方法ではありますが、プロジェクトの規模が大きくなるに連れて、対応しづらい問題を引き起こすことがあります。

グローバルにFlaskインスタンスを作成する代わりに、関数の内側で作ることもでき、この関数が「アプリケーションファクトリー(application factory)」と呼ばれるものです。設定や登録などのアプリケーションのセットアップに必要となる全てのことが関数の中で処理され、そして関数の戻り値としてアプリケーションが返されます。

2-1. Application Factory

ここからコーディングを開始します。まずflaskrディレクトリを作成し、そこに__init__.pyファイルを作成します(ただし、このチュートリアルでは既にファイルを作成されたものとして扱い、作成するためのターミナルコマンド等は省略しています)。__init__.pyファイルは次の2つの役割を持ちます。

  1. アプリケーションファクトリーを記述する
  2. flaskrディレクトリををパッケージとして認識させる
# flaskr/__init__.py
import os

from flask import Flask

def create_app(test_config=None):

    # インスタンス(app)の構築と設定の読み込み
    app = Flask(__name__, instance_relative_config=True)
    app.config.from_mapping(
        SECRET_KEY='dev',
        DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'),
    )
    
    #テスト用設定を用いない場合(テストでない本番環境の場合)、存在すればインスタンスの設定を読み込み
    if test_config is None:
        app.config.from_pyfile('config.py', silent=True)
        
    #テスト設定を用いる場合(つまりテストの時)
    else:
        app.config.from_mapping(test_config)

    # instanceフォルダがあるか確認
    try:
        os.makedirs(app.instance_path)
    except OSError:
        pass

    # "Hello, World!"を返すシンプルなページ
    @app.route('/hello')
    def hello():
        return 'Hello, World!'

    return app

create_appはアプリケーションファクトリー関数です。今後この関数内にコードを追加していきますが、現時点での各コードの役割は下記になります。

  1. app = Flask(__name__, instance_relative_config=True)…Flaskインスタンスの作成
    • __name__…現在のpythonモジュールの名前。アプリケーションは、pathのセットアップのためにディレクトリやファイルの配置を把握する必要があるが、__name__はそれを認識させる役割を持つ
    • instance_relative_config=True…アプリケーションの設定ファイルのpathがinstanceフォルダに対して相対pathであることを伝える。instanceフォルダはflaskrディレクトリの外側に置かれるが、厳秘の設定やデータベースファイルのような、バージョン管理の対象とならないローカルデータの保存場所として利用される
  2. app.config.from_mapping()…アプリケーションが利用する設定のデフォルト値をセットする
    • SECRET_KEY…データの安全性を保つために利用される。開発中の間は利便性の高いdevをセットしているが、本番環境へのデプロイ時にはランダムな値で上書きする
    • DATABASE…SQLiteのデータベースファイルが保存される場所。instanceディレクトリ内に設置される。次のセクションで詳しく扱う
  3. app.config.from_pyfile()instanceフォルダ内にconfig.pyファイルが存在する場合、デフォルトの設定値をそのファイルの値で上書きする。本番デプロイ時に本番用のSECRET_KEYをセットする場合などに使われる
    • test_config…アプリケーションファクトリーの引数で設定できるテスト用設定で、instanceフォルダでの設定の代わりに利用することができる。チュートリアルの後半で扱うテストで用いられる
    • silent=True…引数のファイルが存在しない場合でもエラーが立たない
  4. os.makedirs()app.instance_path(instanceディレクトリ)が存在するか確認する。Flaskは自動的にはinstanceディレクトリを作らないが、チュートリアルではSQLiteデータベースファイルをそこに作るため、instanceディレクトリが必要とされるため、この関数でinstanceフォルダの存在を確認している
  5. @app.route()…アプリケーションが機能することを確認するシンプルなページの実装用に使われている。URL/helloと関数との間のコネクションを確立し、この場合では文字列のレスポンスHello, World!を返す。

2-2. アプリケーションを実行する

flaskコマンドを使って、アプリケーションを実行します。ターミナルを使って、Flaskインスタンスにアプリケーションの場所を伝え、開発者モードで実行させます。実行ディレクトリはflask-tutorialであり、flaskrパッケージの中ではありません。

開発者モードでの実行によって、例外・エラーが発生した際にインタラクティブデバッガーが発動され(画面にエラー内容が表示される)、また起動中にコードが変更されるとサーバがすぐにリスタートされます。これによって開発中にサーバを起動したままにしておくことができます。

# ターミナルで実行(Mac,Linux向け)
flask-tutorial$ export FLASK_APP=flaskr
flask-tutorial$ export FLASK_ENV=development
flask-tutorial$ flask run
 * Serving Flask app "flaskr" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 147-098-836

ターミナルでflaskコマンドを実行すると上記のような表示が出ます。ブラウザでhttp://127.0.0.1:5000/helloにアクセスするとHello, World!がブラウザに表示されます。

tutorial1

3. データベースのアクセスと定義

このアプリケーションでは、ユーザや投稿データを保存するためのデータベースにSQLiteを使用します。PythonはSQLiteをサポートする組み込みのsqliteモジュールをもちます。

SQLiteは別個にサーバを立てる必要がなく、Pythonにも組み込まれているため、非常に便利です。ただし、複数のリクエストで同時にデータベースに書き込もうとするとそれぞれの書き込みは順次処理されるため、時間がかかります。小さなアプリケーションの場合問題はありませんが、アプリケーションの規模が大きくなれば、他のデータベースに切り替えた方が良いです。

チュートリアルでは、SQLiteについて詳しい説明を行わないため、詳細はドキュメントを参照して下さい。

3-1. データベースへの接続

SQLite(や大多数のPythonデータベースライブラリ)を利用するためにまず最初にやることは、SQLiteデータベースとのコネクションを確立することです。あらゆるクエリや操作はコネクションを使って行われ、利用を終了すればコネクションを閉じます。

Webアプリケーションの中でこのコネクションは概ねリクエストに紐づけられます。リクエストを処理する時にコネクションを確立し、レスポンスが送られる際に閉じられます。

# flaskr/db.py
import sqlite3  #Pythonの標準モジュール

import click  #Clickモジュールのインポート
from flask import current_app, g  #flask関数のインポート
from flask.cli import with_appcontext  #flask.cli関数のインポート

# DBとのコネクション確立
def get_db():
    if 'db' not in g:                              # コネクションの確認(g.dbを持たなければコネクションがない)
        g.db = sqlite3.connect(                    # Connectionオブジェクトの作成
            current_app.config['DATABASE'],        # DBの設定キー
            detect_types=sqlite3.PARSE_DECLTYPES   # 戻り値のカラムの型を読み取る
        )
        g.db.row_factory = sqlite3.Row

    return g.db

# コネクションのクローズ
def close_db(e=None):
    db = g.pop('db', None)  #'db'を持てばConnectionオブジェクトを返し、持たなければ'None'を返す

    if db is not None:
        db.close()

gは、それぞれのリクエストに対して一意である、特別なオブジェクトです。リクエストの際に複数の関数からアクセスされるデータを保存するために利用されます。コネクションはgオブジェクトに保存されるため、同一のリクエスト間でget_db関数が2度目の呼び出しがなされた場合、新しいコネクションを作る代わりにgオブジェクトに格納されたコネクションが再利用されます。

current_appは、リクエストを処理するFlaskアプリケーションを指定する、もう一つの特別なオブジェクトです。アプリケーションファクトリーを利用している場合、コードを書いている最中にはまだアプリケーション(FLaskインスタンス)が存在しません。get_dbはアプリケーション(Flaskインスタンス)が生成された時に呼び出され、リクエストを処理するためにcurrent_appを利用します。

sqlite3.Rowはデータベースのレコードをdict型のような形式でアクセスできるようにします。これにより名前を指定することでカラムにアクセスできるようになります。

close_db関数は、g.dbがセットされているかチェックすることによって、コネクションが作られているかチェックされます。コネクションが存在する場合は、そのコネクションが閉じられます。アプリケーションファクトリーの中でアプリケーションにclose_db関数を伝えることで、それぞれのリクエストごとに呼び出されるようになります。

3-2. テーブルの作成

SQLiteの中で、データはテーブルとカラムに保存されます。これらはデータを保存し、抽出できるようになる前に作成しておく必要があります。Flaskrはユーザをuserテーブルに保存し、投稿をpostテーブルに保存します。空のテーブルを作るため、SQLコマンドを使ってファイルを作成します。

# flaskr/schema.sql

DROP TABLE IF EXISTS user;  #テーブルが存在していればそのテーブルを削除
DROP TABLE IF EXISTS post;

# 'user'テーブルの作成
CREATE TABLE user (
  id INTEGER PRIMARY KEY AUTOINCREMENT,  # データ型:INTEGER、入力がない場合は今までの最大値+1を入力
  username TEXT UNIQUE NOT NULL,         # 'NULL'は入力できない
  password TEXT NOT NULL
);

CREATE TABLE post (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  author_id INTEGER NOT NULL,
  created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,  # 入力がない場合は、追加時点のタムスタンプを入力
  title TEXT NOT NULL,
  body TEXT NOT NULL,
  FOREIGN KEY (author_id) REFERENCES user (id)           # 'author_id'は'user'テーブルの'id'の値しか格納できな
);

これらのSQLコマンドを走らせる関数をdb.pyに追加します。

# flaskr.db.py
def init_db():
    db = get_db()  # コネクションの接続

    # f(schema.sql)読込('r'モード) → 'utf-8'でデコード → dbに書き込み
    with current_app.open_resource('schema.sql') as f:
        db.executescript(f.read().decode('utf8'))


@click.command('init-db')
@with_appcontext
def init_db_command():
    # 既存のデータを削除し、新しいテーブルを作る
    init_db()
    click.echo('Initialized the database.')

open_resource()はflaskrからの相対pathで指定されたファイルを開きます。これにより、アプリケーションをデプロイする時にファイルの絶対pathを把握する必要がなくなるためとても便利です。get_dbはデータベースのコネクションを返し、そのファイルから読み取られたコマンドを実行するために利用されます。

click.command()は、init_db関数を呼び出すコマンドラインのコマンドinit-dbを定義し、成功メッセージを表示します。コマンドの記載方法についてはCommand Line Interfaceを参照下さい。

(作成されるテーブルのイメージ)

テーブルイメージ

3-3. アプリケーションへの登録

close_db関数とinit_db_command関数は、アプリケーションインスタンス(Flaskインスタンス,app)に登録される必要があります。登録しないと、アプリケーション内で使用することができません。しかし、このアプリケーションではアプリケーションファクトリーを利用しているので、関数を書いている時点でアプリケーションインスタンスは利用することはできません。そのため、代わりにアプリケーション(Flaskインスタンス,app)を引数にとる関数を定義し、それをアプリケーションファクトリーに登録します。

# flaskr/db.py
def init_app(app):
    app.teardown_appcontext(close_db)
    app.cli.add_command(init_db_command)

app.teardown_appcontext()は、レスポンスを返した後の処理として引数の関数を呼び出すよう設定する関数です.

app.cli.add_command()は、flaskコマンドで呼び出せる新しいコマンドを加えます。

dbモジュールをインポートし、アプリケーションファクトリーにinit_db関数を登録します。

# flaskr/__init__.py
def create_app():
    app = ...
    # existing code omitted

    from . import db
    db.init_app(app)

    return app

3-4. データベースの初期化

これでinit-dbコマンドがアプリケーションに登録されたので、runコマンド同様にflaskコマンドを使って呼び出すことができます。

※ 注記
サーバを起動し続けている場合、init-dbコマンドはサーバを止めて実行することも、別のターミナルを開いてそこで実行することもできます。新しいターミナルで実行する場合、プロジェクトディレクトリに移動し、仮想環境をアクティベートした上で実行する必要があります。また、同様に環境変数FLASK_APPFLASK_ENVを最初に指定する必要があります。

init_dbコマンドを実行します。

(env)flask-tutorial$ flask init-db
Initialized the database.

これでinstanceフォルダ内にflaskr.sqliteファイルが作成されます。

チュートリアル3