Django REST frameworkで色々苦闘したのをまとめる

## 1. この記事は何か


<a href="https://qiita.com/studio_haneya/items/58453516b4b86c914466#5-2-%E3%83%AA%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%A2%E3%83%87%E3%83%AB%E3%82%92%E4%BD%BF%E3%81%86
">先日Django REST frameworkに入門した</a>のですが、チュートリアルから外れたことをやろうとしてかなり苦労したので、そのあれこれをまとめます


## 2. Django REST frameworkのここが辛い


DBへのCRUD操作をやるAPIを作るだけならものすごく簡単に書けるのがDjango REST frameworkの魅力ですが、そこから外れると途端に難しくなります。Serializerを省略してしまえばAPIViewやViewSetで直接Djangoのモデルを操作するだけなので簡単ですが、Serializerを使わないとバリデーションを自前で書くことになってダサいしダルいので何とかSerializerは使いたい。そうなると以下が問題になってきます


<b>問題1. エラーが隠蔽されていて何が問題なのか分かりにくい</b>
<b>問題2. Django REST framework内部の動作順がよく分からない</b>


ちょっと変えた結果エラーが出ているが、何が原因なのか何を変えたらいいのか分からないという状態で悶々とするというのをしばらくやっていましたが、問題は上記の2つなので、内部の動作順を理解する、エラー内容を表示できるようにするという2つをやれば解決できるわけです


## 3. 動作順について理解する


動作順をざっくり把握することで、どこで引っかかってるか調べることが出来るようになります。メソッドの実行順が分かってしまえばオーバーライドしてprintデバッグしたり、pdbを使ったり、logを書きだしたり出来ますから、何が起こっているか把握することが可能になります


### GET(read, list)の場合


クライアントがGETリクエストを送る
　↓
httpリクエストの種類で呼ぶメソッドを決める
　↓
ViewSetのlistメソッド内で以下を実行する
　　Model.objects.all()でModelのデータを取得
　↓
取得したデータをSerializerに渡してResponseを作成する
 

### POST(create)の場合


クライアントがPOSTリクエストでJSONを送る
　↓
httpリクエストの種類で呼ぶメソッドを決める
　↓
ViewSetのcreateメソッド内で以下を実行する
　↓
serializer = Serializer(data=request.data)でインスタンスを作成する
　　レコードを引数instanceをNoneとするとcreateするようになる
serializer.dispatchでParserを選択、受け取ったRequestをParseする
　　ParserはリクエストのheaderのContent-Typeにより
　　JSONParser, FormParser, MultiPartParserから選択される
serializer.is_valid内でrun_validationメソッドを実行
　　run_validationで以下2つのメソッドでバリデーションを実行
　　　　validate_empty_valuesで入力データが抜けてないか確認する
　　　　to_internal_valueで入力データが型に適合しているか確認する
　↓
is_valid()でエラーがなかった場合だけ以下を実行
　　serializer.save()
　　　　Model.create()により新規レコードを登録する


### PUT, PATCH(update)の場合


クライアントがPUTリクエストでJSONを送る
　↓
httpリクエストの種類で呼ぶメソッドを決める
　↓
ViewSetのupdateメソッド内で以下を実行する
　　モデルから更新対象であるレコードを取得する
　↓
serializer = Serializer(data=request.data)でインスタンスを作成する
　　レコードを引数instanceとして渡すとupdateするようになる
serializer.dispatchでParserを選択、受け取ったRequestをParseする
　　ParserはリクエストのheaderのContent-Typeにより
　　JSONParser, FormParser, MultiPartParserから選択される
serializer.is_valid内でrun_validationメソッドを実行
　　run_validationで以下2つのメソッドでバリデーションを実行
　　　　validate_empty_valuesで入力データが抜けてないか確認する
　　　　to_internal_valueで入力データが型に適合しているか確認する
　↓
is_valid()でエラーがなかった場合だけ以下を実行
　　serializer.save()
　　　　Model.update()により対象レコードを更新する


## 4. ViewSetやSerializerのカスタマイズ


チュートリアルで登場するModelViewSetはコード量が最小にできる代わりカスタマイズ余地が少ないんだそうでViewSetを使うのが良さそうです。ViewSetの最小構成は以下のようになります。見て分かるようにメソッド内でModelやSerializerを操作するところを書けるようになっていますので、これを使って複数モデルへの読み書きなど、様々にカスタマイズができます


```python:ViewSet
class ArticleViewSet(ViewSet):
    def list(self, request):
        articles = Article.objects.all()
        serializer = ArticleSerializer(articles, many=True)
        return Response(serializer.data)

    def create(self, request):
        serializer = ArticleSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
```


Serializerについては書き込み時にto_internal_valueメソッドが、読み込み時にto_representationメソッドが呼ばれますので、必要に応じてここに処理を追加することでシリアライズするデータを加工することが出来ます


```python:Serializer
class ArticleSerializer(serializers.ModelSerializer):
    author_id = AuthorSerializer(read_only=True)
    author = serializers.PrimaryKeyRelatedField(queryset=Author.objects.all(), write_only=True)

    class Meta:
        model = Article
        fields = '__all__'

    def to_internal_value(self, data):
        data2 = data.copy()
        data2['author'] = data2.pop('author_id')
        return super().to_internal_value(data2)
```


## 5. 実際にカスタマイズしてみる


### 5-1. 実現したい動作


実際にカスタマイズして思ったように動くようにしましょう
例えば以下のようなリレーションのあるモデルを考えます


