概要
前回Flaskを利用したRESTFulなAPI環境を構築したのですが、親子関係のあるリソースを追加して、ネストしたJSONを返す方法をまとめてみました。
前記事: PythonのFlaskでMySQLを利用したRESTfulなAPIをDocker環境で実装する
https://qiita.com/kai_kou/items/5d73de21818d1d582f00
これが
> curl http://localhost:5000/hoges/3a401c04-44ff-4d0c-a46e-ee4b9454d872
{
"updateTime": "2018-10-13T10:16:06",
"id": "3a401c04-44ff-4d0c-a46e-ee4b9454d872",
"state": "hoge",
"name": "hoge",
"createTime": "2018-10-13T10:16:06"
}
こうなれば、ゴールです。
> curl http://localhost:5000/hoges/3a401c04-44ff-4d0c-a46e-ee4b9454d872
{
"updateTime": "2018-10-13T10:16:06",
"id": "3a401c04-44ff-4d0c-a46e-ee4b9454d872",
"state": "hoge",
"name": "hoge",
"createTime": "2018-10-13T10:16:06",
"parent": {
"updateTime": "2018-10-13T10:16:06",
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "foo",
"createTime": "2018-10-13T10:16:06"
}
}
今回のソースはGitHubにアップしているので、よければご参考ください。
https://github.com/kai-kou/flask-mysql-restful-api-on-docker/tree/feature/use-sqlalchemy-relationship
環境構築
前記事で構築した環境を利用します。
PythonのFlaskでMySQLを利用したRESTfulなAPIをDocker環境で実装する
https://qiita.com/kai_kou/items/5d73de21818d1d582f00
ソースを取得して、DBにテーブルを追加します。
> git clone https://github.com/kai-kou/flask-mysql-restful-api-on-docker.git
> cd flask-mysql-restful-api-on-docker
> docker-compose up -d
> docker-compose exec api flask db upgrade
動作確認しておきます。
> curl -X POST http://localhost:5000/hoges \
-H "Content-Type:application/json" \
-d "{\"name\":\"hoge\",\"state\":\"hoge\"}"
{
"createTime": "2018-11-02T13:16:26",
"id": "691d89de-fd34-41c1-b212-036cacca742e",
"name": "hoge",
"state": "hoge",
"updateTime": "2018-11-02T13:16:26"
}
> curl -X DELETE http://localhost:5000/hoges/691d89de-fd34-41c1-b212-036cacca742e
はい。
リソースを追加する
hoges
リソースに紐付くparents
リソースを追加します。
parents
が親、hoges
が子として、1:多となります。
こんな感じです。
@startuml
entity "parents" {
+ id [PK]
==
name
}
entity "hoges" {
+ id
==
# parent_id [FK]
name
state
}
parents --|{ hoges
@enduml
parentsリソースの追加
まずはリソースを追加するのにModelとAPIを実装します。
> touch src/models/parent.py
> touch src/apis/parent.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 ParentModel(db.Model):
__tablename__ = 'parents'
id = db.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4)
name = 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):
self.name = name
def __repr__(self):
return '<ParentModel {}:{}>'.format(self.id, self.name)
class ParentSchema(ma.ModelSchema):
class Meta:
model = ParentModel
createTime = fields.DateTime('%Y-%m-%dT%H:%M:%S')
updateTime = fields.DateTime('%Y-%m-%dT%H:%M:%S')
from flask_restful import Resource, reqparse, abort
from flask import jsonify
from src.models.parent import ParentModel, ParentSchema
from src.database import db
class ParentListAPI(Resource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
self.reqparse.add_argument('name', required=True)
super(ParentListAPI, self).__init__()
def get(self):
results = ParentModel.query.all()
jsonData = ParentSchema(many=True).dump(results).data
return jsonify({'items': jsonData})
def post(self):
args = self.reqparse.parse_args()
parent = ParentModel(args.name)
db.session.add(parent)
db.session.commit()
res = ParentSchema().dump(parent).data
return res, 201
class ParentAPI(Resource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
self.reqparse.add_argument('name')
super(ParentAPI, self).__init__()
def get(self, id):
parent = db.session.query(ParentModel).filter_by(id=id).first()
if parent == None:
abort(404)
res = ParentSchema().dump(parent).data
return res
def put(self, id):
parent = db.session.query(ParentModel).filter_by(id=id).first()
if parent == None:
abort(404)
args = self.reqparse.parse_args()
for name, value in args.items():
if value is not None:
setattr(parent, name, value)
db.session.add(parent)
db.session.commit()
return None, 204
def delete(self, id):
parent = db.session.query(ParentModel).filter_by(id=id).first()
if parent is not None:
db.session.delete(parent)
db.session.commit()
return None, 204
app.pyにもリソースを追加しておきます。
from flask import Flask, jsonify
from flask_restful import Api
from src.database import init_db
from src.apis.parent import ParentListAPI, ParentAPI
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)
# parentsリソースを追加
api.add_resource(ParentListAPI, '/parents')
api.add_resource(ParentAPI, '/parents/<id>')
api.add_resource(HogeListAPI, '/hoges')
api.add_resource(HogeAPI, '/hoges/<id>')
return app
app = create_app()
hogesリソースのModelとAPIの修正
hoges
からparents
リソースが参照したいので、hoges
側にリレーションの実装を追加します。
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()
from .parent import ParentModel, ParentSchema
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)
# 追加
parent_id = db.Column(UUIDType(binary=False), db.ForeignKey('parents.id'), nullable=False)
parent = db.relationship("ParentModel", backref='hoges')
createTime = db.Column(db.DateTime, nullable=False, default=datetime.now)
updateTime = db.Column(db.DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
# parent_idを追加
def __init__(self, name, state, parent_id):
self.name = name
self.state = state
self.parent_id
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')
# parentがネストされるようにする
parent = ma.Nested(ParentSchema)
追加した箇所を一部抜粋してみます。
外部キーの指定と、relationship
を利用して、hoge.parent
と参照できるようにしています。relationship
の利用方法は下記が参考になります。
SQLAlchemy 1.2 Documentation - Basic Relationship Patterns
https://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html
parent_id = db.Column(UUIDType(binary=False), db.ForeignKey('parents.id'), nullable=False)
parent = db.relationship("ParentModel", backref='hoges')
HogeSchema
でparents
リソースがネストされるように指定しています。
parent = ma.Nested(ParentSchema)
hoges
のAPI定義にparent_id
を追加しておきます。
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)
# 追加
self.reqparse.add_argument('parent_id', 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()
# parent_id追加
hoge = HogeModel(args.name, args.state, args.parent_id)
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')
# 追加
self.reqparse.add_argument('parent_id')
super(HogeAPI, self).__init__()
def get(self, id):
hoge = db.session.query(HogeModel).filter_by(id=id).first()
if hoge == 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 == 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
マイグレーションファイルの生成と手直し
実装ができたので、マイグレーションファイルを生成します。
> docker-compose exec api flask db migrate
通常は、マイグレーションしたらそのままDBへ反映すればよいのですが、Sqlalchemy UtilsでUUIDを利用しているとflask db upgrade
時にエラーとなるので、マイグレーションファイルを修正します。
sqlalchemy_utils
がインポートされないので追加します。
UUIDType(length=16)
となっているのを、UUIDType(binary=False)
に置き換えます。
また、create_foreign_key
とdrop_constraint
の第一パラメータがNone
になっていますが、このままだと、downgradeした際にエラーになります。name
みたいなので、適当に名前をつけてやります。
正直面倒です
(略)
from alembic import op
import sqlalchemy as sa
# 追加
import sqlalchemy_utils
(略)
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('parents',
# sa.Column('id', sqlalchemy_utils.types.uuid.UUIDType(length=16), nullable=False),
# binary=Falseに変更
sa.Column('id', sqlalchemy_utils.types.uuid.UUIDType(binary=False), nullable=False),
(略)
# op.add_column('hoges', sa.Column('parent_id', sqlalchemy_utils.types.uuid.UUIDType(length=16), nullable=False))
# binary=Falseに変更
op.add_column('hoges', sa.Column('parent_id', sqlalchemy_utils.types.uuid.UUIDType(binary=False), nullable=False))
# op.create_foreign_key(None, 'hoges', 'parents', ['parent_id'], ['id'])
# name指定
op.create_foreign_key('parent_id_fk', 'hoges', 'parents', ['parent_id'], ['id'])
(略)
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
# op.drop_constraint(None, 'hoges', type_='foreignkey')
# name指定
op.drop_constraint('parent_id_fk', 'hoges', type_='foreignkey')
op.drop_column('hoges', 'parent_id')
op.drop_table('parents')
# ### end Alembic commands ###
マイグレーションファイルの手直しができたらDBへ反映します。
> docker-compose exec api flask db upgrade
DBで確認するとparents
テーブルやhoges
テーブルにparent_id
が追加されています。
mysql> show tables;
+-----------------+
| Tables_in_hoge |
+-----------------+
| alembic_version |
| hoges |
| parents |
+-----------------+
mysql> SHOW COLUMNS FROM hoges;
+------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| id | char(32) | NO | PRI | NULL | |
| name | varchar(255) | NO | | NULL | |
| state | varchar(255) | NO | | NULL | |
| createTime | datetime | NO | | NULL | |
| updateTime | datetime | NO | | NULL | |
| parent_id | char(32) | NO | MUL | NULL | |
+------------+--------------+------+-----+---------+-------+
動作確認する
DBへ反映されたら動作確認してみます。
> curl -X POST http://localhost:5000/parents \
-H "Content-Type:application/json" \
-d "{\"name\":\"parents!\"}"
{
"id": "5f2f68da-3fa2-41af-8006-87b1bf0b8305",
"updateTime": "2018-11-02T14:50:46",
"name": "parents!",
"createTime": "2018-11-02T14:50:46"
}
> curl -X POST http://localhost:5000/hoges \
-H "Content-Type:application/json" \
-d "{\"name\":\"hoge\",\"state\":\"hoge\",\"parent_id\":\"5f2f68da-3fa2-41af-8006-87b1bf0b8305\"}"
{
"createTime": "2018-11-02T15:01:36",
"state": "hoge",
"parent": {
"createTime": "2018-11-02T14:50:46",
"name": "parents!",
"updateTime": "2018-11-02T14:50:46",
"id": "5f2f68da-3fa2-41af-8006-87b1bf0b8305"
},
"updateTime": "2018-11-02T15:01:36",
"name": "hoge",
"id": "727436e1-e11d-49cd-937a-2885b82faede"
}
> curl http://localhost:5000/hoges
{
"items": [
{
"createTime": "2018-11-02T15:01:36",
"id": "727436e1-e11d-49cd-937a-2885b82faede",
"name": "hoge",
"parent": {
"createTime": "2018-11-02T14:50:46",
"id": "5f2f68da-3fa2-41af-8006-87b1bf0b8305",
"name": "parents!",
"updateTime": "2018-11-02T14:50:46"
},
"state": "hoge",
"updateTime": "2018-11-02T15:01:36"
}
]
}
はい。
うまくネストすることができました。
まとめ
思ったより実装に手間がかかる感じでした。
特にSQLAlchemyでリレーションの設定方法がいくつもあってそのすべてが今回のケースに利用というわけでなく、ハズレを引いてドハマリ。となる可能性があります(ありましたTT
ただ、Schemaでネスト定義ができるので、APIの実装でゴリゴリしなくても良いのは楽でいい感じです^^
参考
PythonのFlaskでMySQLを利用したRESTfulなAPIをDocker環境で実装する
https://qiita.com/kai_kou/items/5d73de21818d1d582f00
SQLAlchemy 1.2 Documentation - Basic Relationship Patterns
https://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html