Python Django 入門トップページ


Django REST Framework による API 開発

  1. API とプロジェクトの概要
  2. Django REST Frameworkのインストール
  3. プロジェクトの作成
  4. 基本設定
  5. アプリケーションの作成と登録
  6. モデルの作成とマイグレーション
  7. テストデータの設定
  8. コメント一覧を出力する API
  9. コメント詳細情報を出力する API
  10. 新規投稿と更新・削除の API
  11. ページネーション
  12. 入力内容の検証(バリデーション)
  13. テストの自動化
  14. Shell でのユーザ登録
  15. Fixtures でのユーザ登録
  16. ユーザ認証を必須にする
  17. コメントにオーナ情報を追加
  18. 権限の設定:Case #1
  19. 権限の設定:Case #2
  20. Python から API への接続
  21. React アプリケーションからの API 接続と CORS エラーの回避

Django REST Framework による API 開発

コメントにオーナ情報を追加

ここではコメントに投稿したユーザの情報,すなわちオーナ情報を追加します.まず,models.py を編集して,User モデルとのリレーションシップを設定します.なお,リレーションシップの詳細はこちらを参照してください.

comments\models.py
from django.db import models
from django.contrib.auth.models import User

# Create your models here.

class Comment(models.Model):
    title = models.CharField(max_length=200)
    body = models.CharField(max_length=1000)
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-updated_at']

comments_comment テーブルに ower_id のフィールドを追加することになるので,データベースとマイグレーション関連のファイルをディレクトリごと一旦削除します.SQLite や Web サーバを別のプロンプト(ターミナル)で起動中であれば,あらかじめ終了ししてから,次のコマンドを実行してください.なお,すでに本番環境で運用中のデータベースにフィールドを追加する場合はリセットできないので,ここの手順を参照してください.

...\django_comment_api>del db.sqlite3 ⏎

...\django_comment_api>rmdir /S comments\migrations ⏎
comments\migrations、よろしいですか (Y/N)? y ⏎

...\django_comment_api>

続いてマイグレーションファイルの生成を行ったのち,マイグレーションファイルを実行してデータベースにテーブルを再生成します.

...\django_comment_api>python manage.py showmigrations ⏎
admin
 [ ] 0001_initial
 [ ] 0002_logentry_remove_auto_add
 [ ] 0003_logentry_add_action_flag_choices
auth
 [ ] 0001_initial
 [ ] 0002_alter_permission_name_max_length
 [ ] 0003_alter_user_email_max_length
 [ ] 0004_alter_user_username_opts
 [ ] 0005_alter_user_last_login_null
 [ ] 0006_require_contenttypes_0002
 [ ] 0007_alter_validators_add_error_messages
 [ ] 0008_alter_user_username_max_length
 [ ] 0009_alter_user_last_name_max_length
 [ ] 0010_alter_group_name_max_length
 [ ] 0011_update_proxy_permissions
 [ ] 0012_alter_user_first_name_max_length
contenttypes
 [ ] 0001_initial
 [ ] 0002_remove_content_type_name
sessions
 [ ] 0001_initial

...\django_comment_api>python manage.py makemigrations comments ⏎
Migrations for 'comments':
  comments\migrations\0001_initial.py
    - Create model Comment

...\django_comment_api>python manage.py showmigrations ⏎
admin
 [ ] 0001_initial
 [ ] 0002_logentry_remove_auto_add
 [ ] 0003_logentry_add_action_flag_choices
auth
 [ ] 0001_initial
 [ ] 0002_alter_permission_name_max_length
 [ ] 0003_alter_user_email_max_length
 [ ] 0004_alter_user_username_opts
 [ ] 0005_alter_user_last_login_null
 [ ] 0006_require_contenttypes_0002
 [ ] 0007_alter_validators_add_error_messages
 [ ] 0008_alter_user_username_max_length
 [ ] 0009_alter_user_last_name_max_length
 [ ] 0010_alter_group_name_max_length
 [ ] 0011_update_proxy_permissions
 [ ] 0012_alter_user_first_name_max_length
comments
 [ ] 0001_initial
