0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

DRF勉強(vol.2) バックエンド作成

Last updated at Posted at 2023-07-24

はじめに

DRF(Django REST framework)の勉強のため、以下書籍を参考にサンプル作成します。
最終的には書籍にあるVue+DRFの作成を目指します。

  1. 現場で使えるDjangoの教科書
  2. 現場で使えるDjango REST Frameworkの教科書

Vol.1ではバックエンドプロジェクトの作成と初期設定まで行いました。
Vol.2ではバックエンドのプログラムを作成していきます。

環境

  • Intel Mac 13.4.1(c)
  • Python 3.11.4
  • Django 4.2.3
  • DRF 3.14.0

前提

  • pyenvがインストールされていること
  • poetryがインストールされていること

モデル

モデルとは

  • Django ORMで利用するクラスのこと。
  • DBのテーブルとカラム定義とモデルクラスとクラス変数を対応させる形で作成する。
    • (通常)はDBの1つのテーブルに対して1つ作成する。
  • django.db.models.Modelを継承して作成する。
  • Meta.db_tableにテーブル名を指定する。
    • 未指定の場合は「[アプリケーション名]_[モデルクラス名(スネークケース)]」になる。
  • テーブルのカラムに対するフィールドをFieldクラスで定義する。
    • django.db.models.fields.Fieldのサブクラスを利用する。
    • DB上の制約や表示名、バリデーションはフィールドオプションで指定する。

作成するモデルのクラス定義

shopアプリケーションにBookモデルクラスを作成します。

shop/models.py
from django.db import models

import uuid


class Book(models.Model):
    class Meta:
        db_table = "book"

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    title = models.CharField(verbose_name="タイトル", max_length=20)
    price = models.IntegerField(verbose_name="価格", null=True, blank=True)
    created_at = models.DateTimeField(verbose_name="登録日時", auto_now_add=True)

    def __str__(self) -> str:
        return self.title
指定したフィールドオプション
オプション 説明
primary_key 主キーとなるフィールドの場合Trueを設定する
default レコード登録時に値が指定されなかった場合のデフォルト値
editable 管理画面、その他の画面に表示するか否か
verbose_name フィールド名
max_length 文字列の最大文字数、CharFieldの場合は指定必須
null Null許可
blank フォーム入力時に入力必須とするか否か、必須にしたい場合Falseを指定する
auto_now_add オブジェクトが最初に作成される場合に、現在日時を自動設定する

マイグレーション

作成したモデル定義をDBに適用します。

# マイグレーションファイルの作成
$ ./manage.py makemigrations shop

# DBに適用
$ ./manage.py migrate

REPLでモデルを試す

Python対話シェルを利用してBookモデルを試してみます。

❯ ./manage.py shell
>>> from shop.models import Book

# 全件取得
>>> Book.objects.all();
<QuerySet []>

# レコード登録
>>> book = Book(title='テスト', price=100)
>>> book.save()

# 登録したレコードの確認
>>> book.id
UUID('0723d0ee-3903-4b07-8c41-190f0b8bec9e')
>>> print(book)
テスト
>>> vars(book)
{'_state': <django.db.models.base.ModelState object at 0x109798c50>, 'id': UUID('0723d0ee-3903-4b07-8c41-190f0b8bec9e'), 'title': 'テスト', 'price': 100, 'created_at': datetime.datetime(2023, 7, 24, 1, 57, 24, 638531, tzinfo=datetime.timezone.utc)}

# 再度 全件取得
>>> Book.objects.all()
<QuerySet [<Book: テスト>]>

# 条件指定で検索
>>> Book.objects.filter(title='テスト')
<QuerySet [<Book: テスト>]>

# 1件取得
>>> Book.objects.filter(title='テスト').get()
<Book: テスト>

# 1件取得でレコードがない場合 → DoesNotExist例外が発生する
>>> book = Book.objects.filter(title='テスト2').get()
Traceback (most recent call last):
   ・・・
