Help us understand the problem. What is going on with this article?

DjangoでAPI実装

はじめに

やあみんな!こんにちは、こんばんは、あるいはおやすみなさい!

どうも僕です。Takurintonです。

今回はDjangoでAPI作ってそれを叩くみたいなことします。
背景としては、弊学のとある科目でそんな感じのことしないと実装できない問題があったのでちょっくらやってやるかって感じです。
まあ無知の状態からだったのでだいぶキツかったです。自由課題なので完全に自爆なんですけどね( ;∀;)

前提条件

所要時間

全体の所要時間の合計としてはググる時間も含めて6時間くらいでした。目安までにどうぞ。

背景

もともと業務委託で作らせてもらっていたECサイトをまんま改造して作りました。でも更新してる最中のサイトをぶち壊してはいけないので、別で環境をDockerに作って、docker-composeで仮想環境をエイヤ!ってしてやろうかなとか思って簡単にですが作りました。
Dockerって使っても使わなくてもデータベースくらいしか変更点ないと思うので、そこら辺は省略します(細かいことは個人ブログでそのうち書きます)

環境

OS macOS Chatalina version 10.15.4
言語 Python, Java
フレームワーク Django REST framework

使用技術

Django REST framework全体のリンクはこちら

作る!!!

構成

もともとデータベースやらロジックはしっかりしてあったので、そこらへんの整備は必要ありませんでした。

今回作成するAPIでは注文情報を取得します。
イメージとして

  • ユーザーのメアド
  • 注文した店舗
  • 注文した商品の値段と写真のリスト
  • 合計金額
  • 希望お届け時間

あたりを取得したいのでそれに合わせて実装しました。

これに関連したデータベースは以下のようになっています。

