Flask1.0.2を使ってRESTfulなAPIをMVCで作ってみる

※この記事は1年半前にインターンシップの課題をしながら記述し、下書き状態で保存していたものを公開しています。
情報が古い場合もあるので、適宜更新していこうと思います。

この記事は、FlaskというPythonの軽量フレームワークを利用してRESTfulなAPIを作ってみようの記事です。

# はじめに
すでにFlaskを使ったRESTfulなAPIは多くの記事が書かれているが、Flaskも最近ではかなり進化していて、2年、3年前の文献とはちょっと違う今時のモダンな記述ができるようになっている。

インターンシップの技術テストでも指定言語にPythonを採用するケースが増えてきている、スラスラPythonをかけるほどのコーディングスキルが身についていなかったので、以前から気になっていたFlaskを利用して勉強してみる事にした。

最近のFlaskの流行りはSQLAlchemyというPythonのORMをラップして作られた、Flask-SQLAlchemyというライブラリを組み合わせてActive Recordでモデル記述をするようだということもわかり実際にここを組み合わせてRESTfulなAPIを作成していく。

# なぜ、Flaskなのか？
本題に入る前になぜ、Flaskというフレームワークを選定したのかということについても記述しておく。

フレームワークを学習するにあたって一番の障壁になることは学習コストであると私は考えている。
特にWebAPIはセッション管理やViewの生成などWebアプリケーションに必要な機能の中で利用しない機能が多い。

なので、無駄な学習コストをかけないということはとても有効的で、個人的に薄いフレームワークはPHPのSlimを愛用している。
PythonではFlaskフレームワークが薄いフレームワークでは有名なので、今回はFlaskを選定した。

# 環境構築
環境構築には毎度のことながらDockerを利用する。

今回利用するコンテナはPythonの実行用コンテナとMySQLコンテナの２つである。
MySQLに関しては特に必要なパッケージがないので、そのままMySQLコンテナを利用する。
Python実行のコンテナについてはFlaskとSQLAlchemy及び、mysqlclientをpipでインストールする必要がある。

## 実行環境
 - Docker for Mac
 - Python3.6.5
 - Flask1.0.2
 - flask-sqlalchemy2.3.2
 - mysql5.7

## 最終的なディレクトリ構成

```
.
├── Build
│   └── app
│       ├── Dockerfile
│       └── requirements.txt
├── DB
├── LICENSE
├── README.md
├── docker-compose.sample
├── docker-compose.yml
└── share
    ├── app.py
    ├── controllers
    │   ├── __init__.py
    │   ├── top_controller.py
    │   └── user_controller.py
    ├── db.py
    └── models
        ├── __init__.py
        └── user.py
```

## Dockerfile
Dockerfileは以下のように記述する
パスはBuild/app/Dockerfile
このDockerfileは/webディレクトリに移動し、requirements.txtをコンテナ内にコピーし、pip installする。

```Dockerfile
# ベースイメージの指定
FROM python:3.6.5

# ソースを置くディレクトリを変数として格納                                                  
ARG project_dir=/web/

# 必要なファイルをローカルからコンテナにコピー
ADD requirements.txt $project_dir

# requirements.txtに記載されたパッケージをインストール                         
WORKDIR $project_dir
RUN pip install -r requirements.txt
```

## Dockerコンテナの起動
```setUp.sh
docker-compose build
docker-compose up -d
```

# 実装
環境構築が終了したら、実際に実装をする。
実装は以下の手順で進める。

 - Flaskのビルトインサーバの立ち上げとdbの設定をする。
 - modelを作成する
 - controllerを作成する

## Flaskの基本仕様について
### ビルトインサーバについて
Flaskでアプリケーション開発をするにはappオブジェクトのrunメソッドを実行し、ビルトインサーバを起動する。
Flaskの立ち上げは以下の7行を記述するだけで最低限実行できる。

