Django REST frameworkのFilterで検索する【完全一致、部分一致、前方一致】

個人でWebサービスを開発していてDjango REST frameworkを初めて使っていますが、日本語情報少ないですよね...。

後世のDjango REST frameworkユーザーのために、はまったとこと実装するために調べてわかったことをメモしていくことにします。

今回は、Django REST frameworkのFilterで検索する時に使えるTips。

## 事前の準備

すでにDjango REST frameworkが動くようになっている状態で、本・ユーザー・コメント（本とユーザーを外部キー荷持つ）テーブルを持つモデルを想定してAPIで検索していきます。

公式ドキュメント読むなり、ググるなりしてDjango REST frameworkが動くようにしておく。
https://www.django-rest-framework.org/

<br>
以下のバージョンで動作させました。

- Python 3.7.3
- django-filter 2.1.0
- djangorestframework 3.9.2


## 検索対象のデータ

今回は、本のタイトルで検索していく。検索対象のデータはこちら。

<img width="1381" alt="スクリーンショット 2019-05-18 15.16.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253272/3feac6b2-f333-f3f2-284a-826329eab2b2.png">

（DjangoのAdmin画面ほぼコード書かずに生成されて超便利。）

## 元々の状態のコード（完全一致検索）

最低限の記述をしたurls.py、models.py、views.py、serializers.pyを書いておきます。

```urls.py
from django.contrib import admin
from django.urls import include, path
from django.views.generic import TemplateView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('app.urls')),
]
```

```app/urls.py
from django.urls import path, include

from .views import *

urlpatterns = [
    path('bookcomments/', BookCommentListView.as_view()),
]
```

```models.py
# coding=utf-8
from django.db import models


class User(models.Model):
    auth_user = models.ForeignKey(AuthUser, on_delete=models.CASCADE, null=True)
    user_name = models.CharField(max_length=128)
    account_name = models.CharField(max_length=128, unique=True, db_index=True)
    description = models.TextField(max_length=128, null=True)
    profile_image_link = models.CharField(max_length=128, unique=True, null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'user'


class Book(models.Model):
    title = models.CharField(max_length=255, db_index=True)
    book_status_choices = [
        ['PRE', 'Pre-Sales'],
        ['PUB', 'Published'],
        ['OOP', 'Out Of Print'],
    ]
    book_status = models.CharField(choices=book_status_choices,
                   default='PUB',
                   max_length=10)
    sale_date = models.DateField(null=True)
    pages_count = models.IntegerField()
    offer_price = models.IntegerField(null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'book'


class BookComment(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    comment_text = models.TextField(max_length=10000)
    comment_date = models.DateField(auto_now=True)
    tweet_flag = models.BooleanField(default=False)
    delete_flag = models.BooleanField(null=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'book_comment'
```

```views.py
from rest_framework import generics

from .serializers import *


class BookCommentListView(generics.ListCreateAPIView):
    queryset = BookComment.objects.all()
    serializer_class = BookCommentWithForeignSerializer

    def get_queryset(self):
        queryset = BookComment.objects.all()
        title = self.request.query_params.get('title', None)
        if title is not None:
            queryset = queryset.filter(book__title=title)
        return queryset
```

検索条件を変えたい場合はview.pyをいじる。

`title = self.request.query_params.get('title', None)` で、クエリパラメータでtitleが指定されていれば、それをもとに検索するようになる。

BOOK_COMMENTテーブルに紐づいているBOOKテーブルのTITLEカラムを検索する。

```serializers.py
from rest_framework import serializers
from .models import *


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'user_name', 'account_name', 'description', 'profile_image_link')


class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ('id', 'title', 'book_status', 'sale_date', 'pages_count', 'offer_price',)


class BookCommentWithForeignSerializer(serializers.ModelSerializer):
    book = BookSerializer()
    user = UserSerializer()

    class Meta:
        model = BookComment
        fields = ('id', 'user', 'book', 'comment_text', 'comment_date', 'tweet_flag', 'delete_flag',)
```

この状態でAPIを叩くと、完全一致する結果が表示される。

<img width="1440" alt="スクリーンショット 2019-05-18 15.14.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253272/afa7d827-27d4-2229-69b5-bdfc77c792a4.png">



## 部分一致検索

部分一致させたければ、「icontains」を使う。

```views.py
from rest_framework import generics

from .serializers import *


class BookCommentListView(generics.ListCreateAPIView):
    queryset = BookComment.objects.all()
    serializer_class = BookCommentWithForeignSerializer

    def get_queryset(self):
        queryset = BookComment.objects.all()
        title = self.request.query_params.get('title', None)
        if title is not None:
            queryset = queryset.filter(book__title__icontains=title)  # 「__icontains」を追加する
        return queryset
```

「?title=React」で検索する。

<img width="1440" alt="スクリーンショット 2019-05-18 15.25.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253272/bdd9e5db-3b78-ad20-4da9-05fd2ff95c79.png">

「React」がタイトルに含まれるコメントが一覧で取得できていますね。

## 前方一致検索

```views.py
...
queryset = queryset.filter(book__title__istartswith=title)  # 「__istartswith」に変更する
...
```

「?title=作りながら学ぶ」で検索する。

<img width="1440" alt="スクリーンショット 2019-05-18 15.30.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253272/7f9dce68-e5fa-acea-4902-96701357bbf3.png">

タイトルが「作りながら学ぶ」で始まるコメントが一覧で取得できていますね。

<br>
実装方法わかれば簡単ですね。

以上。

## 参考にした
- Django REST frameworkのGithub
<br>https://github.com/encode/django-rest-framework
- Django REST frameworkのGithubのfilter
<br>https://github.com/encode/django-rest-framework/blob/master/rest_framework/filters.py