くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

DjangoでRESTfulなAPIしてみた

DjangoREST APIを簡単に作れるフレームワーク(djangorestframework)を使ってみた。 サクッと作れるので、いい感じ!

インストール

とりあえず、pipでインストール

$ pip install django djangorestframework

インストールしてたら、rest_frameworkを追加

INSTALLED_APPS = (
    ...
    'rest_framework',
)

概要

構成要素

  1. Model ・・・ DjangoのModel
  2. Serializer ・・・Modelをどのようにシリアライズ(・デシリアライズ)するかを決めるためのもの
  3. ViewSet ・・・ APIのクエリーをどう解釈するかを決めるためのもの
  4. URL Pattern ・・・ DjangoにURLのパターンを教えるためのもの

依存関係は、「Model ⇐ Serializer ⇐ ViewSet ⇐ URL Pattern」という感じ。

Model(models.py)

from django.db import models

class Todo(models.Model):
  title = models.CharField('タイトル', max_length=20)
  done = models.BooleanField('完了', default=False)

Serializer(serializer.py)

from rest_framework import serializers
from .models import Todo

class TodoSerializer(serializers.ModelSerializer):
    class Meta:
        model = Todo
        fields = '__all__'

ViewSet(views.py)

from rest_framework import viewsets

from .models import Todo
from .serializer import TodoSerializer

class TodoViewSet(viewsets.ModelViewSet):
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer

URL pattern(urls.py)

from rest_framework import routers
from .views import TodoViewSet

router = routers.DefaultRouter()
router.register(r'todos', TodoViewSet)
from django.conf.urls import url, include
from api.urls import router as api_router

urlpatterns = [
    path('api/', include(api_router.urls)), # api.urlsをincludeする
]

これで、「 http://api/todos/ 」にアクセスすると、todoの一覧が取得できるようになる!

小ネタ

MenyToManyFieldやForeignKeyを展開する

デフォルトのだと、外部キーや多対多のModelは展開されず、pkで表現される。 参照しているRelationは展開してほしい。。

展開する場合は、Serializerを変更するとよい感じに。

models.py
from django.db import models

class Category(models.Model):
  name = models.CharField('カテゴリ名', max_length=20)

class Todo(models.Model):
  title = models.CharField('タイトル', max_length=20)
  done = models.BooleanField('完了', default=False)
  categories = models.ManyToManyField(Category, verbose_name='カテゴリ')
serializer.py
from rest_framework import serializers
from .models import Todo, Category

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = '__all__'

class TodoSerializer(serializers.ModelSerializer):
    # 対象のフィールドのSerializerを置き換えると、CategorySerializerを使って展開される
    # ManyToManyのように複数の場合は「many=True」をつける
    # contextを設定すると、URLの展開などをしてくれる
    categories = CategorySerializer(many=True, context=self.context) 
    
    class Meta:
        model = Todo
        fields = '__all__'

MenyToManyFieldごとCreate/Updateしたい

デフォルトのままだと、Todoを追加/更新する場合に、同時にCategoryも追加されてしまう。。 というか、その前のvalidateで、「Categoryがすでに存在します」みたいになる。。 Categoryが存在したらcreateして、存在しなかったらgetしてほしい。。

既存のcreate/update処理をカスタマイズする場合も、Serializerを変更する

models.py
from django.db import models

class Category(models.Model):
  name = models.CharField('カテゴリ名', max_length=20)

class Todo(models.Model):
  title = models.CharField('タイトル', max_length=20)
  done = models.BooleanField('完了', default=False)
  categories = models.ManyToManyField(Category, verbose_name='カテゴリ')
serializer.py
from rest_framework import serializers
from .models import Todo, Category

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = ['id', 'name']
        extra_kwargs = { # validationを無効にする
            'id': {'validators': []}, 
            'name': {'validators': []},
        }
  
    def validate_name(self, data): # 独自のvalidationを追加。なにもしない
    return data

class TodoSerializer(serializers.ModelSerializer):
    categories = CategorySerializer(many=True) 
    
    class Meta:
        model = Todo
        fields = '__all__'
    
    # create時の処理をカスタマイズ
    def create(self, validated_data):
     # categoriesの部分をpopして退避
        categories_data = validated_data.pop('categories')
        
        # 残りのvalidated_dataからTodoをcreate
        todo = Todo.objects.create(**validated_data)
        todo.save()
        
        # 退避したcategories_dataからcategoriesを追加
        # idのcategoryがあれば、取得して追加。なければ、createして追加
        for category_data in categories_data:
          category = Category.objects.filter(id=category_data['id']).first()
          if category is None:
            category = Category.objects.create(**category_data)
          todo.categories.add(category)
    return todo
    
    # update時の処理をカスタマイズ
    def update(self, instance, validated_data):
        # categoriesの部分をpopして退避
        categories_data = validated_data.pop('categories')
        
        # Todo部分の更新処理
        instance.name = validated_data.get('name', instance.name)
        instance.done = validated_data.get('done', instance.name)

        # 一旦、タグを全削除
        instance.tags.clear() 
        # 追加時と同様に、退避したcategories_dataからcategoriesを追加
        for category_data in categories_data:
           category = Category.objects.filter(id=category_data['id']).first()
           if category is None:
               category = Category.objects.create(**category_data)
           instance.categories.add(category)
    
        instance.save()
    return instance
参考サイト

RESTっぽくない、Web APIを追加したい

RESTだとModelと対応したAPIの形になるので、ちょっとロジックを入れ込んだAPIとかを用意しにくい。。 これも、「ViewSetにactionを追加する」という形で独自のエントリポイントを追加できる。

from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

from .models import Todo
from .serializer import TodoSerializer

class TodoViewSet(viewsets.ModelViewSet):
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer
    
    @action(detail=False) # pkを取らないactionの場合は、「detail=False」にする
    def is_done(self, request):
        try:
            name = request.query_params.get('name') # GETパラメタを取得
            todo = Todo.objects.filter(name=name).first()
            
            # 独自の形式のresponse
            response = {
              'data': TodoSerializer(todo, context={'request': request}).data, # serializeしたdictを取得・設定
              'is_done': todo.is_done
            }
            return Response(response)
        except:
            return Response({'message': 'error'}, status=status.HTTP_400_BAD_REQUEST)

これで「 http://api/todos/is_done/?name=AAA 」にアクセスすると、is_doneが呼び出せるようになる!

参考サイト

以上!!

参考にしたサイト様