contenttypes
 [ ] 0001_initial
 [ ] 0002_remove_content_type_name
sessions
 [ ] 0001_initial

...\django_comment_api>python manage.py migrate ⏎
Operations to perform:
  Apply all migrations: admin, auth, comments, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying comments.0001_initial... OK
  Applying sessions.0001_initial... OK

...\django_comment_api>python manage.py showmigrations ⏎
admin
 [X] 0001_initial
 [X] 0002_logentry_remove_auto_add
 [X] 0003_logentry_add_action_flag_choices
auth
 [X] 0001_initial
 [X] 0002_alter_permission_name_max_length
 [X] 0003_alter_user_email_max_length
 [X] 0004_alter_user_username_opts
 [X] 0005_alter_user_last_login_null
 [X] 0006_require_contenttypes_0002
 [X] 0007_alter_validators_add_error_messages
 [X] 0008_alter_user_username_max_length
 [X] 0009_alter_user_last_name_max_length
 [X] 0010_alter_group_name_max_length
 [X] 0011_update_proxy_permissions
 [X] 0012_alter_user_first_name_max_length
comments
 [X] 0001_initial
contenttypes
 [X] 0001_initial
 [X] 0002_remove_content_type_name
sessions
 [X] 0001_initial

...\django_comment_api>

SQLite を操作して生成されたテーブルの定義を確認します.すると,auth_user テーブルの主キーを参照する外部キー owner_id が追加されたことがわかりました.

sqlite> .tables ⏎
auth_group                  comments_comment
auth_group_permissions      django_admin_log
auth_permission             django_content_type
auth_user                   django_migrations
auth_user_groups            django_session
auth_user_user_permissions
sqlite> .schema comments_comment ⏎
CREATE TABLE IF NOT EXISTS "comments_comment" (
  "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
  "title" varchar(200) NOT NULL,
  "body" varchar(1000) NOT NULL,
  "created_at" datetime NOT NULL,
  "updated_at" datetime NOT NULL,
  "owner_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED
);
CREATE INDEX "comments_comment_owner_id_26de7a7f" ON "comments_comment" ("owner_id");
sqlite>

さらに JSON ファイルを生成して,登録するデータに owner_id の情報を追加します.

comments\fixtures\comments-data.json
[
    {
        "model": "comments.comment",
        "fields": {
            "title": "最初のコメント",
            "body": "コメントの本文",
            "owner_id": 1,
            "created_at": "2023-11-23T11:01:00.000",
            "updated_at": "2023-11-23T11:01:00.000"
        }
    },
    {
        "model": "comments.comment",
        "fields": {
            "title": "2個目のコメント",
            "body": "コメントの本文2",
            "owner_id": 2,
            "created_at": "2023-11-23T11:02:00.000",
            "updated_at": "2023-11-23T11:02:00.000"
        }
    },
    {
        "model": "comments.comment",
        "fields": {
            "title": "<3個目>のコメント",
            "body": "<h1>コメントの本文3</h1>",
            "owner_id": 1,
            "created_at": "2023-11-23T11:03:00.000",
            "updated_at": "2023-11-23T11:03:00.000"
        }
    },
    {
        "model": "comments.comment",
        "fields": {
            "title": "4個目のコメント",
            "body": "コメントの本文4",
            "owner_id": 2,
            "created_at": "2023-11-23T11:04:00.000",
            "updated_at": "2023-11-23T11:04:00.000"
        }
    },
    {
        "model": "comments.comment",
        "fields": {
            "title": "5個目のコメント",
            "body": "コメントの本文5",
            "owner_id": 1,
            "created_at": "2023-11-23T11:05:00.000",
            "updated_at": "2023-11-23T11:05:00.000"
        }
    },
    {
        "model": "comments.comment",
        "fields": {
            "title": "6個目のコメント",
            "body": "コメントの本文6",
            "owner_id": 2,
            "created_at": "2023-11-23T11:06:00.000",
            "updated_at": "2023-11-23T11:06:00.000"
        }
    },
    {
        "model": "comments.comment",
        "fields": {
            "title": "7個目のコメント",
            "body": "コメントの本文7",
            "owner_id": 1,
            "created_at": "2023-11-23T11:07:00.000",
            "updated_at": "2023-11-23T11:07:00.000"
        }
    },
    {
        "model": "comments.comment",
        "fields": {
            "title": "8個目のコメント",
            "body": "コメントの本文8",
            "owner_id": 2,
            "created_at": "2023-11-23T11:08:00.000",
            "updated_at": "2023-11-23T11:08:00.000"
        }
    },
    {
        "model": "comments.comment",
        "fields": {
            "title": "9個目のコメント",
            "body": "コメントの本文9",
            "owner_id": 1,
            "created_at": "2023-11-23T11:09:00.000",
            "updated_at": "2023-11-23T11:20:00.000"
        }
    },
    {
        "model": "comments.comment",
        "fields": {
            "title": "10個目のコメント",
            "body": "コメントの本文10",
            "owner_id": 2,
            "created_at": "2023-11-23T11:10:00.000",
            "updated_at": "2023-11-23T11:10:00.000"
        }
    }
]