```python:models.py
class Author(models.Model):
  name = models.CharField(max_length=32)

class Article(models.Model):
  title = models.CharField(max_length=128)
  author_id = models.ForeignKey(Author, on_delete=models.DO_NOTHING)
```


GETとPOSTの際に以下のような入れ子構造のJSONでリレーションのあるモデルのそれぞれに登録したいとします


```json:GETで取得するJSON
{"title": "はらぺこあおむし",
 "author": {'name': 'エリック・カール'}}
```


```json:POSTで送信するJSON
{"title": "はらぺこあおむし",
 "author": {'name': 'エリック・カール'}}
```


JSONはGETとPOSTで同じ形にします


### 5-2. チュートリアルのやり方で実現できる動作


以下のようなSerializerにすることで、GET取得時に入れ子構造のJSONで受け取ることができます


```python.serializers.py
class ArticleSerializer(serializers.ModelSerializer):
    author_id = AuthorSerializer(read_only=True)
    author = serializers.PrimaryKeyRelatedField(queryset=Author.objects.all(), write_only=True)

    class Meta:
        model = Article
        fields = '__all__'
```


このときGETとPOSTでそれぞれ以下のようなJSONを取得、送信することになります。また、Authorは先に登録してpkを取得しておかないといけません


```json:GETで取得するJSON
{"title": "はらぺこあおむし",
 "author_id": {'name': 'エリック・カール'}}
```


```json:POSTで送信するJSON
{"title": "はらぺこあおむし",
 "author": 1}
```


### 5-3. 課題


上記では実現したいやり方とは以下の相違があります


<b> 1. ArticleとAuthorをまとめて登録できていない </b>
<b> 2. JSONのkeyが自由にできない </b>


### 5-4. カスタマイズで解決する


というのが以下で解決します
完成したのを見ると簡単に見えます


```python:views.py
class ArticleSerializer(serializers.ModelSerializer):
    author_id = AuthorSerializer(read_only=True)
    author = serializers.PrimaryKeyRelatedField(queryset=Author.objects.all(), write_only=True)

    class Meta:
        model = Article
        fields = '__all__'

    def to_internal_value(self, data):
        """
        serializerはwrite時にauthorというPrimaryKeyRelatedFieldを
        受け取るつもりでいるのでauthor_idをauthorに置き換えます
        """
        data2 = data.copy()
        data2['author'] = data2.pop('author_id')
        return super().to_internal_value(data2)
    

class ArticleViewSet(ViewSet):
    def list(self, request):
        """
        GET側の処理は元の方法で問題ないですが
        keyがauthor_idで出てきてしまうのでkeyだけ置き換えます
        """
        articles = Article.objects.all()
        serializer = ArticleSerializer(articles, many=True)
        l = [{key.replace('author_id', 'author'): d[key] for key in d.keys()} 
             for d in serializer.data]
        return Response(l)

    def create(self, request):
        """
        authorが未登録なら書き込み
        """
        data = request.data.copy()
        author_data = data.pop('author')
        author = Author.objects.get_or_create(**author_data)[0]
        data['author_id'] = author.pk

        """
        articleをシリアライズして書き込み
        """
        serializer = ArticleSerializer(data=data)
        if serializer.is_valid():
            d = {key: data[key] for key in data.keys()}
            d['author_id'] = author
            Article.objects.create(**d)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
```


### 5-5. 注意点


#### 適切なParserを使う


送ったJSONはまずParserで処理されてcreateメソッドやlistメソッドにrequestという引数で渡されます。ParserはデフォルトでJSONParser, FormParser, MultiPartParserの3つが用意されていて、httpリクエストheaderのContent-Typeにより選択されます。対応関係は以下のようになっています


<b>JSONParser: application/json </b>
<b>FormParser: application/x-www-form-urlencoded </b>
<b>MultiPartParser:  multipart/form-data </b>


上記のうち、JSONParserを使えば入れ子構造のJSONを正常に受け取ってくれますが、FormParserでは以下のように入れ子構造が壊れてしまいます


```python:FormParserにより作成されたrequest.data
{"title": ["はらぺこあおむし"],
 "author_id": ['name']}
```


Pythonのrequestsからheaderの指定を省略すると、Content-Typeがapplication/x-www-form-urlencodedになってしまう為、Parseが上手くいかなくなってしまうので動作テストする際に注意する必要があります


request.dataが適切に作成されているかは以下のようにprintなりlog書き出しなりを仕込んでやることで確認できます


```python:views.py
class ArticleViewSet(ViewSet):
    def create(self, request):
        print(request.data)  # ← 確認用の書き出し
        
        data = request.data.copy()
        author_data = data.pop('author')       
        author = Author.objects.get_or_create(**author_data)[0]
        data['author_id'] = author.pk

        serializer = ArticleSerializer(data=data)
        if serializer.is_valid():
            d = {key: data[key] for key in data.keys()}
            d['author_id'] = author
            Article.objects.create(**d)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
```


#### Field名とkeyが一致するようにする


serializer.is_valid()は__init__とclass Metaで定義したField名に対して動作するので、それと一致させる必要があり、Model.objects.create()するときにはモデルとField名が一致する必要があります。serializerはバリデーション前にto_internal_valueメソッドが実行されるのでその中でkeyを置き換えてやれば良いし、Model.objects.createを実行する前にViewSetのcreateメソッド内で処理できます。こちらもprintやlog書き出しで確認すればそれほど難しくないでしょう


## まとめ


いつもそうですけど、ソースコードを読むとなんとかなりますね