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

## 概要

前回Flaskを利用したRESTFulなAPI環境を構築したのですが、親子関係のあるリソースを追加して、ネストしたJSONを返す方法をまとめてみました。

前記事: PythonのFlaskでMySQLを利用したRESTfulなAPIをDocker環境で実装する
https://qiita.com/kai_kou/items/5d73de21818d1d582f00

これが

```sh:実装前
> 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"
}
```

こうなれば、ゴールです。

```sh:実装後
> 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にテーブルを追加します。

```sh
> 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
```

動作確認しておきます。

```sh
> 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:多となります。
こんな感じです。

![](http://www.plantuml.com/plantuml/png/SoWkIImgAStDuKhDAyaigLHGAaWi0ZABbHIgkHI0G5iXCqKXEi0x5iopjGLJUOcvgLorN70z6VdfgIWwa1KhAq2CZ0SPvEOT2pS1p03QNm8o2sgjWgvkJRK2s3mkXzIy5A3B0000)

```uml
@startuml
entity "parents" {
    + id [PK]
    ==
    name
}

entity "hoges" {
    + id
    ==
    # parent_id [FK]
    name
    state
}
parents --|{ hoges
@enduml
```

### parentsリソースの追加

まずはリソースを追加するのにModelとAPIを実装します。

```sh
> touch src/models/parent.py
> touch src/apis/parent.py
```

```python: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')

```

```python: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にもリソースを追加しておきます。

```python: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``` 側にリレーションの実装を追加します。

```python: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


```python
  parent_id = db.Column(UUIDType(binary=False), db.ForeignKey('parents.id'), nullable=False)
  parent = db.relationship("ParentModel", backref='hoges')
```

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

```python
  parent = ma.Nested(ParentSchema)
```

```hoges``` のAPI定義に```parent_id``` を追加しておきます。

```python: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

```

### マイグレーションファイルの生成と手直し

実装ができたので、マイグレーションファイルを生成します。

```sh
> 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

~~正直面倒です~~

```python:生成されたマイグレーションファイル(手直し箇所を抜粋)
(略)

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へ反映します。

```sh
> docker-compose exec api flask db upgrade
```

DBで確認すると```parents``` テーブルや```hoges``` テーブルに```parent_id``` が追加されています。

```sh

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へ反映されたら動作確認してみます。

```sh
> 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