Flaskを無理やりMVCっぽく運用してみた
こんにちは!
スマレジ テックファームのMichiです!
みなさんはFlaskというフレームワークをご存知でしょうか?
FlaskはPython製のマイクロWebフレームワークで、MVTモデル(Model, View, Template)というアーキテクチャを採用しています。
ですが、「やっぱり使い慣れたMVCの枠組みで実装したい!」ということで、今回はFlaskを無理やりMVCっぽく運用する方法を解説します。
解説
アプリのサンプルはFlask公式ドキュメントのチュートリアルより、記事用に一部改変したものを使います。
ディレクトリ構成
. ├── app │ ├── controllers │ └── models │ └── static │ └── templates │ └── __init__.py │ └── auth.py │ └── config.py │ └── database.py │ └── route.py ├── migrations ├── .env ├── local.sqlite ├── requirements.txt
基本設定
公式ドキュメントではすべての設定を__init__.py
ファイルに書きますが、設定ごとに分割したいのでファイルを分けます。
認証系
auth.py
from flask import Flask from flask_login import LoginManager login_manager = LoginManager() def init_auth(app: Flask): login_manager.login_view = 'login' login_manager.login_message = '' login_manager.init_app(app)
コンフィグ
config.py
from flask import Flask from pathlib import Path def set_config(app: Flask): app.config.update( SECRET_KEY='dev', WTF_CSRF_SECRET_KEY='csrf', SQLALCHEMY_DATABASE_URI=f'sqlite:///{Path(__file__).parent.parent / "local.sqlite"}', SQLALCHEMY_TRACK_MODIFICATIONS=False, )
データベース
database.py
from flask import Flask from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() def init_db(app: Flask): db.init_app(app) Migrate(app, db)
Flaskインスタンスの作成
__init__.py
from flask import Flask from flask_wtf.csrf import CSRFProtect from app.auth import init_auth from app.database import init_db from app.config import set_config from app.route import create_route def create_app(testing: bool=False): app = Flask(__name__) csrf = CSRFProtect() csrf.init_app(app) set_config(app) init_db(app) init_auth(app) create_route(app) if testing: app.config.update(TESTING=True) return app
ここまでで作成した設定ファイルの関数を__init__.py
側で呼び出し、引数としてFlask
インスタンスを渡します。 これでアプリの初期設定が完了しました。
尚、create_route(app)
については解説がまだですが、後ほど解説します。
モデル
モデルも公式ドキュメントでは一つのファイルにすべてのモデルを定義していますが、今回はテーブルごとに分割します。
├── models │ └── __init__.py │ └── article.py │ └── user.py
models
ディレクトリ直下にそれぞれのモデルと__init__.py
ファイルを用意します。
article.py
from app.database import db from datetime import datetime class Article(db.Model): __tablename__ = 'articles' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) title = db.Column(db.String, nullable=True) body = db.Column(db.String, nullable=True) created_at = db.Column(db.DateTime, nullable=False, default=datetime.now) user = db.relationship('User', backref='articles')
user.py
from app.auth import login_manager from app.database import db from flask_login import UserMixin class User(db.Model, UserMixin): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String, unique=True, nullable=True) password = db.Column(db.String, nullable=True) @login_manager.user_loader def load_user(id): return User.query.get(id)
__init__.py
from .article import Article from .user import User __all__ = [Article, User]
__init__.py
に定義したモデルを記述するのを忘れないように。
ここに記述しておかないと、Flaskが各モデルを認識してくれません。
ルーティング
公式ドキュメントではルーティングとビジネスロジックをすべて同じファイル内に記述していますが、責務分離を明確にしたいので、ルーティングとコントローラにそれぞれ分割します。
まずはルーティングから。appディレクトリ直下にroute.py
ファイルを置きます。
route.py
from flask import Flask from flask_login import login_required from app.controllers import article_controller, auth_controller, user_controller def create_route(app: Flask): @app.route('/signup', methods=['GET', 'POST']) def signup(): return user_controller.signup() @app.route('/login', methods=['GET', 'POST']) def login(): return auth_controller.login() @app.route('/logout', methods=['GET']) def logout(): return auth_controller.logout() @app.route('/', methods=['GET']) @login_required def index(): return article_controller.read() @app.route('/create', methods=['GET', 'POST']) @login_required def create(): return article_controller.create() @app.route('/<int:id>/update', methods=['GET', 'POST']) @login_required def update(id): return article_controller.update(id) @app.route('/<int:id>/delete', methods=['POST']) @login_required def delete(id): return article_controller.delete(id)
全体を括っているcreate_route
関数は、最初に設定した__init__.py
ファイルからFlask
インスタンスを渡して呼び出します。
コントローラ
├── controllers │ └── article_controller.py │ └── auth_controller.py │ └── user_controller.py
controllers
ディレクトリの中身はこんな感じ。
記事のCRUD(article)、認証系(auth)、ユーザー登録(user)でファイルを分割してみました。
article_controller.py
from flask import ( abort, flash, redirect, render_template, request, url_for ) from flask_login import current_user from app.database import db from app.models import Article def read(): articles = Article.query.order_by('created_at').all() return render_template('index.html', articles=articles) def create(): if request.method == 'POST': title = request.form['title'] body = request.form['body'] error = None if not title: error = 'Title is required.' if error is not None: flash(error) else: article = Article(title=title, body=body, user_id=current_user.id) db.session.add(article) db.session.commit() return redirect(url_for('index')) return render_template('create.html') def update(id): article = Article.query.filter(Article.id == id).first() if article is None: abort(404, f"Post id {id} doesn't exist.") elif article.user_id != current_user.id: abort(403) if request.method == 'POST': title = request.form['title'] body = request.form['body'] error = None if not title: error = 'Title is required.' if error is not None: flash(error) else: article.title = title article.body = body db.session.commit() return redirect(url_for('index')) return render_template('update.html', article=article) def delete(id): query = Article.query.filter(Article.id == id) article = query.first() if article is None: abort(404, f"Article id {id} doesn't exist.") elif article.user_id != current_user.id: abort(403) query.delete() db.session.commit() return redirect(url_for('index'))
auth_controller.py
from flask import ( flash, redirect, render_template, request, url_for, ) from flask_login import login_user, logout_user from werkzeug.security import check_password_hash from app.models import User def login(): if request.method == 'POST': name = request.form['name'] password = request.form['password'] error = None user = User.query.filter(User.name == name).first() if user is None: error = 'Incorrect name.' elif not check_password_hash(user.password, password): error = 'Incorrect password.' if error is None: login_user(user) return redirect(url_for('index')) flash(error) return render_template('login.html') def logout(): logout_user() return redirect(url_for('index'))
user_controller.py
from flask import ( flash, redirect, render_template, request, url_for, ) from flask_login import login_user from werkzeug.security import generate_password_hash from app.database import db from app.models import User def signup(): if request.method == 'POST': name = request.form['name'] password = request.form['password'] error = None if not name: error = 'name is required.' elif not password: error = 'Password is required.' elif User.query.filter(User.name == name).first() is not None: error = f"User {name} is already registered." if error is None: user = User( name=name, password=generate_password_hash(password) ) db.session.add(user) db.session.commit() login_user(user) return redirect(url_for('index')) flash(error) return render_template('signup.html')
あとは、templates
とstatic
にHTMLとCSSを書いて、マイグレーションを実行すればアプリ完成です!
まとめ
FlaskをMVCっぽく使う方法について解説しました。
今回はコントローラにDBとのやり取りを記述しましたが、これをサービス層に切り出すとより明確な責務分離が行えます。
(私も実務ではservices
ディレクトリを切って運用を行っています)
ここまでやるならDjango使えばええやん