DBに格納したデータを返すwebAPIをDjangoとSQLiteで開発する

# 概要

　この記事は初心者の自分がRESTfulなAPIとswiftでiPhone向けのクーポン配信サービスを開発した手順を順番に記事にしています。技術要素を1つずつ調べながら実装したため、とても遠回りな実装となっております。

　前回の [APIで取得したデータをswiftのTableViewに表示する](https://qiita.com/Ajyarimochi/items/3f56eb2ed474310506fc)までで、クーポン情報を配信するwebAPIと、そのwebAPIを利用してクーポンをユーザに届けるiPhoneアプリが"超"必要最低限レベルで出来上がりました。ここからwebAPIとアプリの実装方式や機能、UIを改善していきます。

　今回はクーポンのデータをハードコーディングしているのを、データベースで管理するように改造します。それに合わせてAPIのリクエストとレスポンスの仕様も再整理します。データベースはPythonに最初から組み込まれているSQLiteを使います。

###  参考

* Python Django 超入門　掌田津耶乃 著　秀和システム
* [Python Django入門 (6)
Python](https://qiita.com/kaki_k/items/b76acaeab8a9d935c35c)
* [Django データベース操作 についてのまとめ](https://qiita.com/okoppe8/items/66a8747cf179a538355b)
* [Djangoのクエリセットから特定の値を取り出す](http://wpress.biz/django/2017/02/22/django%E3%81%AE%E3%82%AF%E3%82%A8%E3%83%AA%E3%82%BB%E3%83%83%E3%83%88%E3%81%8B%E3%82%89%E7%89%B9%E5%AE%9A%E3%81%AE%E5%80%A4%E3%82%92%E5%8F%96%E3%82%8A%E5%87%BA%E3%81%99/)
* [Djangoで、OR検索](https://narito.ninja/blog/detail/91/)

### 環境
Mac OS 10.15
VSCode 1.39.2
pipenv 2018.11.26
Python 3.7.4
Django 2.2.6



# 手順

* webAPIのリクエストとレスポンスの仕様を整理する
* データベースの設定をする
* モデルを作成する
* マイグレーションしてモデルをもとにテーブルを自動生成
* テーブルにデータを投入する
* SQLiteに登録したクーポン1つの情報をレスポンスするように改造
* 複数のクーポン情報をレスポンスするように改造
* リクエストパラメータの条件に合うクーポン全ての情報をレスポンスするように改造
* 利用期限やステータスを満たすクーポンのみレスポンスするように改造

### webAPIのリクエストとレスポンスの仕様を整理する

 データベースで管理するデータを決める必要があるので、APIのリクエストとレスポンスの仕様を検討します。題材のクーポン配信APIでは下記の仕様としました。

**リクエストパラメータ**

* 利用可能店舗　（但し、指定なしでもリクエスト出来る様にする）

**レスポンス**

* クーポンコード
* クーポン特典
* コメント
* 利用可能店舗
* 利用開始日
* 利用期限
* ステータス（利用可能/不可のフラグ）

テーブル名は`Coupon`にします。

リクエストパラメータで利用可能店舗を受け取った場合は、指定の店舗で利用出来るクーポンだけを返し。リクエストパラメータが空の場合は現在利用可能なクーポンを全て返します。

利用可能なクーポンの定義は下記の通りとします。

* 利用開始日を過ぎていること
* 利用期限を過ぎていないこと
* ステータスが利用可能になっていること


### データベースの設定をする

　データベースの設定はプロジェクト名のディレクトリ（この記事の例だと`/ami_coupon_api`）配下にある `settings.py` に記載されています。

　スクリーンショットのように基本的に最初からSQLite を使う場合の設定がされているので、SQLiteを使う場合は編集不要です。念のため初期設定から変更されていないかだけ確認をしました。
<img width="818" alt="check-settingpy-for-sqlite.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/92b5ea99-8a67-18bf-aad6-9629c5ee13be.png">

プロジェクト作成時にデフォルトでdb.sqlite3 が生成されています。
<img width="536" alt="check-sqlite-explorer.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/fef21230-c24e-2c38-4f3e-9eea0f781b03.png">


なお、DjangoはSQLiteの他に MySQLとPostgreSQL に対応しています。


### モデルを作成する

　Djangoではモデルを元にデータベースとテーブルを自動で作成してくれるので、自分の手でテーブルを作る必要がありません。元となるモデルを作成します。モデル（モデルクラス）はテーブルと1:1で紐づき、モデルフィールドとテーブルの項目（インスタンス）も1:1で紐づくのでイメージが簡単です。

　今回必要なのはクーポン情報を格納するCouponテーブルなので、アプリケーションのディレクトリ（この記事の例だと`/coupon`）配下の`models.py`を開き、そこへCouponクラスを作成していきます。

モデルは下記のように定義していきます。
`[モデルフィールド名] = models.[データ型]`


作成したモデルはこちらです。

```python:models.py

from django.db mport models

class Coupon(models.Model):
    code = models.CharField(max_length=20) # クーポンコード
    benefit = models.CharField(max_length=1000) # クーポン特典
    explanation = models.CharField(max_length=2000) # コメント
    store = models.CharField(max_length=1000) # 利用可能店舗
    start = models.DateField() # 利用開始日
    deadline = models.DateField() # 利用期限
    status = models.BooleanField() # ステータス（利用可能/不可のフラグ）

    def __str__(self):
        return '<Coupon:id=' + str(self.id) + ',' + self.code + ',' + self.benefit + '>'
```

　上記のコードの`def __str__(self):〜`の部分は、Djangoサーバにブラウザでアクセスして格納されたデータを表示する際の、**表示ルールの定義** になります。記述がなくてもモデルとして機能しますが、管理をし易くするために定義しておいた方が良いと思います。

　下記のコードの場合、 ブラウザで管理画面にアクセスした際に、各クーポンデータの `id`、`code`、`benefit`が表示される設定です。`id`は主キーを設定しなかった場合に自動で生成されるようです。

```python

    def __str__(self):
        return '<Coupon:id=' + str(self.id) + ',' + self.code + ',' + self.benefit + '>'
```


### マイグレーションしてモデルを元にテーブルを自動生成

　ターミナルでpipenvのシェルに入り、プロジェクトのディレクトリ（manage.pyがあるディレクトリ）に移動します。そこでマイグレーションファイルを作成するコマンドを実行します。

```
$ python manage.py makemigrations [アプリケーション名（この記事の例だと Coupon）]
```

マイグレーションファイルが作成されました。
<img width="422" alt="make-migration-file-coupon.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/2570ff1b-be2e-df3a-bee6-90a950cc9244.png">

続いてマイグレーションを実行します。

```
$ python manage.py migrate
```

マイグレートが成功し、恐らくSQLiteにcouponテーブルが作成されました。
<img width="487" alt="migrate-coupon-01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/6261fa97-2021-ba6c-6f12-81ec2f1d484c.png">



### テーブルにデータを投入する

　djangoに標準で用意されているデータベースの管理ツールを使い、恐らく作成されたと思われるcouponテーブルの確認と、クーポンのデモデータを投入します。管理ツールはdjangoのサーバで動くWebアプリなので、ツールを使う時はdjangoのサーバを起動しておく必要があります。

　管理ツールにログインする前に、コマンドを使って管理者を登録しておく必要があります。設定が必要なのは下記の4項目です。コマンドを入力すると順次入力を求められます。

* Username
* Email address
* Password

```
$ python manage.py createsuperuser
```
<img width="402" alt="make-superuser-of-django-mask.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/1a95e6c4-35c6-a4b2-d7ba-9748b9fb005c.png">



 次にCouponモデルを管理ツールで管理できるように登録します。登録はアプリケーション（この記事の例だと Coupon）のディレクトリ配下の`admin.py` に行います。下記の通り`admin.py`を修正します。

* `models.py`のCouponクラスをインポートします。
* `admin.site.register`メソッドを呼び出し、の引数にモデル名を指定します。

```admin.py

from django.contrib import admin
from .models import Coupon # 追加

# Register your models here.
admin.site.register(Coupon) # 追加
```


django のサーバを起動して、`http://10.0.0.127:8000/admin`にアクセスすると管理ツールのログイン画面が開きます。
<img width="500" alt="django-webconsole-login-s.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/8b5485d5-ac0a-fdba-5f3e-cb9ac7eca109.png">



先に作成した管理者でログインすると、利用可能なテーブル（モデル）が表示されます。（下側にCouponアプリケーションで作成したCouponテーブルが表示されています。Couponsと表示されていますが、「s」は管理ツールが勝手に付けて表示しているので気にする必要はありません）
<img width="500" alt="django-webconsole-home-s.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/ce0d582e-45ef-1e14-8b1c-0dd508d5c8dd.png">


Couponテーブルを選択すると、テーブルの中身を確認できます。初期状態ではデータが空なので何も表示されません。
<img width="500" alt="django-webconsole-coupon-home-s.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/4be10367-9547-4ed5-d0f8-8f6c9db6d19f.png">


右上の **「ADD COUPON　＋」** ボタンを押すとデータ入力画面に進みます。ここで、モデルで定義した全てのモデルフィールドが表示されているので、テーブルが問題なく作成されている事を確認できます。
<img width="500" alt="django-webconsole-coupon-add-s.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/6323ea03-1a62-c0e7-c88c-8d6291b0bcf3.png">


データを投入して右下の **「SAVE」** ボタンを押すとデータが登録されます。
<img width="500" alt="django-webconsole-after-input-s.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/dc6a7790-a2cf-8d37-bea6-c424ccca5afb.png">


直感的な操作が可能かと思います。同じ要領で5つのクーポンデータを投入します。利用可能店舗や利用開始日、終了日、ステータスは後で試験が出来る様にバリエーションを持たせて投入しました。

一覧表示を見ると各行のデータは `models.py` のcouponクラスの `__str__(self)` で定義したフォーマットで表示されています。
<img width="500" alt="django-webconsole-coupon-records-s.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/397bb6ec-45b2-4ade-dc95-6d9d2bee6891.png">



### SQLiteに登録したクーポン1つの情報をJSONでレスポンスするように改造

 `views.py` を改造して、SQLiteに登録したクーポンのうち1つの情報をjson形式でレスポンスするように改造します。

まず`views.py`に `models.py`のCouponクラスをインポートします。`views.py`に`from .models import Coupon`を追加します。

次に、リクエストに対するレスポンス処理を定義します。 ここではCouponテーブルの全てのデータを取得する処理を追加します。
`data = Coupon.object.all()`


次に`Json.dump`するための辞書型のデータを作ります。

'Coupon.object.all()'で取得できるデータはdata型の配列になっており、配列の1つ1つにテーブル1行のレコードが入っています。1行のレコードを取り出すには、`data型変数の配列の[x]番目`を指定します。レコード中の項目を取り出すには、`data[x]後に.[取得したい項目のカラム名]`とします。

更に、data型のままでは`json.dumps`が受け付けてくれないため、String型にキャストします。

配列の0番目を指定して、一番先頭のレコードのクーポン情報を辞書型に整形する処理を書きました。

```python

params = {
            'coupon_code':str(data[0].code),
            'coupon_benefits':str(data[0].benefit),
            'coupon_explanation':str(data[0].explanation),
            'coupon_store':str(data[0].store),
            'coupon_start':str(data[0].start),
            'coupon_deadline':str(data[0].deadline),
            'coupon_status':str(data[0].status),
         }
```

`json.dumps`の処理から先は修正前と同じです。修正後の`views.py`は下記になります。

```views.py

from django.shortcuts import render
from django.http import HttpResponse
from .models import Coupon # Couponクラスをインポート
import json

def coupon(request):
    data = Coupon.objects.all() # テーブルにある全てのレコードを取得
    params = {
            'coupon_code':str(data[0].code), # 個々のレコードはdata型なのでString型にキャストする
            'coupon_benefits':str(data[0].benefit),
            'coupon_explanation':str(data[0].explanation),
            'coupon_store':str(data[0].store),
            'coupon_start':str(data[0].start),
            'coupon_deadline':str(data[0].deadline),
            'coupon_status':str(data[0].status),
        }
    json_str = json.dumps(params, ensure_ascii=False, indent=2)
    return HttpResponse(json_str)
```

ブラウザでリクエストURLを入力してjsonがレスポンスされるか確認すると、クーポン1つの情報がjsonでレスポンスされています。
<img width="794" alt="get-one-record-from-sqlite.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/e00ade85-5fe7-84d5-9a9f-d5313d6eaf09.png">



### 複数のクーポン情報をレスポンスするように改造

 APIで実現したいのは**リクエストパラメータで受けた条件に該当するクーポンを全てレスポンスする仕様**なので、まずは複数のクーポン情報をjsonでレスポンス出来るように改造します。

複数レコードをjsonでレスポンスするには、辞書型にした1レコードの情報を配列に格納し、それを`json.dumps`に渡せば良いです。

まずは、複数のレコードを辞書型として格納するための配列を用意します。配列名は`coupons`としました。


次に、レコードを辞書型にする処理をレコードの数だけ繰り返すようにfor文を追加します。

`for record in data:`とし、一周ごとにレコード1行が辞書型変数（record）に格納されるようにします。この処理を更にfor文で囲み、一周ごとに1レコードずつ辞書型に変換されたクーポン情報がcouponsの配列へ格納されるようにします。

最後に`json.dumps`に辞書型を渡していたのを、辞書型が複数格納された配列couponsを渡すように変更します。修正した`views.py`はこちらです。

```views.py

from django.shortcuts import render
from django.http import HttpResponse
from .models import Coupon # Couponクラスをインポート
import json

def coupon(request):
    data = Coupon.objects.all() # テーブルにある全てのレコードを取得

    coupons = [] # 複数のレコードを辞書型として格納するための配列を用意
    for record in data: # for文を使い1レコードずつ辞書型に変換
        params = {
            'coupon_code':str(record.code), # 個々のレコードはdata型なのでString型にキャストする
            'coupon_benefits':str(record.benefit),
            'coupon_explanation':str(record.explanation),
            'coupon_store':str(record.store),
            'coupon_start':str(record.start),
            'coupon_deadline':str(record.deadline),
            'coupon_status':str(record.status),
            }
        coupons.append(params) # 辞書型にしたレコードを配列に格納

    json_str = json.dumps(coupons, ensure_ascii=False, indent=2) # 辞書型にした複数のレコードを格納した配列を渡す
    return HttpResponse(json_str)
```

ブラウザでリクエストパラメータを指定せずにリクエストをすると、全てのクーポン情報がJsonでレスポンスされるようになりました。
<img width="500" alt="get-all-couponData-from-sqlite-s.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/76061e3b-7e06-9076-dfc4-0e3a64c5ae7f.png">




### リクエストパラメータの条件に合うクーポンをレスポンスするように改造

 特定の条件に合うレコードを取得するには、`Coupon.object.filter（[条件式])`を使います。

　リクエストパラメータは利用可能店舗（Couponテーブルのstoreに紐づく項目）なので、`if else`文を使い、パラーメータ有りの場合は `Coupon.objects.filter`で指定店舗と全店で利用可能なクーポンを取得し、パラメータ無しの場合は`Coupon.objects.all`で全てのクーポンを取得すれば良い。

なお、条件式は 
**Couponテーブルの利用可能店舗がリクエストされた店舗と同じ　OR　利用可能店舗が全店**
となります。

複数条件の式はQオブジェクトを使うと書きやすいですので、'views.py'にQオプジェクトをインポートします。

```python

from django.db.models import Q
```

次に、ここまでに作ったプログラムの、

```python

data = Coupon.objects.all() # テーブルにある全てのレコードを取得`
```
の部分に条件分岐を加えて下記のようにします。Qオプジェクトを使って上記で述べたように **リクエスト店舗と同じクーポン**と**全店になっているクーポン**の OR条件の条件式を設定しています。

```python

if 'coupon_store' in request.GET: 
    coupon_store = request.GET['coupon_store'] 
    data = Coupon.objects.filter(Q(store=coupon_store) | Q(store='全店'))
else:
    data = Coupon.objects.all()
```


Qオブジェクトを使った複数条件の記述方法について、

```
OR検索は
Q(store=coupon_store) | Q(store='全店')

AND検索は
Q(store=coupon_store) , Q(store='全店')
```
となります。


試しにブラウザで神田店を指定してリクエストすると、神田店と全店のクーポン情報が表示されます。
<img width="500" alt="get-couponData-canuse-kanda-s.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/f8544329-19e9-33b9-7ded-0b9c6ade997b.png">


### 利用期限やステータスを満たすクーポンのみレスポンスするように改造する。

 ここまで実装したプログラムをベースに、**利用期限を過ぎておらず且つステータスが「利用可能」**なクーポンのみレスポンスするように、検索条件を加えます。

　なお利用開始日については、予告としてクーポンを配信しておきたいというニーズを考え、検索条件に加えない事にしました。

利用期限とステータスに紐づくCouponテーブルのモデルフィールドの項目はそれぞれ下記の通りです。

```
利用期限： deadline
ステータス： status
```

 まずリクエストを受けた日付（年月日）とクーポンの利用期限を比較して、利用期限を過ぎていないものだけをCouponテーブルから取得するように改造します。

日付を取得できるように`views.py`に`datetime`をインポートします。

```python

import datetime # 日時を取得出来るようにdatetimeをインポート
```

日付を取得する処理を、coupon関数の中に追加します。

```python

today = datetime.date.today()
```

**クーポンの利用期限が、上で取得した日付より後かどうか** を判定するクエリー`Q(deadline__gte=today)`を`Coupon.object.filter`に追加します。

```python

if 'coupon_store' in request.GET: # リクエストパラメータで店舗を指定された場合の処理
        coupon_store = request.GET['coupon_store']
        data = Coupon.objects.filter(Q(deadline__gte=today),Q(store=coupon_store) | Q(store='全店')) # リクエストされた店舗と全店で使えるクーポンを取得
    else: # リクエストパラメータが無い場合は全てのクーポンを返す
        data = Coupon.objects.filter(Q(deadline__gte=today))
```


これで利用期限が過ぎていないものをレスポンスする処理ができました。リクエストパラメータを付けずにリクエストすると、利用期限が切れているクーポン（0004）がレスポンスされなくなっています。
<img width="500" alt="get-all-couponData-with-deadline-filter-s.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/0d451abd-9dcb-c31e-02d1-5095968fd639.png">



次に、statusが利用可能（True）のクーポンだけCouponテーブルから取得するように、`Coupon.object.filter`に`Q(status=True)`クエリを追加します。

```python

    if 'coupon_store' in request.GET: # リクエストパラメータで店舗を指定された場合の処理
        coupon_store = request.GET['coupon_store']

        data = Coupon.objects.filter(Q(deadline__gte=today),Q(status=True),Q(store=coupon_store) | Q(store='全店')) # リクエストされた店舗と全店で使えるクーポンを取得
    else: # リクエストパラメータが無い場合は全てのクーポンを返す
        data = Coupon.objects.filter(Q(deadline__gte=today),Q(status=True))
```

リクエストすると、statusがFalseのクーポンがレスポンスに含まれなくなっています。
<img width="500" alt="get-all-couponData-with-status-filter-s.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/c84783bd-da58-c457-508f-20bcc84b19a4.png">


以上です。

次は、[このAPIにリクエストするiOSアプリ側の改造](https://qiita.com/Ajyarimochi/items/50cdc57f898b79cfb48e)をします。