models.py
class CustomUserManager(UserManager):
    use_in_migrations = True
    def _create_user(self, email, password=None, zip_code=None, address1=None, address2=None, address3=None,  **extra_fields):
        if not email:
            raise ValueError('メールは必須で被りなし')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        phone_number_regex = RegexValidator(regex=r'^[0-9]+$', message = ("Tel Number must be entered in the format: '09012345678'. Up to 15 digits allowed."))
        phone_number = models.CharField(validators=[phone_number_regex], max_length=15, verbose_name='電話番号')
        zip_code = models.CharField(max_length=8)
        address1 = models.CharField(max_length=40)
        address2 = models.CharField(max_length=40)
        address3 = models.CharField(max_length=40, blank=True)
        user.save(using=self._db)
        return email



    def create_user(self, request_data, **kwargs):
        if not request_data['email']:
            raise ValueError('Users must have an email address.')

        user = self.model(
            email=request_data['email'],
            first_name=request_data['first_name'], 
            last_name=request_data['last_name'], 
            # password=request_data['password'], 
            zip_code=request_data['zip_code'],
            address1=request_data['address1'], 
            address2=request_data['address2'], 
            address3=request_data['address3'], 
        )

        user.set_password(request_data['password'])
        user.save(using=self._db)
        return user

    def create_superuser(self, email, phone_number=None, password=None, zip_code=None, address1=None, address2=None, address3=None,  **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')
        return self._create_user(email, password, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
    #username = models.CharField(_('username'), max_length=20, unique=True)
    email = models.EmailField(_('email address'), unique=True)
    first_name = models.CharField(_('first name'), max_length=30)
    last_name = models.CharField(_('last name'), max_length=150)
    zip_code = models.CharField(max_length=8)
    address1 = models.CharField(max_length=40)
    address2 = models.CharField(max_length=40)
    address3 = models.CharField(max_length=40, blank=True)
    phone_number_regex = RegexValidator(regex=r'^[0-9]+$', message = ("Tel Number must be entered in the format: '09012345678'. Up to 15 digits allowed."))
    phone_number = models.CharField(validators=[phone_number_regex], max_length=15, verbose_name='電話番号', null=True, blank=True)

    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_(
            'Designates whether the user can log into this admin site.'),
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

    objects = CustomUserManager()
    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []


    def user_has_perm(self, user, perm, obj):
        return _user_has_perm(user, perm, obj)

    def has_perm(self, perm ,obj=None):
        return _user_has_perm(self, perm, obj=obj)

    def has_module_perms(self, app_label):
        return self.is_staff

    def get_short_name(self):
        return self.first_name

    class Meta:
        # db_table = 'api_user'
        swappable = 'AUTH_USER_MODEL'

class Company(models.Model):
    name = models.CharField(max_length=255)
    introduction = models.TextField(max_length=65536)
    postal_code = models.CharField(max_length=8)
    company_image = models.ImageField()
    homepage = models.CharField(max_length=255, null=True, blank=True)
    images = models.BooleanField(verbose_name='', default=False)
    place = models.CharField(max_length=255)

    def __str__(self):
        return str(self.name)


class Product(models.Model):
    company = models.ForeignKey(Company, on_delete=models.CASCADE)
    name = models.CharField(max_length=255)
    contents = models.CharField(max_length=255)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    product_image = models.ImageField()
    option = models.CharField(max_length=255, null=True, blank=True, default=None)
    price = models.IntegerField()
    def __str__(self):
        return str(self.name)

class Cart(models.Model):
    cart_id = models.IntegerField(null=True, blank=True)
    user = models.ForeignKey(User, on_delete=models.CASCADE, blank=True, null=True)
    is_active = models.BooleanField(default=True)
    pub_date = models.DateTimeField(null=True, blank=True)

    def __str__(self):
        return str(self.cart_id)



class UserInfomation(models.Model):
    cart = models.ForeignKey(Cart, on_delete=models.CASCADE, blank=True, null=True)
    user = models.ForeignKey(User, on_delete=models.CASCADE, blank=True, null=True)
    day = models.CharField(max_length=255, default=None)
    time = models.CharField(null=True, blank=True, max_length=255,  default=None)
    status = models.BooleanField(default=False, null=True, blank=True)
    total = models.IntegerField(null=True)
    remark = models.TextField(max_length=65535, null=True, blank=True)
    pub_date = models.DateTimeField(default=now)

    def __str__(self):
        return str(self.user)


class OrderItems(models.Model):
    user = models.ForeignKey(UserInfomation, on_delete=models.CASCADE)
    cart = models.ForeignKey(Cart, on_delete=models.CASCADE, blank=True, null=True)
    item = models.ForeignKey(Product, on_delete=models.CASCADE, blank=True, null=True)
    number = models.IntegerField(null=True)
    price = models.IntegerField(null=True)
    total = models.IntegerField(null=True)

  • Userテーブルはカスタムユーザーモデルを拡張しています
  • それぞれのパーミションを取得するための関数をいくつか定義しています
  • companyはproductと1対多になっています
  • userはcartと1対多になっていますが、アクティブなカートは1つだけになります
  • UserInfomationには確定した注文が入っています
  • OrderItemsにはUserの情報が入っています

他にもテーブルはあるのですが、今回はこれだけあればできるので省略します。

作っていく

準備

まずrestframeworkをインストールしていない方はインストールします。

pip install djangorestframework

次にsettings.pyに以下の設定を追加します。

settings.py
...  

INSTALLED_APPS = [
    ...
    'rest_framework',
]

...



JWT_AUTH = {
    'JWT_VERIFY_EXPIRATION': False, # tokenの永続化
    'JWT_AUTH_HEADER_PREFIX': 'JWT',
}


REST_FRAMEWORK = { 
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),  
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    ),  
    'NON_FIELD_ERRORS_KEY': 'detail',
    'TEST_REQUEST_DEFAULT_FORMAT': 'json', 
    'DEFAULT_FILTER_BACKENDS': (
        'django_filters.rest_framework.DjangoFilterBackend', 
    ), 
}



ログイン機能

ここでは既に存在するユーザーにログインする機能を作成します。
今回は全てのリクエストに認証をかけます。ログイン中のユーザーのみ操作を可能にするのでここは必須です。
まずはエンドポイントを作成します。

project_name/urls.py
from django.conf.urls import url

from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    url(r'^login_/', obtain_jwt_token),
    ..., 
]

このエンドポイントを追加することでログイン機能が完成します。
あくまでデータベースを構築している前提です。obtain_jwt_tokenというのは、このエンドポイントにアクセスした時に認証用のトークンを返してくれるようにします。

既存のユーザーでログインしてみたいと思います。
実験段階ではipythonを使っていきます(楽だから)

In [1]: import requests
In [2]: import json
In [3]: data = {'email': 'hogehoge@gmail.com', 'password': 'hogehoge'}
In [4]: r = requests.post('localhost:8000/login_/', data=data)
In [5]: print(r.json())
Out[5]: {'token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo5LCJ1c2VybmFtZSI6ImIxODA2NDI5QGdtYWlsLmNvbSIsImV4cCI6MTU5NDc5OTMwNCwiZW1haWwiOiJiMTgwNjQyOUBnbWFpbC5jb20ifQ.Torhy69ZyKMOOxQUUv3Ebn9V6wqSwUlsQUD5IPUaDJA'}