shop.models.Book.DoesNotExist: Book matching query does not exist.

シリアライザー

シリアライザーとは

  • APIのリクエスト/レスポンスとオブジェクトを相互変換するためのクラス。
  • シリアライザーのインスタンス作成時、「dataに入力データ」「instanceにDBから取得したモデルオブジェクト」を指定する。
  • シリアライズ、デシリアライズ、バリデーションを行う。
  • JSONの構造がモデルのフィールド定義がベースな場合、rest_framework.serializers.ModelSerializerを継承して作成する。

作成するシリアライザーのクラス定義

apiアプリケーションにBookモデル用のシリアライザークラスを作成します。

apiv1/serializers.py
from rest_framework import serializers

from shop.models import Book


class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ["id", "title", "price"]

REPLでシリアライザーを試す

Python対話シェルを利用してBook用シリアライザーを試してみます。

共通

以降の手順で共通となるモジュールインポート部分になります。

❯ ./manage.py shell
Python 3.11.4 (main, Jun 20 2023, 16:59:59) [Clang 14.0.3 (clang-1403.0.22.14.1)] on darwin
>>> from io import BytesIO
>>> from rest_framework.parsers import JSONParser
>>> from rest_framework.renderers import JSONRenderer
>>> from apiv1.serializers import BookSerializer
>>> from shop.models import Book

データ登録の場合

データ登録での リクエスト受信 → DBに保存 → レスポンス返却のイメージになります。

# リクエストデータ相当を作成
>>> input = JSONParser().parse(BytesIO('{"title":"テスト2","price":100}'.encode()))
>>> input
{'title': 'テスト2', 'price': 100}

# シリアライザーの作成
>>> serializer = BookSerializer(data=input)

# バリデーション
>>> serializer.is_valid()
True

# 検証済みデータの確認(`is_valid()`実行前だとAssertionErrorになる)
>>> serializer.validated_data
OrderedDict([('title', 'テスト2'), ('price', 100)])

# オブジェクトの生成(モデルの場合は永続化まで行われる)
>>> serializer.save()
<Book: テスト2>

# 生成したオブジェクトの取得(`save()`前は取得できない)
>>> serializer.instance
<Book: テスト2>

# レスポンス作成
>>> serializer.data
{'id': 'cc45c34e-d09b-4ab0-8b27-f92bd0427b2d', 'title': 'テスト2', 'price': 100}
>>> JSONRenderer().render(serializer.data)
b'{"id":"cc45c34e-d09b-4ab0-8b27-f92bd0427b2d","title":"\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x882","price":100}'

データ更新の場合

データ更新(PATCH)での リクエスト受信 → DBに保存 → レスポンス返却のイメージになります。

# リクエストデータ相当を作成
>>> input = JSONParser().parse(BytesIO('{"id":"cc45c34e-d09b-4ab0-8b27-f92bd0427b2d", "price":1000}'.encode()))
>>> input
{'id': 'cc45c34e-d09b-4ab0-8b27-f92bd0427b2d', 'price': 1000}


# モデル取得
>>> instance = Book.objects.get(pk="cc45c34e-d09b-4ab0-8b27-f92bd0427b2d")
>>> instance
<Book: テスト2>

# シリアライザーの作成 (リクエストの内容だけ更新したいので `partial=True`を指定)
>>> serializer = BookSerializer(instance=instance, data=input, partial=True)

# バリデーション
>>> serializer.is_valid()
True

# オブジェクトの生成(モデルの場合は永続化まで行われる)
>>> serializer.save()
<Book: テスト2>

# 生成したオブジェクトの取得
>>> serializer.instance
<Book: テスト2>
>>> serializer.instance.price
1000

# レスポンス作成
>>> JSONRenderer().render(serializer.data)
b'{"id":"cc45c34e-d09b-4ab0-8b27-f92bd0427b2d","title":"\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x882","price":1000}'

バリデーションエラーになる場合

