たとえば特定のカテゴリに所属するアイテムの一覧を取得する場合に/categories/<category>/items/
みたいな感じのURLにしたくてdrf-nested-routers使ってみた。
drf-nested-routersで実装してみる
READMEを見ながら下記のURLのように特定のCategoryの下にItemがくるようにネストしたURLを実装する。
/categories
/categories/{pk}
/categories/{category_pk}/items
/categories/{category_pk}/items/{pk}
models.py
まずはカテゴリとアイテムのモデルを実装。
class Category(models.Model):
name = models.CharField(max_length=30)
slug = models.SlugField(unique=True)
class Item(models.Model):
name = models.CharField(max_length=100)
category = models.ForeignKey(Category)
display_order = models.IntegerField(default=0, help_text='表示順')
serializers.py
カテゴリとアイテムのserializersも実装する。
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = (
'pk',
'name',
'slug',
)
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = (
'pk',
'name',
'display_order',
'category',
)
views.py
後述するがurlの生成はrouterを使うのだけれどもpk
やcategory_pk
が数字になるように指定ができない。
なのでうっかり/category/hoge/items
とか/category/1/items/hoge
みたいに入力されるとValueError
が発生してInternalServerErrorになってしまうということに気をつけなければいけない。
ValueError
を回避するためにretrieve()
の方はdjango.shortcuts.get_object_or_404
じゃなくてrest_framework.generics.get_object_or_404
を使用する。
ただ、list()
のほうが残念ながらrest_framework.generics.get_list_or_404
は存在しないのでrest_framework.generics.get_object_or_404
に倣ってTypeErrorとValuErrorが発生した場合はHttp404をraiseする。
from rest_framework.generics import get_object_or_404
class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
class ItemViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = ItemSerializer
queryset = Item.objects.all()
def retrieve(self, request, pk=None, category_pk=None):
item = get_object_or_404(self.queryset, pk=pk, category__pk=category_pk)
serializer = self.get_serializer(item)
return Response(serializer.data)
def list(self, request, category_pk=None):
try:
items = get_list_or_404(self.queryset, category__pk=category_pk)
except (TypeError, ValueError):
raise Http404
else:
serializer = self.get_serializer(items, many=True)
return Response(serializer.data)
urls.py
NestedSimpleRouterを使ってルーティングする。
from rest_framework_nested import routers
router = routers.SimpleRouter(trailing_slash=False)
router.register(r'categories', CategoryViewSet)
categories_router = routers.NestedSimpleRouter(
router, r'categories', lookup='category', trailing_slash=False)
categories_router.register(r'items', ItemViewSet)
urlpatterns = [
url(r'^', include(router.urls)),
url(r'^', include(categories_router.urls)),
]
上記の実装で登録されるURLは下記の通り。
categories$ [name='category-list']
categories/(?P<pk>[^/.]+)$ [name='category-detail']
categories/(?P<categoary_pk>[^/.]+)/items$ [name='item-list']
categories/(?P<categoary_pk>[^/.]+)/items/(?P<pk>[^/.]+)$ [name='item-detail']
更新系のメソッドの実装
READMEを見た感じだと子供側のViewSetにlist()
とretrieve()
以外の実装のサンプルがなかったので試してみた。
create
気を付けなくちゃいけないのは2点。
1点目はclientから受け取ったリクエストのデータに'category'
が存在しても無視してcategory_pkを使用すること(もしくはエラーで弾くこと)。
2点目はcategoryの存在チェックも忘れずにすること。
それらに気をつけながら実装するとこんな感じ。
def create(self, request, category_pk=None):
category = get_object_or_404(Category.objects, pk=category_pk)
request.data['category'] = category.pk
return super(ItemViewSet, self).create(request)
update
基本的に気をつけることはcreateと同じ。
ただ、clientから受け取ったデータにcategoryの指定があった場合の処理は悩ましいところではあるけどvalidationで弾いてしまう。
views.py:
def update(self, request, category_pk=None, *args, **kwargs):
category = get_object_or_404(Category.objects, pk=category_pk)
request.data['category'] = category.pk
return super(ItemViewSet, self).update(request, *args, **kwargs)
serializers.py:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = (
'pk',
'name',
'display_order',
'category',
)
def validate_category(self, category):
if self.instance and self.instance.category_id != category.id:
raise serializers.ValidationError("can not update to category.")
return category
categoryを変更できるようにしても良いと思うのだけれど、その場合のステータスコードはstatus.HTTP_204_NO_CONTENT
あたりが適切なのかなぁ。
PUTしたときのURLにはリソースが存在しなくなるわけだし。
destroy
destroyも基本的に気をつけることはcreateと同じ。
ただ、categoryを取得する必要はないのでget_object_or_404
でitemを取得するように実装する。
def destroy(self, request, pk=None, category_pk=None):
item = get_object_or_404(self.queryset, pk=pk, category__pk=category_pk)
self.perform_destroy(item)
return Response(status=status.HTTP_204_NO_CONTENT)
子をソートする
/categories/{category_pk}/items
にアクセスした結果をソートするのは簡単。
ViewSetのquery_setを修正するだけ。
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
queryset = Item.objects.all().order_by('display_order')
ただ上記の方法では/categories/{category_pk}
の結果にitemsを入れて、そいつをソートすることはできない。
この場合はModelのorderingで指定するしか方法がなさそう。
serializers.py:
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = (
'pk',
'name',
'slug',
'item_set'
)
item_set = ItemSerializer(many=True, read_only=True)
models.py:
class Item(models.Model):
class Meta(object):
ordering = ('display_order', 'pk')
name = models.CharField(max_length=100)
category = models.ForeignKey(Category)
display_order = models.IntegerField(default=0, help_text='表示順')
URLにslugを使う
categories/1/items
ではなくてcategories/some-slug/items
ってしたい場合はViewSetのlookup_fieldを指定するだけ。
class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
lookup_field = 'slug'
この場合に登録されるURLは下記の通り。
categories$ [name='category-list']
categories/(?P<slug>[^/.]+)$ [name='category-detail']
categories/(?P<category_slug>[^/.]+)/items$ [name='item-list']
categories/(?P<category_slug>[^/.]+)/items/(?P<pk>[^/.]+)$ [name='item-detail']
ItemViewSetに渡される引数がcategory_slugになるので各種メソッドの引数はそれに合わせる必要がある。
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
queryset = Item.objects.all().order_by('display_order')
def retrieve(self, request, pk=None, category_slug=None):
item = get_object_or_404(self.queryset, pk=pk, category__slug=category_slug)
serializer = self.get_serializer(item)
return Response(serializer.data)
list_ruote
list_routeも普通に使える。
例えばitemを指定の順番に並び替えたい場合にcategories/1/items/sort
でdisplay_orderを更新するメソッドを実装したりする。
class ItemViewSet(viewsets.ModelViewSet):
(...省略)
@list_route(methods=['patch'])
@transaction.atomic
def sort(self, request, category_pk=None):
items = self.queryset.filter(category__pk=category_pk)
for i, pk in enumerate(request.data.get('item_pks', [])):
items.filter(pk=pk).update(display_order=i + 1)
return Response()
ソースコード
気が向いたらどっかにサンプルコードを上げる。
各種バージョン
Python==3.6
Django==1.10.6
djangorestframework==3.6.2
drf-nested-routers==0.90.0
参考サイト