```app.py
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

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

### configについて
app.configの配列に値を追加することによってアプリケーションの設定を記述できる。
今回はデータベースと文字コードの設定をする。
データベースの設定は、ユーザ名、パスワード、ホスト名、データベース名の順番になっている。

```app.py
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://%s:%s@%s/%s?charset=utf8' % (
    "root",
    "BzHSGDBv2fya",
    "db",
    "database",
)
app.config['JSON_AS_ASCII'] = False
```

### データベースの設定について
Flask-SQLAlchemyを利用するので、db.pyでインスタンスを作成する。
　以下の2行を記述することによってdbインスタンスを作成する。
あとは、app.pyでdb.init_app(app)というようにdbにappインスタンスを登録する。

```db.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
```

### ルーティングについて
Flaskのデフォルトルーティングは、@app.route('/')というデコレータを利用して実装するが、このルーティングでは、デコレータの後に呼びだせる関数がグローバル関数に限定されてしまう。
なので、app.add_url_ruleを利用する。
以下の２つのプログラムはそれぞれ同じ動作をする

```app.py
@app.route('/')
def get():
    return 'Hello, World!'
```

```app.py
class TopController(MethodView):
    def get(self):
        return 'Hello, World!'

app.add_url_rule('/',view_func=TopController.as_view('top_controller'))
```

最終的に出来上がったapp.pyは以下のようになる。

```app.py
from flask import Flask
from db import db
from controllers.top_controller import TopController
from controllers.user_controller import UserController

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://%s:%s@%s/%s?charset=utf8' % (
    "root",
    "BzHSGDBv2fya",
    "db",
    "database",
)
app.config['JSON_AS_ASCII'] = False
app.secret_key = 'secret'

app.add_url_rule('/', view_func=TopController.as_view('top_controller'))
app.add_url_rule('/<int:id>', view_func=UserController.as_view('user_controller'))


if __name__ == '__main__':
    db.init_app(app)
    app.run(host="0.0.0.0", port=80, debug=True)
```

## モデルの作成
モデルもFlask-SQLAlchemyを利用するので、dbインスタンスを利用して作成する。
SQLAlchemyとの違いは、db.Columnやdb.Modelといったようにdbと先頭につける必要がある。
また、非常に重要なメソッドである、to_dict()を作成する。
to_dict（）はmodelを取得した後にAPIのレスポンスとして、jsonを作成する際にmodelオブジェクトは扱えないので、辞書型に変更するためのメソッドである。

作成した、userモデルを示す、

```user.py
from db import db
from datetime import datetime


class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
    updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)

    def to_dict(self):
        return {
            'id': self.id,
            'username': self.username,
            'email': self.email,
            'created_at': self.created_at.strftime('%Y-%m-%d %H:%M'),
            'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M'),
        }

    def __repr__(self):
        return '<User %r>' % self.username
```

## [WIP]コントローラの作成
最後にコントローラを作成する。
コントローラの作成は一番いろいろ考えたが、いい方法が見つからなかったので、とりあえず今の形になった。
もっと良い設計方法があると思うので、今後検討していきたい。

TopControllerは以下のようになった。ポイントは、ベースURLに対するリクエスト処理を記述していることだ。
userIdを指定されていないので、getではそのまま全てのデータを返却している。
postでは新しい、userを作成する。
jsonifyを利用して、辞書型をjsonに変更する。
また、一番はじめにアクセスする時には必ず、db.drop_all()とdb.create_all()を実行して、テーブル作成を行う。

```top_controller.py
from flask import request
from models.user import User
from flask.json import jsonify
from flask.views import MethodView
from db import db


class TopController(MethodView):
    def get(self):
        """
        全てのテーブルデータを取得する
        :return: json
        """
        # db.drop_all()
        # db.create_all()
        return jsonify({"data": [User.to_dict(user) for user in User.query.all()]})

    def post(self):
        """
        新しいユーザを作成する
        :return: json
        """
        username = request.form['username']
        email = request.form['email']
        user = User(username=username, email=email)
        db.session.add(user)
        db.session.commit()
        return jsonify({'response': 'ok'})
```

# 参考
[flask公式リファレンス](http://flask.pocoo.org/docs/1.0/patterns/wtforms/)
[flask-sqlalchemy公式リファレンス](http://flask-sqlalchemy.pocoo.org/2.3/quickstart/)
[Flask – SQLAlchemy](https://www.tutorialspoint.com/flask/flask_sqlalchemy.htm)
[yoshiya0503/Hermetica 04 View と RESTful API](https://github.com/yoshiya0503/Hermetica/wiki/04-View-%E3%81%A8-RESTful-API)
[Flaskの簡単な使い方](https://qiita.com/zaburo/items/5091041a5afb2a7dffc8)