# 不正なインプット(価格がの文字列)
>>> input = JSONParser().parse(BytesIO('{"title":"テスト2","price":"文字列"}'.encode()))

# バリデーション
>>> serializer = BookSerializer(data=input)
>>> serializer.is_valid()
False
>>> serializer.errors
{'price': [ErrorDetail(string='A valid integer is required.', code='invalid')]}

# バリデーションで例外をスロー
>>> serializer.is_valid(raise_exception=True)
Traceback (most recent call last):
   ・・・
rest_framework.exceptions.ValidationError: {'price': [ErrorDetail(string='A valid integer is required.', code='invalid')]}

ビュー

ビューとは

  • リクエストオブジェクトを受け取り、レスポンスオブジェクトを返却するクラス。
  • 以下のいずれかを継承して作成する。
    • rest_framework.viewsets.ModelViewSet(または ReadOnlyModelViewSet)
      • 利用シーン: 単一モデルのCRUD処理を実装する場合に利用。
    • rest_framework.generics.CreateAPIViewなどの「汎用APIView」
      • 利用シーン: 単一モデルの一部アクションのみを実装する場合に利用。
    • rest_framework.views.APIView
      • 利用シーン: モデルを扱わない、または 同時に複数のモデルを扱う場合などに利用。
ModelViewSet系ビューの対応アクション
クラス 一覧
GET
詳細
GET
登録
POST
更新
PUT
一部更新
PATCH
削除
DELETE
ModelViewSet
ReadOnlyModelViewSet - - - -

作成するビューのクラス定義

apiv1/views.py
from rest_framework import viewsets

from shop.models import Book
from .serializers import BookSerializer


class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

ルーティング設定の追加

作成したビューへのルーティングを設定します。

アプリケーションへのルート設定

apiv1/urls.pyを作成し、apiv1アプリケーションにルート設定を行います。

apivi/urls.py
from django.urls import path, include
from rest_framework import routers

from . import views

router = routers.DefaultRouter()
router.register("books", views.BookViewSet)

app_name = "apiv1"
urlpatterns = [path("", include(router.urls))]

プロジェクトへのルート設定

config/urls.pyを修正して、アプリケーションのルートを設定します。

config/urls.py
  from django.contrib import admin
- from django.urls import path
+ from django.urls import path, include
  
  urlpatterns = [
      path("admin/", admin.site.urls),
+     path("api/v1/", include("apiv1.urls")),
  ]

動作確認

開発サーバーを起動してAPIの動作確認をします。

開発サーバーの起動

$ ./manage.py runserver

curlでの動作確認

一覧取得
$ curl -i -H "Content-Type: application/json" \
    -X GET http://localhost:8000/api/v1/books/

HTTP/1.1 200 OK
Date: Mon, 24 Jul 2023 06:14:32 GMT
Server: WSGIServer/0.2 CPython/3.11.4
Content-Type: application/json
Vary: Accept, Cookie
Allow: GET, POST, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 167
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

[{"id":"0723d0ee-3903-4b07-8c41-190f0b8bec9e","title":"テストテスト","price":100},{"id":"d7cc2fa5-7d79-4d50-8220-5c97e305b1c5","title":"テスト2","price":100}]%
詳細取得
$ curl -i -H "Content-Type: application/json" \
    -X GET http://localhost:8000/api/v1/books/0723d0ee-3903-4b07-8c41-190f0b8bec9e/
    
HTTP/1.1 200 OK
Date: Mon, 24 Jul 2023 06:15:52 GMT
Server: WSGIServer/0.2 CPython/3.11.4
Content-Type: application/json
Vary: Accept, Cookie
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 86
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"id":"0723d0ee-3903-4b07-8c41-190f0b8bec9e","title":"テストテスト","price":100}%
詳細取得(対象レコードがない場合)
$ curl -i -H "Content-Type: application/json" \
    -X GET http://localhost:8000/api/v1/books/hogehoge/
    
