概要
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アプリケーションであれば、これだけですぐに実装ができるのですが、その反面実現したいことによっては利用するモジュールが増えます。増えました。
[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://cloudpack.media/43978
flask
コマンドが実行できるように環境変数を指定しています。services.api.command
でflask 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メソッドを定義するとしたら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)
- GET hoges:
- HogeAPI
- GET hoges/[id]:
def get(self, id)
- PUT hoges/[id]:
def put(self, id)
- DELETE hoges/[id]:
def delete(self, id)
- GET hoges/[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_at
をcreateTime
にしてたりしますが、そのへんはお好みで。
src/models/hoge.py
from datetime import datetime from flask_marshmallow import Marshmallow from flask_marshmallow.fields import fields 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_version
とhoges
テーブルが作成されていたらお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を実装する場合、モジュールを利用すると、いい感じに実装できるものの、利用するモジュールが増えて、学習コストがそこそこ掛かりそうです。
次は別の記事で単体テストを追加してみます。