こんな感じで長めのJSONが返ってきます。
この時の注意点としては、Pythonには辞書をJSONに変換してくれるjson.dumps(dict)というようなものがありますが、それをやるとエラーになります。ここで渡すのはあくまでもdict型のデータになります。また、この返ってきたトークンは後々使うので大切にしておいてください。
ログイン時に使うデータとしては、主キーであるメールアドレスとパスワードになっています。なんだかわからない人はmodels.pyのコードを読んでみてください。

余談ですが、Pythonのrequestsではdataにbody、headersにヘッダーを入れることができます。
r.json()にすることでレスポンスをdict型に変換し、使いやすい形に変換することができます。
requestsとjsonはだいぶ密です。密です。密です。

カスタムJSON

次は自分用にカスタマイズしたJSONを返していきます。
個人的にはここでつまづきました。

最初はSerializerGeneric viewsを使って実装する方がいいと思っていたのですが、どうやらこれではカスタムのJSON(これらを使うと任意の単一テーブルに存在するフィールドのものしか返せない)を返すことができないようなので、やり方を変更しました。
今のやり方にたどり着くまでにだいぶ時間がかかってしまったのでそこが反省です。

カスタムのJSONを返したい場合にはシリアライザーなどはいらないらしいので、直接views.pyに記述していきます。
エンドポイントはurls.pyに記述します。

urls.py
from django.urls import path
from . import views


urlpatterns = [
...
path('get_shop_view', views.ShopView.as_view()), #これを追加
]
views.py
from django.http import HttpResponse
from rest_framework import generics

class ShopView(generics.ListCreateAPIView):
    # list(self, request, *args, **kwargs) を使うことでカスタムJSONを生成することができるようになる
    def list(self, request, *args, **kwargs):
        # 最終的に返すJSONのリスト
        return_list = list()
        try:
            user_info = UserInfomation.objects.all() # 購入経験のある全てのユーザーを取得
            for i in user_info:
                order = OrderItems.objects.filter(user=i) # ユーザーを指定
                shop = order[0].item.company.name # 会社を指定

                # 商品一覧を取得
                order_items = dict()
                for i in order:
                    order_items[i.item.name] = {
                                                'price': i.item.price, # 値段
                                                'images': str(i.item.product_image) # 画像のURL
                                                } 


                total = sum([i.total for i in order]) #合計金額。リスト内包で生成したリストの合計を出す

                date_time_ = UserInfomation.objects.get(cart=i.cart)
                date_time = date_time_.day + date_time_.time # 希望お届け時間を取得

                # 全体のリストにこれらのデータを入れる
                return_list.append(
                    {
                        'user': str(i.user),   # ユーザーのメアド
                        'shop': shop,          # 注文した店舗
                        'order': order_items,  # 注文した商品一覧
                        'total': total,        # 合計金額
                        'datetime': date_time, # 希望お届け時間
                    }
                )    
        # エラーが出たら空のリストを返す        
        except:
            pass

        return Response(
            return_list
        )

ここまで作成してから先ほど作成したエンドポイントに今度はgetリクエストを投げてみます

In [1]: {'Content-Type': 'application/json',
 'Authorization': 'JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo5LCJ1c2VybmFtZSI6ImIxODA2NDI5QGdtYWlsLmNvbSIsImV4cCI6MTU5NDc5OTMwNCwiZW1haWwiOiJiMTgwNjQyOUBnbWFpbC5jb20ifQ.Torhy69ZyKMOOxQUUv3Ebn9V6wqSwUlsQUD5IPUaDJA'}
In[2]: order_list = requests.get('http://localhost:8000/get_shop_view', headers=headers)
In [3]: order_list.json()
Out[3]:
[{'user': 'hogehoge@gmail.com',
  'shop': '店舗B',
  'order': {'ハンバーグ': {'price': 1000,
    'images': 'IMG_4145_ykxb9h'}},
  'total': 1000,
  'datetime': '2020年7月14日今すぐ'},
 {'user': 'hogehoge@gmail.com',
  'shop': '店舗B',
  'order': {'ハンバーグ': {'price': 1000,
    'images': 'IMG_4145_ykxb9h'},
   'カレー': {'price': 2,
    'images': '11967451898714_y6tgch'}},
  'total': 2002,
  'datetime': '2020年7月14日今すぐ'},
 {'user': 'fugafuga@gmail.com',
  'shop': '店舗B',
  'order': {'シチュー': {'price': 11111,
    'images': 'IMG_4900_oyb5ny'},
   'ハンバーグ': {'price': 1000,
    'images': 'IMG_4145_ykxb9h'},
   'コーヒー': {'price': 199,
    'images': '54490_jawqyl'},
   'カレー': {'price': 2,
    'images': '11967451898714_y6tgch'},
   'パンケーキ': {'price': 100,
    'images': 'tweet_p7chgi'},
   'Takurinton': {'price': 100,
    'images': 'npyl13'}},
  'total': 24220,
  'datetime': '2020年7月16日今すぐ'}]