HTTP/1.1 404 Not Found
Date: Mon, 24 Jul 2023 06:20:26 GMT
Server: WSGIServer/0.2 CPython/3.11.4
Content-Type: application/json
Vary: Accept, Cookie
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 23
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"detail":"Not found."}%
登録
$ curl -i -H "Content-Type: application/json" \
    -X POST http://localhost:8000/api/v1/books/ \
    -d '{"title":"テスト3", "price":300}'
    
HTTP/1.1 201 Created
Date: Mon, 24 Jul 2023 06:16:32 GMT
Server: WSGIServer/0.2 CPython/3.11.4
Content-Type: application/json
Vary: Accept, Cookie
Allow: GET, POST, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 78
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"id":"ae1bc53c-5194-4e8d-848d-479216dcf3fa","title":"テスト3","price":300}%
登録(バリデーションエラーの場合)
$ curl -i -H "Content-Type: application/json" \
    -X POST http://localhost:8000/api/v1/books/ \
    -d '{"title":"テスト3", "price":"hogehoge"}'
    
HTTP/1.1 400 Bad Request
Date: Mon, 24 Jul 2023 06:26:11 GMT
Server: WSGIServer/0.2 CPython/3.11.4
Content-Type: application/json
Vary: Accept, Cookie
Allow: GET, POST, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 42
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"price":["A valid integer is required."]}%
更新
$ curl -i -H "Content-Type: application/json" \
    -X PUT http://localhost:8000/api/v1/books/ae1bc53c-5194-4e8d-848d-479216dcf3fa/ \
    -d '{"title":"テスト3-2", "price":320}'
    
HTTP/1.1 200 OK
Date: Mon, 24 Jul 2023 06:17:34 GMT
Server: WSGIServer/0.2 CPython/3.11.4
Content-Type: application/json
Vary: Accept, Cookie
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 80
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"id":"ae1bc53c-5194-4e8d-848d-479216dcf3fa","title":"テスト3-2","price":320}%
更新(対象レコードがない場合)
$ curl -i -H "Content-Type: application/json" \
    -X PUT http://localhost:8000/api/v1/books/hogehoge/ \
    -d '{"title":"テスト3-2", "price":320}'
    
HTTP/1.1 404 Not Found
Date: Mon, 24 Jul 2023 06:23:00 GMT
Server: WSGIServer/0.2 CPython/3.11.4
Content-Type: application/json
Vary: Accept, Cookie
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 23
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"detail":"Not found."}%
一部更新
$ curl -i -H "Content-Type: application/json" \
    -X PATCH http://localhost:8000/api/v1/books/ae1bc53c-5194-4e8d-848d-479216dcf3fa/ \
    -d '{"price":350}'
    
HTTP/1.1 200 OK
Date: Mon, 24 Jul 2023 06:18:48 GMT
Server: WSGIServer/0.2 CPython/3.11.4
Content-Type: application/json
Vary: Accept, Cookie
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 80
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"id":"ae1bc53c-5194-4e8d-848d-479216dcf3fa","title":"テスト3-2","price":350}%
削除
$ curl -i -H "Content-Type: application/json" \
    -X DELETE http://localhost:8000/api/v1/books/ae1bc53c-5194-4e8d-848d-479216dcf3fa/
    
HTTP/1.1 204 No Content
Date: Mon, 24 Jul 2023 06:19:29 GMT
Server: WSGIServer/0.2 CPython/3.11.4
Vary: Accept, Cookie
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 0
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
削除(対象レコードがない場合)
$ curl -i -H "Content-Type: application/json" \
    -X DELETE http://localhost:8000/api/v1/books/hogehoge/
    
HTTP/1.1 404 Not Found
Date: Mon, 24 Jul 2023 06:23:19 GMT
Server: WSGIServer/0.2 CPython/3.11.4
Content-Type: application/json
Vary: Accept, Cookie
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 23
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"detail":"Not found."}%
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?