10
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Flask-RESTfulとFlask-SQLAlchemyを利用してリソースをネストしてJSONを返す方法

Last updated at Posted at 2018-11-08

概要

前回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
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 '<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')

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/<id>')

  api.add_resource(HogeListAPI, '/hoges')
  api.add_resource(HogeAPI, '/hoges/<id>')

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

HogeSchemaparents リソースがネストされるように指定しています。

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

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?