これは僕が事前にカートにホイホイ突っ込んでおいたやつですが、無事に返ってきました!!!
嬉しい〜!!!ここまで5時間45分くらいかかった。。。

Javaで叩いてみる

レポートはJavaなのでJavaでも叩けるようになりたいなと思ってググってみたら意外と簡単でした。気難しい言語だと思ってたのでちょっと嬉しい。。。笑笑

Test.java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.nio.charset.StandardCharsets;

public class Test{
    public static void main(String[] args){
        try {
            HttpRequest request = HttpRequest
                    .newBuilder(URI.create("http://localhost:8000/get_shop_view"))
                    .header("Content-Type", "application/json")
                    .header("Authorization", "JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo5LCJ1c2VybmFtZSI6ImIxODA2NDI5QGdtYWlsLmNvbSIsImV4cCI6MTU5NDc5OTMwNCwiZW1haWwiOiJiMTgwNjQyOUBnbWFpbC5jb20ifQ.Torhy69ZyKMOOxQUUv3Ebn9V6wqSwUlsQUD5IPUaDJA")
                    .GET()
                    .build();

            BodyHandler<String> bodyHandler = HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8);
            HttpResponse<String> response = HttpClient.newBuilder().build().send(request, bodyHandler);

            String body = response.body();
            System.out.println(body);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

相変わらずコードは長いですが、許容範囲内です。
HttpRequestにじゃんじゃか書いていくスタンスみたいです。ヘッダーのトークンは先ほど取得したものを同じものを利用することができます。
なんかあまり深く理解できてないけど、、、。

コンパイルして実行します。

(base) Hogehoge:working takurinton$ javac Test.java
(base) Hogehoge:working takurinton$ java Test
[{'user': 'hogehoge@gmail.com',
  'shop': '店舗B',
  'order': {'ハンバーグ': {'price': 1000,
    'images': 'IMG_4145_ykxb9h'}},
  'total': 1000,
  'datetime': '2020年7月14日今すぐ'},
 {'user': 'hogehoge@gmail.com',
  'shop': '店舗B',
  'order': {'ハンバーグ': {'price': 1000,
    'images': 'IMG_4145_ykxb9h'},
   'カレー': {'price': 2,
    'images': '11967451898714_y6tgch'}},
  'total': 2002,
  'datetime': '2020年7月14日今すぐ'},
 {'user': 'fugafuga@gmail.com',
  'shop': '店舗B',
  'order': {'シチュー': {'price': 11111,
    'images': 'IMG_4900_oyb5ny'},
   'ハンバーグ': {'price': 1000,
    'images': 'IMG_4145_ykxb9h'},
   'コーヒー': {'price': 199,
    'images': '54490_jawqyl'},
   'カレー': {'price': 2,
    'images': '11967451898714_y6tgch'},
   'パンケーキ': {'price': 100,
    'images': 'tweet_p7chgi'},
   'Takurinton': {'price': 100,
    'images': 'npyl13'}},
  'total': 24220,
  'datetime': '2020年7月16日今すぐ'}]

無事返ってきます。ReactとかVueだとブラウザの関係でもっとめんどくさくなるのですが、Javaならコマンドラインで完結するので簡単でした。

まとめ

実はここに書いたもの意外にユーザー作成、ユーザーの情報取得、お店の情報取得、お店が持ってる商品一覧などなど、、、たくさん作ってあります。
ここら辺は1つのテーブルに依存する部分が多いので、カスタムよりもフィールドを指定して作る方が速かったりするので使い分けが重要ってことですね。
Java触ると疲れちゃうので今日はここら辺にしておきます。笑笑

また、6時間くらいで実装することができましたが、これをまだどんどんいじって行かないといけないので長い旅になりそうです。

コロナウイルスによるオンライン授業で課題ばっかり出て大変!テストはやるんかい!とか思ってる大学生の皆さん!一緒にコロナウイルスに負けないくらい勉強してこの状況を乗り越えましょう!あと2週間頑張るぞ〜!!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした