PythonのFlaskでMySQLを利用したRESTfulなAPIをDocker環境で実装する

## 概要

Flaskを利用してRESTfulなAPIを実装する場合、いくつかのモジュールを導入するといい感じに実装できるのですが、モジュールそれぞれのドキュメントはあるものの、じゃあ合わせて利用するには？って記事が少なかったのでまとめてみました。

今回利用したソースはGitHubにアップしています。

kai-kou/flask-mysql-restful-api-on-docker
https://github.com/kai-kou/flask-mysql-restful-api-on-docker


## 環境

MacのDockerコンテナ上で動作する環境をつくりました。
MySQLもDockerコンテナで動作させます。

```sh
> docker --version
Docker version 18.06.1-ce, build e68fc7a

> docker-compose --version
docker-compose version 1.22.0, build f46880f
```

## 利用したモジュール

気がついたらこんなに利用していました。
各モジュールを利用するにあたり公式や参考にさせてもらった記事をざっくりとまとめておきます。
~~ひととおり動作するところまで進んで、Django使ったほうが早かったかもと後悔したのは、また別のお話。~~

- Flask
- Flask-RESTful
- SQLAlchemy
- Flask-SQLAlchemy
- Flask-Migrate
- Flask-Marshmallow
- PyMySQL
- Gunicorn


### Flask

軽量Webフレームワークですね。簡単なWebアプリケーションであれば、これだけですぐに実装ができるのですが、その反面実現したいことによっては利用するモジュールが増えます。増えました。

Flask
http://flask.pocoo.org/

[Python] 軽量WebフレームワークのFlaskに入門（準備、起動、HTML、静的ファイル、GET、POSTなど）
https://www.yoheim.net/blog.php?q=20160505


### Flask-RESTful

Flask単体でもRESTfulなAPIは実装できるのですが、実装をすっきりさせたかったので、導入しています。

Flask-RESTful
https://flask-restful.readthedocs.io/en/latest/

こちらの記事が詳しかったです。感謝！

Flask-RESTful - KZKY memo
http://kzky.hatenablog.com/entry/2015/11/02/Flask-Restful


### SQLAlchemy

Pythonで定番のORM(オブジェクト・リレーショナル・マッパー)モジュールです。SQLを書かなくても良いのです。

SQLAlchemy - The Database Toolkit for Python
https://www.sqlalchemy.org/

Python3 の 定番ORM 「 SQLAlchemy 」で MySQL ・ SQLite 等を操作 – 導入からサンプルコード
https://it-engineer-lab.com/archives/1183


### Flask-SQLAlchemy

FlaskでSQLAlchemyを簡単に利用するためのモジュールです。

Flask-SQLAlchemy
http://flask-sqlalchemy.pocoo.org/2.1/

Flask-SQLAlchemyの使い方
https://qiita.com/msrks/items/673c083ca91f000d3ed1


### Flask-Migrate

DBスキーマをマイグレーション管理するのに利用します。

Alembicを使用したFlask+SQLAlchemyでマイグレーションするための拡張だそうです。(公式より)
自分でマイグレーションファイルを作成しなくても良いのがとても魅力的です。

Flask-Migrate documentation
https://flask-migrate.readthedocs.io/en/latest/

Flask + SQLAlchemyプロジェクトを始める手順
https://qiita.com/shirakiya/items/0114d51e9c189658002e


### Flask-Marshmallow

Flask-SQLAlchemyで取り扱うモデルをJSONに変換してくれるモジュールです。
マシュマロって名前が良いですね。

Flask-Marshmallow
https://flask-marshmallow.readthedocs.io/en/latest/

SQLAlchemy x marshmallowでModelからJSONへの変換を楽に行う
https://techblog.recochoku.jp/3107


### marshmallow-sqlalchemy

Flask-Marshmallowを利用するのに必要となります。

marshmallow-sqlalchemy
https://marshmallow-sqlalchemy.readthedocs.io/en/latest/


### PyMySQL

PythonのMySQLクライアントモジュールです。

PyMySQL
https://github.com/PyMySQL/PyMySQL


### Gunicorn

DockerでFlaskアプリを動作させるのに利用しています。

Gunicorn - Python WSGI HTTP Server for UNIX
https://gunicorn.org/


## ファイル構成

