0utputab1e

[ Python ] FlaskとSqlAlchemyで簡単なCRUDアプリを作ってみた

 2020-07-12
 

過去の記事で、SQLAlchemyを使ってデータベースのテーブルを作成しました。今回は、Flaskと組み合わせて簡単なCRUDアプリを作成してみようと思います。

ディレクトリ構成

以下の構成で作成していきます。

$ tree
.
├── crud.py
├── database.py
└── templates
    ├── base.html
    ├── index.html
    └── update.html

1 directory, 5 files

MySQLのセットアップ

今回のアプリケーションは

過去の記事「Pythonライブラリ「SQLAlchemy」を使ってみた」で完成した環境をベースに作成していきます。

MySQLはDockerを使って立てています。立てかたは

過去の記事「MysqlをDocker(docker-compose)で立ててみた」を参考にしてみてください。

モデルの作成

以下のように、メインプログラムから呼び出すモデルクラスを定義していきます。

import sys
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.sql.functions import current_timestamp
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, TIMESTAMP

DATABASE = 'mysql://%s:%s@%s/%s?charset=utf8mb4' % (
    "docker",  #ユーザー「docker」の
    "docker",  #パスワード「docker」で
    "127.0.0.1:3306",  #MySQLサーバ(http:\/\/127.0.0.1:3306)に接続し
    "test_db", #データベース「test_db」にアクセスします
)

#DB接続用のインスタンスを作成
ENGINE = create_engine(
    DATABASE,
    convert_unicode=True,
    echo=True  #SQLをログに吐き出すフラグ
)

#上記のインスタンスを使って、MySQLとのセッションを張ります
session = scoped_session(
    sessionmaker(
        autoflush = False,
        autocommit = False,
        bind = ENGINE,
    )
)

#以下に書いていくDBモデルのベース部分を作ります
Base = declarative_base()
Base.query = session.query_property()

#DBとデータをやり取りするためのモデルを定義
class User(Base):
    __tablename__ = 'users'
    id = Column('id', Integer, primary_key = True)
    name = Column('name', String(200))
    age = Column('age', Integer)
    created_at = Column('created_at', TIMESTAMP, server_default=current_timestamp())
    updated_at = Column('updated_at', TIMESTAMP, nullable=False, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))

#このPythonスクリプトを実行したとき、テーブルを一旦削除して新規作成する
def main(args):
    Base.metadata.drop_all(bind=ENGINE)
    Base.metadata.create_all(bind=ENGINE)

#このファイルを直接実行したとき、mainメソッドでテーブルを作成する
if __name__ == "__main__":
    main(sys.argv)

このファイルを直接呼び出せばテーブル初期化を行い、メインプログラムから呼び出せばクエリ発行器として機能します。

 
ベースモデル作成時の記述に

Base.query = session.query_property()

とありますが、このようにすると

User.query.filter(User.id == 1).one()

のように、sessionからクエリを作らなくても、Baseモデルを継承したモデル(この例では「User」)から直接クエリを呼び出すことができるようになります。

 
また、

  • 作成日時(created_at)
  • 更新日時(updated_at)

を自動で日時セットするには

created_at = Column('created_at', TIMESTAMP, server_default=current_timestamp())
updated_at = Column('updated_at', DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))

のようにして、

  • CURRENT_TIMESTAMP
  • CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

をデフォルト値に適用すればOKです。

そして、データベースの初期化を行います。

$ python database.py

(中略)

2020-07-12 23:37:57,806 INFO sqlalchemy.engine.base.Engine
CREATE TABLE users (
	id INTEGER NOT NULL AUTO_INCREMENT,
	name VARCHAR(200),
	age INTEGER,
	created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
	updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
	PRIMARY KEY (id)
)


2020-07-12 23:37:57,806 INFO sqlalchemy.engine.base.Engine ()
2020-07-12 23:37:57,811 INFO sqlalchemy.engine.base.Engine COMMIT

CSRF対策の準備

CSRF対策(フォームなどを処理するWebアプリケーションが、本来拒否すべき他サイトからのリクエストを受信し処理してしまわないための対策)も実装します。

※CSRFについてはこちらのサイトが参考になります。

pythonのライブラリで「Flask-WTF」というFlaskのCSRF対策用ライブラリがありますので、こちらをインストールしていきます。

$ pip install Flask-WTF

アプリケーション本体の実装

Flaskで「一覧表示」、「新規作成」「更新」、「削除」を実装してみます。
今回はフォームのバリデーションは実装してないです(後日更新しますね)。

from flask import Flask, render_template, request, redirect, url_for
from flask_wtf.csrf import CSRFProtect
import os
from database import * #さっき作成した「database.py」から部品を取得


app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24) #セッション情報を暗号化するための設定
csrf = CSRFProtect(app)

@app.route('/', methods=['GET'])
def index():
    title = "ユーザー一覧"
    names = session.query(User).order_by(desc(User.updated_at)).all()
    return render_template('index.html', names=names, title=title)


@app.route('/', methods=['POST'])
def insert():
    try:
        user = User()
        user.id = 0
        user.age = request.form.get('age', None)
        user.name = request.form.get('name', None)
        session.add(user)
        session.flush()
        session.commit()
        return redirect(url_for('index'))
    except Exception as e:
        return redirect(url_for('index'))

@app.route('/<int:id>', methods=['GET'])
def edit(id):
    try:
        title = '編集画面'
        name = User.query.filter(User.id == id).one()
        return render_template('update.html', name=name, title=title)
    except Exception as e:
        return redirect(url_for('index'))

@app.route('/<int:id>/update', methods=['POST'])
def update(id):
    try:
        if request.form.get('_method', '') == 'PUT':
            user = User.query.filter(User.id == id).one()
            user.age = request.form.get('age', None)
            user.name = request.form.get('name', None)
            session.commit()
            return redirect(url_for('one_user', id=int(id)))
        else:
            return redirect(url_for('index'))
    except Exception as e:
        print("Failed to update user")
        print(e)
        return redirect(url_for('index'))

@app.route('/delete', methods=['POST'])
def delete():
    try:
        if request.form.get('_method', '') == 'DELETE':
            id = request.form['id']
            user = User.query.filter(User.id == id).one()
            session.delete(user)
            session.commit()
        return redirect(url_for('index'))
    except Exception as e:
        return redirect(url_for('index'))

if __name__=="__main__":
    app.debug = True
    app.threaded = True
    app.run(host='0.0.0.0', port="8888")

テンプレート側の実装

先程サーバ側でCSRFの実装をしたので、フォーム送信時にトークンをサーバに送る記述も忘れないように。

<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>

が該当の部分です。

ちなみにHTMLではHTTPメソッドのうち「GET」「POST」歯科対応してないので、「PUT」「DELETE」など他のHTTPメソッドでやり取りしたい場合はテンプレート側に以下のような記述が必要です。

<input type="hidden" name="_method" value="DELETE"/>

こうしてHTML側から送信したメソッドの要求をサーバ側で読み取るには

request.form.get('_method', '') == 'PUT'

のようにしてアクセスの判定等に使用します。

 
それでは、base.htmlと呼び出しごとのテンプレートを用意します。

  • base.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>{{ title }}</title>
    </head>
    <body>
    {% block content %}
    <!--ここに部品としてのHTMLを入れることになります-->
    {% endblock %}
    </body>
</html>
  • index.html(一覧+新規登録)
{% extends "base.html" %}
{% block content %}
<h2>ユーザー登録フォーム</h2>
<form style="line-height: 14px;" action="/" method="POST">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
    <p><label>名前: </label><input type="text" name="name" value=""></p>
    <p><label>年齢: </label><input type="number" name="age" value=""></p>
    <p><button type="submit">新規登録</button></p>
</form>
<h2>{{ title }}</h2>
{% for name in names %}
<div style="line-height: 12px;">
    <p>id: {{ name.id }}</p>
    <p>name: {{ name.name }}</p>
    <p>age: {{ name.age }}</p>
    <p>created_at: {{ name.created_at }}</p>
    <p>updated_at: {{ name.updated_at }}</p>
</div>
<div style="margin-bottom: 40px;">
    <a href="/{{ name.id }}"><button type="button">編集</button></a>
    <form action="/delete" method="POST">
        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
        <input type="hidden" name="_method" value="DELETE"/>
        <input type="hidden" name="id" value="{{ name.id }}">
        <button type="submit">削除</button>
    </form>
</div>
{% endfor %}
{% endblock %}
  • update.html(既存のユーザーデータを編集するための画面)
{% extends "base.html" %}
{% block content %}
<h2>{{ title }}</h2>
<form action="/{{ name.id }}/update" method="POST">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
    <input type="hidden" name="_method" value="PUT"/>
    <p>id: {{ name.id }}</p>
    <p><label>name: </label><input type="text" name="name" value="{{ name.name }}"></p>
    <p><label>age: </label><input type="number" name="age" value="{{ name.age }}"></p>
    <p>created_at: {{ name.created_at }}</p>
    <p>updated_at: {{ name.updated_at }}</p>
    <p><button type="submit">保存</button></p>
</form>
<form action="/delete" method="POST">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
    <input type="hidden" name="_method" value="DELETE"/>
    <input type="hidden" name="id" value="{{ name.id }}"/>
    <p><button type="submit">削除</button></p>
</form>
{% endblock %}

アプリケーション起動

MySQLを起動したら、アプリケーションを起動します。

$ python crud.py

ブラウザで「http://localhost:8888」を呼び出してみます。

  • 一覧/新規登録画面(それぞれのユーザーデータに「編集」「削除」ボタンがあります)
    一覧/新規登録画面

  • 個別編集画面(データを編集して保存、あるいは削除ができます)
    個別編集画面

動作もOKです!

最後に

FlaskはDjangoのようなフルスタックフレームワークとは違い、自分でいろいろ付け足しながらしないといけない大変さがありますが、仕組みを学習していくのにはすごくいいと思います。

次回以降、徐々に機能を付け足していけたらと思います。

 
では!

 

あわせて読みたい記事

>> Homeに戻る