概要
前回Flaskを利用したRESTFulなAPI環境を構築したのですが、親子関係のあるリソースを追加して、ネストしたJSONを返す方法をまとめてみました。
前記事: PythonのFlaskでMySQLを利用したRESTfulなAPIをDocker環境で実装する
https://cloudpack.media/43980
これが
実装前
> 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://cloudpack.media/43980
ソースを取得して、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
src/models/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 ''.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')
src/apis/parent.py
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にもリソースを追加しておきます。
src/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/') api.add_resource(HogeListAPI, '/hoges') api.add_resource(HogeAPI, '/hoges/ ') return app app = create_app()
hogesリソースのModelとAPIの修正
hoges
からparents
リソースが参照したいので、hoges
側にリレーションの実装を追加します。
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() 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 ''.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
を追加しておきます。
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) # 追加 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
みたいなので、適当に名前をつけてやります。
https://github.com/miguelgrinberg/Flask-Migrate/issues/155
正直面倒です
生成されたマイグレーションファイル(手直し箇所を抜粋)
(略) 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://cloudpack.media/43980
SQLAlchemy 1.2 Documentation – Basic Relationship Patterns
https://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html