今回のファイル構成です。
それぞれのファイルについて説明をしていきます。
```__init__.py``` は今回空ですが、作成していないと、```import``` でハマるので、侮ってはいけません(1敗

```sh
> tree
.
├── Dockerfile
├── docker-compose.yml
├── mysql
│   ├── Dockerfile
│   ├── my.cnf
│   └── sqls
│       └── initialize.sql
└── src
    ├── __init__.py
    ├── apis
    │   └── hoge.py
    ├── app.py
    ├── config.py
    ├── database.py
    ├── models
    │   ├── __init__.py
    │   └── hoge.py
    ├── requirements.txt
    └── run.py
```

## Dockerの環境設定

### MySQLの設定

MySQLの設定に関しては下記を参考にさせていただきました。

docker-composeとMySQL公式イメージで簡単に開発環境用DBを作る
https://qiita.com/K_ichi/items/e8826c300e797b90e40f


```yaml:docker-compose.yaml(一部抜粋)
version: '3'

services:
(略)
  db:
    build: ./mysql/
    volumes:
      - ./mysql/mysql_data:/var/lib/mysql # データの永続化
      - ./mysql/sqls:/docker-entrypoint-initdb.d # 初期化時に実行するSQL
    environment:
      - MYSQL_ROOT_PASSWORD=hoge # パスワードはお好みで
```

```txt:mysql/Dockerfile
FROM mysql
EXPOSE 3306

ADD ./my.cnf /etc/mysql/conf.d/my.cnf # 設定ファイルの読み込み

CMD ["mysqld"]
```

文字コードの設定

```cnf:mysql/my.cnf
[mysqld]
character-set-server=utf8
[mysql]
default-character-set=utf8
[client]
default-character-set=utf8
```

今回利用するデータベースが初期化時に作成されるようにします。

```sql:mysql/sqls/initialize.sql
CREATE DATABASE hoge;
use hoge;
```

### 動作確認

MySQLのDockerコンテナが立ち上がるか確認するには以下のようにします。

```sh
> docker-compose build db
> docker-compose up -d db
> docker-compose exec db mysql -u root -p
Enter password:
(略)
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>
```

はい。

つながったらデータベースが作成されているか確認しておきます。

```sh
mysql> show databases;

+--------------------+
| Database           |
+--------------------+
| hoge               |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.01 sec)
```

## Flaskの設定

Flaskを動作させるDocker Composeの設定を確認します。

下記記事の設定をベースにしています。

Python+Flask環境をDockerで構築する
https://qiita.com/kai_kou/items/e78b546b9820c7d8f1f9


```flask``` コマンドが実行できるように環境変数を指定しています。
```services.api.command``` で```flask run``` コマンドを指定して、Flaskアプリが起動するようにしています。```-h 0.0.0.0``` オプションの指定がないとDockerコンテナ外からアクセスできないので、ご注意ください。

```yaml:docker-compose.yml(完全版)
version: '3'

services:
  api:
    build: .
    ports:
      - "5000:5000"
    volumes:
      - "./src:/src"
    tty: true
    environment:
      TZ: Asia/Tokyo
      FLASK_APP: run.py
      FLASK_ENV: development
    command: flask run -h 0.0.0.0
  db:
    build: ./mysql/
    volumes:
      - ./mysql/mysql_data:/var/lib/mysql
      - ./mysql/sqls:/docker-entrypoint-initdb.d
    environment:
      - MYSQL_ROOT_PASSWORD=hoge
```

Dockerfileで```pip install``` するようにしています。

```txt:Dockerfile
FROM python:3.6

ARG project_dir=/src/

ADD src/requirements.txt $project_dir

WORKDIR $project_dir

RUN pip install -r requirements.txt
```

最初に紹介したモジュールを指定しています。

```txt:requirements.txt
flask
sqlalchemy
flask-restful
flask-sqlalchemy
sqlalchemy_utils
flask-migrate
pymysql
gunicorn
flask_marshmallow
marshmallow-sqlalchemy
```

### 動作確認

こちらもDockerコンテナが起動するか確認するには以下のようにします。

```sh
> docker-compose build api
> docker-compose up -d api
> docker-compose logs api
(略)
api_1  |  * Serving Flask app "run.py" (lazy loading)
api_1  |  * Environment: development
api_1  |  * Debug mode: on
api_1  |  * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
api_1  |  * Restarting with stat
api_1  |  * Debugger is active!
api_1  |  * Debugger PIN: 221-047-422
```

はい。

もし、記事に沿って環境構築されている場合、まだ実装がないので、```http://0.0.0.0:5000``` にアクセスしても、エラーになりますので、ご注意ください。


## 実装

前置きが長くなりましたが、これでFlaskとMySQLが利用できるようになりましたので、実装を確認していきます。

```run.py``` はFlaskアプリ起動用となります。

```python:src/run.py
from src.app import app


if __name__ == '__main__':
  app.run()

```

```app.py``` でデータベース設定やAPIリソースのルーティング設定をしています。

Flask-RESTfulの```add_resource``` を利用することで、APIリソースの実装を```apis``` に切り離すことができるのが良いところですね。

Flask-SQLAlchemyの利用方法については下記がとても参考になりました。感謝！

Flask + SQLAlchemyプロジェクトを始める手順
https://qiita.com/shirakiya/items/0114d51e9c189658002e


```python:src/app.py
from flask import Flask, jsonify

from flask_restful import Api

from src.database import init_db

from src.apis.hoge import HogeListAPI, HogeAPI


def create_app():

  app = Flask(__name__)
  app.config.from_object('src.config.Config')

  init_db(app)

  api = Api(app)
  api.add_resource(HogeListAPI, '/hoges')
  api.add_resource(HogeAPI, '/hoges/<id>')

  return app


app = create_app()

```

```app.py``` でインポートしている```config.py``` はデータベースの接続文字列など、アプリケーションの設定情報の指定に利用しています。

```python:src/config.py
import os


class DevelopmentConfig:

  # SQLAlchemy
  SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@{host}/{database}?charset=utf8'.format(
    **{
      'user': os.getenv('DB_USER', 'root'),
      'password': os.getenv('DB_PASSWORD', 'hoge'),
      'host': os.getenv('DB_HOST', 'db'),
      'database': os.getenv('DB_DATABASE', 'hoge'),
    })
  SQLALCHEMY_TRACK_MODIFICATIONS = False
  SQLALCHEMY_ECHO = False


Config = DevelopmentConfig

```

```database.py``` ではデータベースを利用するための初期化処理やマイグレーション管理のために必要なメソッドを定義しています。

```python:src/database.py
from flask_sqlalchemy import SQLAlchemy

from flask_migrate import Migrate


db = SQLAlchemy()


def init_db(app):
  db.init_app(app)
  Migrate(app, db)

```

### APIリソースの実装

```app.py``` で読み込んでいるAPIリソースの実装です。

下記記事が公式ドキュメントのサンプルをベースに詳しく説明してくれています。感謝！

Flask-RESTful - KZKY memo
http://kzky.hatenablog.com/entry/2015/11/02/Flask-Restful


```python:src/apis/hoge.py
from flask_restful import Resource, reqparse, abort

from flask import jsonify

from src.models.hoge import HogeModel, HogeSchema

from src.database import db


class HogeListAPI(Resource):
  def __init__(self):
    self.reqparse = reqparse.RequestParser()
    self.reqparse.add_argument('name', required=True)
    self.reqparse.add_argument('state', required=True)
    super(HogeListAPI, self).__init__()


  def get(self):
    results = HogeModel.query.all()
    jsonData = HogeSchema(many=True).dump(results).data
    return jsonify({'items': jsonData})


  def post(self):
    args = self.reqparse.parse_args()
    hoge = HogeModel(args.name, args.state)
    db.session.add(hoge)
    db.session.commit()
    res = HogeSchema().dump(hoge).data
    return res, 201


class HogeAPI(Resource):
  def __init__(self):
    self.reqparse = reqparse.RequestParser()
    self.reqparse.add_argument('name')
    self.reqparse.add_argument('state')
    super(HogeAPI, self).__init__()


  def get(self, id):
    hoge = db.session.query(HogeModel).filter_by(id=id).first()
    if hoge is None:
      abort(404)

    res = HogeSchema().dump(hoge).data
    return res


  def put(self, id):
    hoge = db.session.query(HogeModel).filter_by(id=id).first()
    if hoge is None:
      abort(404)
    args = self.reqparse.parse_args()
    for name, value in args.items():
      if value is not None:
        setattr(hoge, name, value)
    db.session.add(hoge)
    db.session.commit()
    return None, 204


  def delete(self, id):
    hoge = db.session.query(HogeModel).filter_by(id=id).first()
    if hoge is not None:
      db.session.delete(hoge)
      db.session.commit()
    return None, 204

```

### ポイント: １リソース1クラスにできなさそう

```hoges``` リソースに対して以下のようにHTTPメソッドを定義するとしたら```HogeListAPI``` と```HogeAPI``` クラスのように分ける必要があるっぽいです。個人的にはまとめてしまいたい感じです。

#### 実装するHTTPメソッド
- GET hoges
- POST hoges
- GET hoges/[id]
- PUT hoges/[id]
- DELETE hoges/[id]

#### Flask-RESTfulの実装
- HogeListAPI
  - GET hoges: ```def get(self)``` 
  - POST hoges: ```def post(self)``` 
- HogeAPI
    - GET hoges/[id]: ```def get(self, id)``` 
    - PUT hoges/[id]: ```def put(self, id)``` 
    - DELETE hoges/[id]: ```def delete(self, id)``` 


### ポイント: モデルは```jsonify``` で返せない

以下のように取得した情報を```jsonify``` でJSON形式にして返せたらシンプルなのですが、駄目なので、Flask-Marshmallowを利用してJSON形式に変換しています。

```python:src/apis/hoge.py(だめな例)
  def get(self, id):
    hoge = db.session.query(HogeModel).filter_by(id=id).first()
    if hoge == None:
      abort(404)

    return jsonify(hoge) # これだとだめ(´・ω・｀)

```

Flask-Marshmallowについては下記の記事を参考にさせていただきました。感謝！

SQLAlchemy x marshmallowでModelからJSONへの変換を楽に行う
https://techblog.recochoku.jp/3107


### モデルの実装

Flask-SQLAlchemyを利用したモデルの実装になります。
APIリソースで利用、Flask-Migrateでマイグレーションする際に参照されます。

以下を記事を参考にして実装しました。感謝！

Flask + SQLAlchemyプロジェクトを始める手順
https://qiita.com/shirakiya/items/0114d51e9c189658002e

Flask-SQLAlchemyの使い方
https://qiita.com/msrks/items/673c083ca91f000d3ed1

SQLAlchemy x marshmallowでModelからJSONへの変換を楽に行う
https://techblog.recochoku.jp/3107

SQLAlchemyでのupdate
http://motomizuki.github.io/blog/2015/05/20/sqlalchemy_update_20150520/


```id``` をUUIDにしてたり、```created_at``` を```createTime``` にしてたりしますが、そのへんはお好みで。

```python:src/models/hoge.py
from datetime import datetime

from flask_marshmallow import Marshmallow

from flask_marshmallow.fields import fields

from sqlalchemy_utils import UUIDType

from src.database import db

import uuid

ma = Marshmallow()


class HogeModel(db.Model):
  __tablename__ = 'hoges'

  id = db.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4)
  name = db.Column(db.String(255), nullable=False)
  state = db.Column(db.String(255), nullable=False)

  createTime = db.Column(db.DateTime, nullable=False, default=datetime.now)
  updateTime = db.Column(db.DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)

  def __init__(self, name, state):
    self.name = name
    self.state = state


  def __repr__(self):
    return '<HogeModel {}:{}>'.format(self.id, self.name)


class HogeSchema(ma.ModelSchema):
  class Meta:
    model = HogeModel

  createTime = fields.DateTime('%Y-%m-%dT%H:%M:%S')
  updateTime = fields.DateTime('%Y-%m-%dT%H:%M:%S')

```


## マイグレーションする

マイグレーションに必要なファイルが準備できましたので、Flask-Migrateを利用して、データベースにテーブル追加してみます。

こちらも先程からなんども参考にしている下記が参考になります。

Flask + SQLAlchemyプロジェクトを始める手順
https://qiita.com/shirakiya/items/0114d51e9c189658002e#migration%E3%82%92%E8%A1%8C%E3%81%86%E3%81%AB%E3%81%AF

apiのコンテナに入って作業します。

```sh
> docker-compose exec api bash
```

```flask db init``` コマンドでマイグレーションに必要となるファイルが作成されます。

```sh:コンテナ内
> flask db init

  Creating directory /src/migrations ... done
  Creating directory /src/migrations/versions ... done
  Generating /src/migrations/env.py ... done
  Generating /src/migrations/alembic.ini ... done
  Generating /src/migrations/script.py.mako ... done
  Generating /src/migrations/README ... done
  Please edit configuration/connection/logging settings in '/src/migrations/alembic.ini' before proceeding.
```

```flask db migrate``` で```

```sh:コンテナ内
> flask db migrate

INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'hoges'
  Generating /src/migrations/versions/a6e84088c8fe_.py ... done
```

```sh:コンテナ内
> flask db upgrade

INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 244b6323079a, empty message
```

データベースにテーブルが追加されたか確認しています。

```sh
> docker-compose exec db mysql -u root -p
```

```sh:コンテナ内
mysql> use hoge;
mysql> show tables;
+-----------------+
| Tables_in_hoge  |
+-----------------+
| alembic_version |
| hoges           |
+-----------------+
2 rows in set (0.00 sec)

mysql> desc hoges;
+------------+--------------+------+-----+---------+-------+
| Field      | Type         | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| id         | varchar(255) | NO   | PRI | NULL    |       |
| name       | varchar(255) | NO   |     | NULL    |       |
| state      | varchar(255) | NO   |     | NULL    |       |
| createTime | datetime     | NO   |     | NULL    |       |
| updateTime | datetime     | NO   |     | NULL    |       |
+------------+--------------+------+-----+---------+-------+
5 rows in set (0.10 sec)
```

マイグレーション管理用の```alembic_version``` と```hoges``` テーブルが作成されていたらおｋです。

## 動作確認する

APIにアクセスしてみます。

```sh
> curl -X POST http://localhost:5000/hoges \
  -H "Content-Type:application/json" \
  -d "{\"name\":\"hoge\",\"state\":\"hoge\"}"

{
    "updateTime": "2018-10-13T10:16:06",
    "id": "3a401c04-44ff-4d0c-a46e-ee4b9454d872",
    "state": "hoge",
    "name": "hoge",
    "createTime": "2018-10-13T10:16:06"
}

> curl -X PUT http://localhost:5000/hoges/3a401c04-44ff-4d0c-a46e-ee4b9454d872 \
  -H "Content-Type:application/json" \
  -d "{\"name\":\"hogehoge\"}"

> curl http://localhost:5000/hoges/3a401c04-44ff-4d0c-a46e-ee4b9454d872

{
    "id": "3a401c04-44ff-4d0c-a46e-ee4b9454d872",
    "createTime": "2018-10-13T10:16:06",
    "state": "hoge",
    "updateTime": "2018-10-13T10:19:23",
    "name": "hogehoge"
}

> curl http://localhost:5000/hoges

{
  "items": [
    {
      "createTime": "2018-10-13T10:16:06",
      "id": "3a401c04-44ff-4d0c-a46e-ee4b9454d872",
      "name": "hogehoge",
      "state": "hoge",
      "updateTime": "2018-10-13T10:19:23"
    }
  ]
}
```

```DELETE``` する前にテーブルの中身をみておきます。

```sh
> docker-compose exec db mysql -u root -p
Enter password:
mysql> use hoge;
mysql> select * from hoges;
+--------------------------------------+----------+-------+---------------------+---------------------+
| id                                   | name     | state | createTime          | updateTime          |
+--------------------------------------+----------+-------+---------------------+---------------------+
| 3a401c04-44ff-4d0c-a46e-ee4b9454d872 | hogehoge | hoge  | 2018-10-13 10:16:06 | 2018-10-13 10:19:23 |
+--------------------------------------+----------+-------+---------------------+---------------------+
1 row in set (0.00 sec)

mysql> quit
```

ではDELETEしてみます。

```sh
> curl -X DELETE http://localhost:5000/hoges/3a401c04-44ff-4d0c-a46e-ee4b9454d872
> curl http://localhost:5000/hoges/3a401c04-44ff-4d0c-a46e-ee4b9454d872
{
    "message": "The requested URL was not found on the server.  If you entered the URL manually please check your spelling and try again."
}
```

はい。一通りのAPIがうまく機能していることが確認できました。

## まとめ

FlaskでDBを利用するRESTfulなAPIを実装する場合、モジュールを利用すると、いい感じに実装できるものの、利用するモジュールが増えて、学習コストがそこそこ掛かりそうです。

次は別の記事で単体テストを追加してみます。