前置き
タイトルを見て、一体何を言っているんだ?と思われた方もいるかと思いますが他にいいのが思いつきませんでした。
やりたいこととしては、例えば↓のようなレスポンスを返すDjango REST Frameworkで作ったAPIがあったとします。
[
{
"id": 1,
"name": "伏見稲荷大社",
"postcode": "612-0882",
"prefecture": {
"id": 26,
"name": "京都府"
},
"address": "京都市伏見区深草藪之内町68"
},
{
"id": 2,
"name": "広島平和記念資料館",
"postcode": "730-0811",
"prefecture": {
"id": 34,
"name": "広島県"
},
"address": "広島市中区中島町1-2"
}
]
これを↓のようにしたいんです。
[
{
"id": 1,
"name": "伏見稲荷大社",
"postcode": "612-0882",
"prefecture": "京都府",
"address": "京都市伏見区深草藪之内町68"
},
{
"id": 2,
"name": "広島平和記念資料館",
"postcode": "730-0811",
"prefecture": "広島県",
"address": "広島市中区中島町1-2"
}
]
ポイントはprefecture
です。beforeでは入れ子になっていますが、afterは入れ込になっていないのがわかると思います。
何故そうしたいのか、どうすればできるのか、の前にまずはbeforeのレスポンスを返すようなAPIを作るためのコードを用意したいと思います。
Django REST Frameworkを使いますので、準備としてひとまずは以下の画面がみられる状態にだけしておいて貰えればと思います。それと、django-admin startproject app
というようにapp
という名前でプロジェクトを作った前提で話しています。
モデルの定義
観光スポットモデルと都道府県マスタの為のモデルを以下のように定義します。CharField
のmax_length
の値とかが本当にこれが最適かと思うようなところもありますが、大体こんな感じになると思います。
from django.db import models
class Prefecture(models.Model):
name = models.CharField(
max_length=16,
)
def __str__(self):
return str(self.name)
class SightseeingSpot(models.Model):
name = models.CharField(
max_length=255,
)
postcode = models.CharField(
max_length=8,
)
prefecture = models.ForeignKey(
Prefecture,
on_delete=models.PROTECT,
)
address = models.CharField(
max_length=128,
)
def __str__(self):
return str(self.name)
ちなみに伏見稲荷大社と広島平和記念資料館は、2018年 外国人に人気の日本の観光スポットランキングの1位と2位らしいですよ。
モデルを作ったらマイグレーションします。
$ python manage.py makemigration
$ python manage.py migrate
なお、このSQLで都道府県とランキング1位と2位をDBに登録できます。
シリアライザーの定義
シリアライザーも以下のように定義します。
from .models import Prefecture
from .models import SightseeingSpot
from rest_framework import serializers
class PrefectureSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(
required=False,
)
name = serializers.CharField(
required=False,
)
class Meta(object):
model = Prefecture
fields = (
'id',
'name',
)
class SightseeingSpotSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(
required=False,
)
name = serializers.CharField(
required=False,
)
postcode = serializers.CharField(
required=False,
)
prefecture = PrefectureSerializer(
read_only=True,
)
address = serializers.CharField(
required=False,
)
class Meta(object):
model = SightseeingSpot
fields = (
'id',
'name',
'postcode',
'prefecture',
'address',
)
ViewSetの定義
ModelViewSetを定義します。
from .models import SightseeingSpot
from .serializers import SightseeingSpotSerializer
from rest_framework import viewsets
class SightseeingSpotViewSet(viewsets.ModelViewSet):
queryset = SightseeingSpot.objects.all()
serializer_class = SightseeingSpotSerializer
ルートの設定
/api/sightseeingspots
で観光スポット一覧が表示できるように設定します。
from .views import SightseeingSpotViewSet
from django.conf.urls import include
from django.conf.urls import url
from rest_framework import routers
router = routers.DefaultRouter()
router.register(r'^sightseeingspots', SightseeingSpotViewSet)
urlpatterns = [
url(r'^api/', include(router.urls)),
]
動作確認
このように/api/sightseeingspots
で観光スポット一覧が表示できればばっちりです。
ネストされていないかのように扱う データの取得時
さて、prefecture
がネストされています。気になります。どうしてもネストをなくしたい。どうればいいか。私はこうしました。
from .models import Prefecture
from .models import SightseeingSpot
from rest_framework import serializers
class PrefectureSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(
required=False,
)
name = serializers.CharField(
required=False,
)
class Meta(object):
model = Prefecture
fields = (
'id',
'name',
)
class SightseeingSpotSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(
required=False,
)
name = serializers.CharField(
required=False,
)
postcode = serializers.CharField(
required=False,
)
prefecture = serializers.SerializerMethodField(
read_only=True,
)
address = serializers.CharField(
required=False,
)
class Meta(object):
model = SightseeingSpot
fields = (
'id',
'name',
'postcode',
'prefecture',
'address',
)
def get_prefecture(self, obj):
return obj.prefecture.name
SightseeingSpotSerializer
でserializers.SerializerMethodField
を使うようにしました。これで返ってくるレスポンスは以下のようになります。
[
{
"id": 1,
"name": "伏見稲荷大社",
"postcode": "612-0882",
"prefecture": "京都府",
"address": "京都市伏見区深草藪之内町68"
},
{
"id": 2,
"name": "広島平和記念資料館",
"postcode": "730-0811",
"prefecture": "広島県",
"address": "広島市中区中島町1-2"
}
]
ネストされていないかのように扱う データの登録、更新時
ところでこのままでは登録、更新ができません。なので、app/serializers.py
を以下のようにします。
from .models import Prefecture
from .models import SightseeingSpot
from rest_framework import serializers
class PrefectureSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(
required=False,
)
name = serializers.CharField(
required=False,
)
class Meta(object):
model = Prefecture
fields = (
'id',
'name',
)
class SightseeingSpotSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(
required=False,
)
name = serializers.CharField(
required=False,
)
postcode = serializers.CharField(
required=False,
)
prefecture = serializers.SerializerMethodField(
read_only=True,
)
prefecture_id = serializers.IntegerField(
write_only=True,
)
address = serializers.CharField(
required=False,
)
class Meta(object):
model = SightseeingSpot
fields = (
'id',
'name',
'postcode',
'prefecture',
'prefecture_id',
'address',
)
def get_prefecture(self, obj):
return obj.prefecture.name
prefecture_id
を更新できるようにします。試しに
{
"prefecture_id": 4
}
というリクエストを/api/sightseeingspots/1/
に送り、伏見稲荷大社を宮城県に誘致してみます。すると、
{
"id": 1,
"name": "伏見稲荷大社",
"postcode": "612-0882",
"prefecture": "宮城県",
"address": "京都市伏見区深草藪之内町68"
}
というレスポンスが返ってくるので伏見稲荷大社を宮城県京都市に誘致できました。
おわりに
無事にタイトルを回収できました。真面目な話、ネストされたモデルをネストされていないかのように扱いたい時としては、今回のようにid
とname
だけを持ったテーブル(所謂マスターテーブル)のモデルを扱いたい時に需要があるのではないでしょうか。
マスターデータのモデルを扱いたい->そのためにネストされたjsonが返ってくるのは些か過剰->ネスト無しでname
だけ返ってきてほしい
といった感じで。そういうケースで使える方法だと思います。