概要
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
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 # パスワードはお好みで
FROM mysql
EXPOSE 3306
ADD ./my.cnf /etc/mysql/conf.d/my.cnf # 設定ファイルの読み込み
CMD ["mysqld"]
文字コードの設定
[mysqld]
character-set-server=utf8
[mysql]
default-character-set=utf8
[client]
default-character-set=utf8
今回利用するデータベースが初期化時に作成されるようにします。
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.command
でflask run
コマンドを指定して、Flaskアプリが起動するようにしています。-h 0.0.0.0
オプションの指定がないとDockerコンテナ外からアクセスできないので、ご注意ください。
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
するようにしています。
FROM python:3.6
ARG project_dir=/src/
ADD src/requirements.txt $project_dir
WORKDIR $project_dir
RUN pip install -r 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アプリ起動用となります。
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
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
はデータベースの接続文字列など、アプリケーションの設定情報の指定に利用しています。
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
ではデータベースを利用するための初期化処理やマイグレーション管理のために必要なメソッドを定義しています。
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
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形式に変換しています。
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
にしてたりしますが、そのへんはお好みで。
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_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を実装する場合、モジュールを利用すると、いい感じに実装できるものの、利用するモジュールが増えて、学習コストがそこそこ掛かりそうです。
次は別の記事で単体テストを追加してみます。