9
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

DBに格納したデータを返すwebAPIをDjangoとSQLiteで開発する

概要

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

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

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

参考

環境

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を使う場合は編集不要です。念のため初期設定から変更されていないかだけ確認をしました。
check-settingpy-for-sqlite.png

プロジェクト作成時にデフォルトでdb.sqlite3 が生成されています。
check-sqlite-explorer.png

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

モデルを作成する

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

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

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

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

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サーバにブラウザでアクセスして格納されたデータを表示する際の、表示ルールの定義 になります。記述がなくてもモデルとして機能しますが、管理をし易くするために定義しておいた方が良いと思います。

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


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

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

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

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

マイグレーションファイルが作成されました。
make-migration-file-coupon.png

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

$ python manage.py migrate

マイグレートが成功し、恐らくSQLiteにcouponテーブルが作成されました。
migrate-coupon-01.png

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

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

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

  • Username
  • Email address
  • Password
$ python manage.py createsuperuser

make-superuser-of-django-mask.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にアクセスすると管理ツールのログイン画面が開きます。
django-webconsole-login-s.png

先に作成した管理者でログインすると、利用可能なテーブル(モデル)が表示されます。(下側にCouponアプリケーションで作成したCouponテーブルが表示されています。Couponsと表示されていますが、「s」は管理ツールが勝手に付けて表示しているので気にする必要はありません)
django-webconsole-home-s.png

Couponテーブルを選択すると、テーブルの中身を確認できます。初期状態ではデータが空なので何も表示されません。
django-webconsole-coupon-home-s.png

右上の 「ADD COUPON +」 ボタンを押すとデータ入力画面に進みます。ここで、モデルで定義した全てのモデルフィールドが表示されているので、テーブルが問題なく作成されている事を確認できます。
django-webconsole-coupon-add-s.png

データを投入して右下の 「SAVE」 ボタンを押すとデータが登録されます。
django-webconsole-after-input-s.png

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

一覧表示を見ると各行のデータは models.py のcouponクラスの __str__(self) で定義したフォーマットで表示されています。
django-webconsole-coupon-records-s.png

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

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

まずviews.pymodels.pyのCouponクラスをインポートします。views.pyfrom .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番目を指定して、一番先頭のレコードのクーポン情報を辞書型に整形する処理を書きました。


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でレスポンスされています。
get-one-record-from-sqlite.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でレスポンスされるようになりました。
get-all-couponData-from-sqlite-s.png

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

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

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

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

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


from django.db.models import Q

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


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

の部分に条件分岐を加えて下記のようにします。Qオプジェクトを使って上記で述べたように リクエスト店舗と同じクーポン全店になっているクーポンの OR条件の条件式を設定しています。


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='全店')

となります。

試しにブラウザで神田店を指定してリクエストすると、神田店と全店のクーポン情報が表示されます。
get-couponData-canuse-kanda-s.png

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

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

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

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

利用期限: deadline
ステータス: status

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

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


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

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


today = datetime.date.today()

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


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)がレスポンスされなくなっています。
get-all-couponData-with-deadline-filter-s.png

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


    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のクーポンがレスポンスに含まれなくなっています。
get-all-couponData-with-status-filter-s.png

以上です。

次は、このAPIにリクエストするiOSアプリ側の改造をします。

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
9
Help us understand the problem. What are the problem?