【Django】REST frameworkで検索エンジン的なAPIを作った件について【REST framework】

# はじめに
久々にアウトプットしたいと思ったので、業務に触りたい技術を絡めて勉強しました。

今回はDjangoのREST frameworkってやつです。既存の記事もあるんですけどサンプルをやってみたものが多かったので、もう少し応用よりな記事にしようと思います。

そこでありがちですけど、検索エンジンAPIを作ってみました。ちなみにRestfulAPIってことでフロント部分は別で作りましたが今回は載せません。

少しでもお役にたてれば幸いです。

===2019.12.26 追記===
Django/RESTflameworkに関する高いいね記事をリンクしておきます。
[Django REST Frameworkを使って爆速でAPIを実装する](https://qiita.com/kimihiro_n/items/86e0a9e619720e57ecd8)
[Django REST Framework の使い方メモ](https://qiita.com/__init__/items/f5a5a64a05541fcda713)
[Django REST framework カスタマイズ方法 - チュートリアルの補足](https://qiita.com/okoppe8/items/c58bb3faaf26c9e2f27f)

# 導入
## 実装環境
* macOS ver10.13
* python ver3.5.2

## 導入
以下を```pip install```で準備します。

* Django (ver2.0.7)
* djangorestframework (ver3.8.2)

## プロジェクト生成

```
django-admin startproject config
mv config searchAPI(プロジェクト名)

cd searchAPI/
python manage.py startapp search(アプリ名)
```

startprojectでできるファイル群が設定ファイルが多いのであえてconfigという名前で生成して一番外のフォルダ名変更しています。

python manage.py startapp でアプリケーション本体の型を生成します。
* admin.py： 管理画面へのテーブル登録
* apps.py： よくわからんw
* migrations： DBへのマイグレーションのときに生成されるファイルが可能される
* models.py： モデルを記述する
* views.py： コントローラーにあたるコードを記述

# 実装
## モデル
今回の検索エンジンAPIはあるデータベースのスキーマ・テーブル情報を検索する感じにしました。
そこでスキーマ情報とテーブル情報をそれぞれにモデルとしました。

```python:searchAPI/search/models.py
from django.db import models


class Schema_test(models.Model):
    schema_name = models.CharField(max_length=20)
    describe = models.TextField()
    term_of_use = models.TextField()

class Table_test(models.Model):
    schemaID = models.IntegerField()
    table_name = models.CharField(max_length=20)
    describe = models.TextField()
```

今回このAPIのデータベースはデフォルトのsqliteを使うのdb関連の設定はいじりません。

### 他のDBMSを使う場合
ちなみに他のDBMSを使いたい場合は以下のように設定を変更します。(PostgreSQLの場合)
config/settings.pyを以下のように変更します。

```python:config/settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'DB_name',
        'USER': 'User_name',
        'PASSWORD': 'Password',
        'HOST': 'DB_host',
        'PORT': '5432',
    }
}
```

pythonのPostgreSQL用のドライバとしてpsycopg2を使うので、pipで入れてください。
もしかしたら、psycopg2-binaryも必要になるかもなのでこれも入れるといいかもです。

* psycopg2 (ver2.7.5)
* psycopg2-binary (ver2.7.5)  

## マイグレーション
models.pyで記述したモデルをデータベースに登録します。その前にconfig/settings.pyに生成したアプリを記述しておきます。

```python:config/settimg.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'search',
    'rest_framework',
]
```

```:terminal
python manage.py makemigrations
python manage.py migrate
```
python manage.py makemigrationsでマイグレーションファイルを作ります。
python manage.py migrateでデータベースに登録されます。

このままではテーブルしか作れていないのレコードを登録します。
ここで使えるのはDjangoの管理サイトです。GUIベースでデータベースへのレコード登録を行えます。

```:terminal
python manage.py runserver
```

一度、アプリケーションを起動させます。デフォルトでポート8000番で起動するので、以下のアドレスにブラウザからアクセスします。

http://localhost:8000/admin/

そーすると以下の画面にアクセスできます。

<img width="1192" alt="スクリーンショット 2018-07-15 19.28.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/148317/10736ab8-b8b4-b5bc-b2f7-21e65263addb.png">

ただ、ユーザー登録が必要なので、ユーザーを作ります。

```:terminal
python manage.py createsuperuser
```

ユーザー名とパスワードを入力して再び先ほどのアドレスにアクセスして、ログインします。

<img width="972" alt="スクリーンショット 2018-07-15 19.33.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/148317/1affeee4-b50e-75ef-be7d-3a39d54a9b70.png">

スキーマに関する情報を登録したいと思ったら、Schema_testsをクリックします。

<img width="1409" alt="スクリーンショット 2018-07-15 19.34.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/148317/3ce06c8b-b979-a8f2-9f68-7507222821d7.png">

右上のADDボタンを押すと以下の画面が開き、先ほどモデルに記述したカラムが登録できるようになります。

<img width="1409" alt="スクリーンショット 2018-07-15 19.34.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/148317/09c5abd2-5ebe-9dff-ec36-f941ea88b6e0.png">

必要な情報を入力しSAVEをしてレコード登録が完了します。

ちなみにこのUIを日本語表記にする設定もあるので必要であれば調べてください。

これでモデルの構築、DBへの登録・操作が完了しました。

## シリアライザ
REST framworkで一番重要なのがDBとのシリアライズを担当する部分です。
search直下にserializers.pyというファイルを作ります。

```python:searchAPI/search/serializers.py
from rest_framework import serializersd
from search.models import Schema_test, Table_test

class Schema_testSerializer(serializers.ModelSerializer):
    class Meta:
        model = Schema_test
        fields = ('schema_name', 'describe', 'term_of_use')

class Table_testSerializer(serializers.ModelSerializer):
    class Meta:
        model = Table_test
        fields = ('table_name', 'describe')
```

このファイルでは出力形式を決めています。class Metaの部分でmodelにはインポートしたmodels.pyで記述したモデルを渡します。fieldsには出力するカラムを指定します。

イメージとしてはSQL文のselect ~ from テーブル名の ~ にあたる部分を指定する感じです。Table_testのschemaIDいらなかったので指定していません。

ここが実装しているとかなりありがたいところで、いちいちSQL文を書き換えたりしなくてもここのfieldsの指定カラムを編集するだけで簡単に変更できます。

## ビュー(コントローラ)
次にsearch/views.pyを編集します。MVCでいうコントローラにあたる部分かなと思っています。

```python:search/views.py
from rest_framework import viewsets
from .models import Schema_test, Table_test
from .serializers import Schema_testSerializer, Table_testSerializer


class Schema_testViewSet(viewsets.ModelViewSet):
    queryset = Schema_test.objects.all()
    serializer_class = Schema_testSerializer

class Table_testViewSet(viewsets.ModelViewSet):
    queryset = Table_test.objects.all()
    serializer_class = Table_testSerializer
```

models.pyとserializers.pyの両方から必要なモデルとシリアライザをインポートします。
querysetにはDjango専用のORMを使用してDBへの操作を記述します。

以下の記事がORMについてまとまっています。
[Django データベース操作 についてのまとめ](https://qiita.com/okoppe8/items/66a8747cf179a538355b)

ここまで書けば全件取得が可能になります。objects.all()が指定テーブル(schema_testやtable_test)からの全件を取得してくれます。ちなみに今回はviewsets.ModelViewSetを継承していますが、viewsets.Viewsetを継承して定義する方法もあるみたいです。

http://www.django-rest-framework.org/api-guide/viewsets/

また条件検索したい時ありますよね。
そんな時はdjango-filtersが使いやすいです。

* django-crispy-forms (ver1.7.0)
* django-filter (ver1.1.0)

上記を```pip install```してview.pyとserializers.pyに追記します。

```python:search/serializers.py
from rest_framework import serializers
from django_filters import rest_framework as filters
from search.models import Schema_test, Table_test

class Schema_testSerializer(serializers.ModelSerializer):
    class Meta:
        model = Schema_test
        fields = ('schema_name', 'describe', 'term_of_use')

class Table_testSerializer(serializers.ModelSerializer):
    class Meta:
        model = Table_test
        fields = ('table_name', 'describe')

class SearchSchema_testFilter(filters.FilterSet):
    schema_name = filters.CharFilter( lookup_expr='contains')

    class Meta:
        model = Schema_test
        fields = ('schema_name', 'describe', 'term_of_use')

class SearchTable_testFilter(filters.FilterSet):
    table_name = filters.CharFilter( lookup_expr='contains')

    class Meta:
        model = Table_test
        fields = ('id', 'table_name')
```

追加したclassが条件検索を可能にする部分になります。lookup_expr='contains'は部分一致を表します。以下がわかりやすくまとまっている記事になります。
[Django REST framework で django-filter を使う](https://qiita.com/okoppe8/items/77f7f91f6878e3f324cc)

ここのfieldsは条件検索で使いたいカラムを指定します。

views.pyも編集します。

```python:search/views.py
from rest_framework import viewsets
from .models import Schema_test, Table_test
from .serializers import Schema_testSerializer, Table_testSerializer, SearchSchema_testFilter, SearchTable_testFilter


class Schema_testViewSet(viewsets.ModelViewSet):
    queryset = Schema_test.objects.all()
    serializer_class = Schema_testSerializer

class Table_testViewSet(viewsets.ModelViewSet):
    queryset = Table_test.objects.all()
    serializer_class = Table_testSerializer

class SearchSchema_testViewSet(viewsets.ModelViewSet):
    queryset = Schema_test.objects.all()
    serializer_class = Schema_testSerializer
    filter_class = SearchSchema_testFilter

class SearchTable_testViewSet(viewsets.ModelViewSet):
    queryset = Table_test.objects.all()
    serializer_class = Table_testSerializer
    filter_class = SearchTable_testFilter
```

追記した部分がserializers.pyで定義したdjango-filterを使ったフィルターになります。
serializer_classはシリアライザを使い出力形式を決定し、filter_classで条件検索を設定します。

さらにconfig/settings.pyに記述します。

```python:config/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'search',
    'rest_framework',
    'django_filters',
    'crispy_forms',
]

・・・

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',)
}

```

REST_FRAMEWORKはデフォルトにはないので追加してください。


## ルーティング
最後にルーティングを設定します。

ここでは2つのファイルを編集します。1つ目がconfig/urls.pyです。
このファイルでは全体のルーティングを設定します。

```python:config/urls.py
from django.conf.urls import url, include
from django.contrib import admin

from search.urls import router as search_router
from search import views

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^search/', include(search_router.urls)),
]
```

adminは管理サイト用のパスなのでそのままにしてurl(r'^search/', include(search_router.urls))を追記します。

url関数の第一引数はhttp://localhost:8000/ に続くパス名を指定します(ローカルで起動した場合)。第二引数ではinclude関数を使ってsearch/urls.pyでrouterに登録したパスにつながるように設定できます。


次にsearch直下につながるパスをルーティングするsearch/urls.pyです。このファイルはデフォルトで生成されていないので作ります。


```python:search/urls.py
from rest_framework import routers
from .views import Schema_testViewSet, Table_testViewSet, SearchSchema_testViewSet, SearchTable_testViewSet


router = routers.DefaultRouter()
router.register(r'all_schemas', Schema_testViewSet)
router.register(r'all_tables', Table_testViewSet)
router.register(r'schema', SearchSchema_testViewSet)
router.register(r'table', SearchTable_testViewSet)
urlpatterns = router.urls
```

views.pyから必要なViewSetをインポートします。register関数の第一引数にはhttp://localhost:8000/search/~ の ~ にあたる部分を記述します。そのパスに対してどんな操作をするかをインポートしたViewSetを設定します。

## 確認
設定したルーティングと挙動があっているかを確認します。

```:terminal
python manage.py runserver
```

アプリケーションを起動して、http://localhost:8000/search/ にアクセスします。すると、以下の画面にアクセスできると思います。

<img width="1274" alt="スクリーンショット 2018-07-16 0.19.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/148317/d845c350-24cd-c5c6-6989-fb300e774660.png">

この画面はみてわかる通り、このアプリに登録したルテーィングの一覧を表示しています。
all_schemaをクリックしてみます。このパスはobjects.all()のDB操作を設定しているので全件取得になります。

<img width="1194" alt="スクリーンショット 2018-07-16 0.29.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/148317/4f4f60d8-ad8d-e4a1-6fc8-3e582df54c69.png">

次にschemaをクリックします。

<img width="1182" alt="スクリーンショット 2018-07-16 0.32.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/148317/9142daac-7b9e-d85f-da98-1988dfc763f6.png">

右上にあるFilterをクリックしてフィルタリングを設定することができます。
schema_nameにjapaを入力してフィルタリングしてみると以下のようになります。

<img width="661" alt="スクリーンショット 2018-07-16 0.34.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/148317/9c5f02c8-0301-504b-e182-7257f0008c31.png">

<img width="1192" alt="スクリーンショット 2018-07-16 0.34.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/148317/bf00d225-4728-ba1f-b42a-94ce283b8933.png">

注目してほしいのがフィルタリング後のURLです。GETの場合はURLクエリにこのように入力すると下の画面の出力が得られます。UIにはこのように反映されていますが、実際にcurlなどでたたくとjsonで返ってきます。

# Access-Control-Allow-Originに関するエラー

こんな感じで一通りRESTfullなAPIを作れると思います。
後、自分がつまづいたところでフロントのjavascriptからリクエストを飛ばして、ちゃんと返ってるはずなのに「Access-Control-Allow-Originがheaderにない」のようなエラーが返ってきました。その時の対処法が以下になります。

config/settings.pyを編集します。

```python:config/settings.py
ALLOWED_HOSTS = ['*']


# Application definition

INSTALLED_APPS = [
    ・・・
    'corsheaders',
]


MIDDLEWARE = [
    ・・・
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.BrokenLinkEmailsMiddleware',
]

CORS_ORIGIN_ALLOW_ALL = True
```

INSTALLED_APPSに'corsheaders'を追加するので、```pip install django-cors-headers```してください。

多分、これでレスポンスがきっちり返ると思います。

## おわりに
一通り型を覚えるとかなり手軽にAPIを作れると思います。
できるだけ複雑な処理をしないようにORMの挙動とテーブル構成を決めるといいかなと思います。

djangoでAPIを作る手助けになれば嬉しいです。