LoginSignup
104
102

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-10-16

概要

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コンテナで動作させます。

> 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敗

> 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

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 # パスワードはお好みで
mysql/Dockerfile
FROM mysql
EXPOSE 3306

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

CMD ["mysqld"]

文字コードの設定

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

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

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

動作確認

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

> 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>

はい。

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

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.commandflask run コマンドを指定して、Flaskアプリが起動するようにしています。-h 0.0.0.0 オプションの指定がないとDockerコンテナ外からアクセスできないので、ご注意ください。

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 するようにしています。

Dockerfile
FROM python:3.6

ARG project_dir=/src/

ADD src/requirements.txt $project_dir

WORKDIR $project_dir

RUN pip install -r requirements.txt

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

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

動作確認

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

> 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アプリ起動用となります。

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

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 はデータベースの接続文字列など、アプリケーションの設定情報の指定に利用しています。

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 ではデータベースを利用するための初期化処理やマイグレーション管理のために必要なメソッドを定義しています。

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

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リソース1クラスにできなさそう

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

実装する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形式に変換しています。

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_atcreateTime にしてたりしますが、そのへんはお好みで。

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のコンテナに入って作業します。

> docker-compose exec api bash

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

コンテナ内
> 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 で```

コンテナ内
> 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
コンテナ内
> 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

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

> docker-compose exec db mysql -u root -p
コンテナ内
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_versionhoges テーブルが作成されていたらおkです。

動作確認する

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

> 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 する前にテーブルの中身をみておきます。

> 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してみます。

> 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を実装する場合、モジュールを利用すると、いい感じに実装できるものの、利用するモジュールが増えて、学習コストがそこそこ掛かりそうです。

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

104
102
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
104
102