すでにリセット済みのデータベースにデータを登録します.このときユーザのデータを先に登録しなければならないことに注意してください.この理由は順序を間違えるとリレーションシップの参照整合性に違反するからです.つまり,コメントを先に登録しようとしたときに,owner_id の参照ができないためにエラーが発生します.

...\django_comment_api>python manage.py loaddata comments\fixtures\user-data.json ⏎
Installed 3 object(s) from 1 fixture(s)

...\django_comment_api>python manage.py loaddata comments\fixtures\comments-data.json ⏎
Installed 10 object(s) from 1 fixture(s)

...\django_comment_api>

SQLite を操作して,二つのテーブルにデータが登録されていることを確認します.さらに内部結合 (INNER JOIN) ができることも確認しておきます.

sqlite> select id, username, email from auth_user; ⏎
id|username|email
1|user_a|a@sample.com
2|user_b|b@sample.com
3|user_c|c@sample.com
sqlite> select * from comments_comment; ⏎
id|title|body|created_at|updated_at|owner_id
1|最初のコメント|コメントの本文|2023-11-23 11:01:00|2023-11-23 11:01:00|1
2|2個目のコメント|コメントの本文2|2023-11-23 11:02:00|2023-11-23 11:02:00|2
3|<3個目>のコメント|<h1>コメントの本文3</h1>|2023-11-23 11:03:00|2023-11-23 11:03:00|1
4|4個目のコメント|コメントの本文4|2023-11-23 11:04:00|2023-11-23 11:04:00|2
5|5個目のコメント|コメントの本文5|2023-11-23 11:05:00|2023-11-23 11:05:00|1
6|6個目のコメント|コメントの本文6|2023-11-23 11:06:00|2023-11-23 11:06:00|2
7|7個目のコメント|コメントの本文7|2023-11-23 11:07:00|2023-11-23 11:07:00|1
8|8個目のコメント|コメントの本文8|2023-11-23 11:08:00|2023-11-23 11:08:00|2
9|9個目のコメント|コメントの本文9|2023-11-23 11:09:00|2023-11-23 11:20:00|1
10|10個目のコメント|コメントの本文10|2023-11-23 11:10:00|2023-11-23 11:10:00|2
sqlite> SELECT
   ...>   comments_comment.id,
   ...>   comments_comment.title,
   ...>   auth_user.username
   ...> FROM
   ...>   comments_comment
   ...> INNER JOIN
   ...>   auth_user
   ...> ON
   ...>   comments_comment.owner_id = auth_user.id; ⏎
id|title|username
1|最初のコメント|user_a
2|2個目のコメント|user_b
3|<3個目>のコメント|user_a
4|4個目のコメント|user_b
5|5個目のコメント|user_a
6|6個目のコメント|user_b
7|7個目のコメント|user_a
8|8個目のコメント|user_b
9|9個目のコメント|user_a
10|10個目のコメント|user_b
sqlite>

さらに API からの出力結果に owner_id を表示したいので,serializers.py を編集します.

comments\serializers.py
from .models import Comment
from rest_framework import serializers

class CommentSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Comment
        fields = ['id', 'owner_id', 'title', 'body', 'updated_at']

    def validate_title(self, value):
        if len(value) > 10:
            raise serializers.ValidationError("タイトルの最大文字数は10文字です")
        return value

    def validate_body(self, value):
        if len(value) > 15:
            raise serializers.ValidationError("本文の最大文字数は15文字です")
        return value

実際に curl コマンドで確認します.GET リクエストでは owner_id が正しく出力されていることがわかります.

...\django_comment_api>curl -u user_a:password http://127.0.0.1:8000/comments/ ⏎
{"count":10,"next":"http://127.0.0.1:8000/comments/?page=2","previous":null,"results":[{"id":9,"owner_id":1,"title":"9個目のコメント","body":"コメントの本文9","updated_at":"2023-11-23T11:20:00"},{"id":10,"owner_id":2,"title":"10個目のコメント","body":"コメントの本文10","updated_at":"2023-11-23T11:10:00"}]}
...\django_comment_api>curl -u user_a:password http://127.0.0.1:8000/comments/1/ ⏎
{"id":1,"owner_id":1,"title":"最初のコメント","body":"コメントの本文","updated_at":"2023-11-23T11:01:00"}
...\django_comment_api>

さらに,新規投稿時に投稿者の ID を記録する必要がるので,views.py に perform_create 関数を追加します.

comments/views.py
from django.shortcuts import render
from rest_framework import generics
from rest_framework import permissions
from .models import Comment
from .serializers import CommentSerializer
from .paginations import LargeResultsSetPagination

# Create your views here.

class CommentList(generics.ListCreateAPIView):
    queryset = Comment.objects.all()
    serializer_class = CommentSerializer
    pagination_class = LargeResultsSetPagination
    permission_classes = [permissions.IsAuthenticated]

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

class CommentDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Comment.objects.all()
    serializer_class = CommentSerializer
    permission_classes = [permissions.IsAuthenticated]

次に,POST リクエストを検証します.user_a と user_b のアカウントを使って順番に投稿したところ,owner_id が正しく設定されていることもわかりました.

...\django_comment_api>curl -X POST -d "title=owner" -d "body=store owner_id" -u user_a:password http://127.0.0.1:8000/comments/ ⏎
{"id":11,"owner_id":1,"title":"owner","body":"store owner_id","updated_at":"2023-11-23T17:28:40.544771"}
...\django_comment_api>curl -X POST -d "title=owner" -d "body=store owner_id" -u user_b:password http://127.0.0.1:8000/comments/ ⏎
{"id":12,"owner_id":2,"title":"owner","body":"store owner_id","updated_at":"2023-11-23T17:28:50.507987"}
...\django_comment_api>curl -u user_a:password http://127.0.0.1:8000/comments/ ⏎
{"count":12,"next":"http://127.0.0.1:8000/comments/?page=2","previous":null,"results":[{"id":12,"owner_id":2,"title":"owner","body":"store owner_id","updated_at":"2023-11-23T17:28:50.507987"},{"id":11,"owner_id":1,"title":"owner","body":"store owner_id","updated_at":"2023-11-23T17:28:40.544771"}]}
...\django_comment_api>

SQLite を使って投稿されたコメントに正しい owner_id が設定されていることを確認します.

sqlite> select id, title, owner_id from comments_comment;
id|title|owner_id
1|最初のコメント|1
2|2個目のコメント|2
3|<3個目>のコメント|1
4|4個目のコメント|2
5|5個目のコメント|1
6|6個目のコメント|2
7|7個目のコメント|1
8|8個目のコメント|2
9|9個目のコメント|1
10|10個目のコメント|2
11|owner|1
12|owner|2
sqlite>

Web ブラウザでも確認すると良いでしょう.

django-api-2023-23

これで投稿時に owner_id を設定することができるようになりました.しかしながら,現時点では他のユーザが投稿したコメントを更新・削除したりできてしまうという状況で,多くの現実的なシステムではこれは問題になるでしょう.次のページから2種類のケースを想定した設定を行います.

目次に戻る