[DRF]結合テーブルの取得【N+1問題】
Django-rest-frameworkでAPIを作成します
前回までの基本編の記事はこちらです。
記事 |
---|
[DRF]apiの作成(patch) |
[DRF]apiの作成(post) |
[DRF]apiの作成(get)part2 |
[DRF]apiの作成(get)part1 |
この記事からは応用編ということで、外部キーなどで結合されたテーブルの操作をまとめて行こうかと思います。
model
今回からのmodelは以下のとおりです。
from django.db import models
DISTRICT_CATEGORIES = [
("1", "地区1"),
("2", "地区2"),
("3", "地区3"),
("4", "地区4"),
]
class Student(models.Model):
"""生徒情報"""
# 生徒ID
student_id = models.CharField(max_length=4, primary_key=True)
# クラス
class_no = models.CharField(max_length=1)
# 出席番号
attendance_no = models.IntegerField()
# 名前
name = models.CharField(max_length=20)
# 地区番号
district_no = models.CharField(max_length=1, choices=DISTRICT_CATEGORIES)
# フリーコメント
comment = models.CharField(max_length=200,blank=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["class_no", "attendance_no"],
name="class_attendance_unique"
),
]
class Exam(models.Model):
"""試験情報"""
# 国語
japanese_score = models.IntegerField(null=True, blank=True)
# 数学
math_score = models.IntegerField(null=True, blank=True)
# 英語
english_score = models.IntegerField(null=True, blank=True)
# 生徒ID
student = models.OneToOneField(Student, on_delete=models.CASCADE)
Studentに加えて、そのstudentごとの試験結果を格納するテーブルを作成し、OneToOneField
で接続させています。生徒と試験結果が1対1で対応していると考えて下さい。
model同士の関係性の定義はいろいろあります。
このようにstudentとexamが1対1で接続しているときはOneToOneField
を用いますし、1対多であれば外部キーということでForeignKey
、多対多の場合はManyToManyField
を用います。この場合、migration時にdjangoが中間テーブルを自動的に作成してくれます。
今回はOneToOneField
を用いていますね。この場合、ExamオブジェクトからはOneToOneField
として定義したstudent
に_id
をつけて、student_id
でstudentの主キーにアクセスすることができます。
また、studentからはexamで、examからはstudentで、それぞれのオブジェクトが参照できるようです。
参照
今回はこのmodelに対して特定の生徒の情報と試験の情報の両方を抜き出して参照できるAPIを作成します。
serializer
複数テーブルを返すserializerは以下のようになります。
exam, student両方のserializerを準備して、studentの方でexamのフィールドを定義してあげます。このようにするとstudentの中にexamをネストしてserializerに格納することが可能です。
from rest_framework import serializers
from ..models import Student, Exam
class ExamSerializer(serializers.ModelSerializer):
"""Examクラスのシリアライザ"""
class Meta:
model = Exam
fields=[
'japanese_score',
'math_score',
'english_score'
]
class StudentSerializer(serializers.ModelSerializer):
"""Studentクラスのシリアライザ"""
exam = ExamSerializer()
class Meta:
model = Student
fields = [
'student_id',
'class_no',
'attendance_no',
'name',
'district_no',
'comment',
'exam'
]
view
serializerに対する、getのviewは以下のとおりです。今回はpkで取得するviewとします。
特に基本編と変わらないviewを作成しています。
from rest_framework.views import APIView
from rest_framework.response import Response
from ..models import Student
from ..serializers import StudentSerializer
class StudentExamRetriveAPIView(APIView):
"""生徒情報、試験情報を取得"""
def get(self, request, pk, *args, **kwargs):
# pkからレコード取得
instance = Student.objects.get(pk=pk)
# serializer作成
serializer = StudentSerializer(instance)
# Response
return Response(serializer.data)
実行
URLや実行ファイルは割愛します。
実行結果は以下のとおりです。
{"student_id":"0001","class_no":"1","attendance_no":1,"name":"山田太郎","district_no":"4","comment":"にゃ〜","exam":{"japanese_score":100,"math_score":100,"english_score":90}}
studentの中にexamがネストされて出力されているのがわかりますね。
重要なのはStudent.objects.get(pk=pk)
でstudentのobjectだけとってるけど、onetooneで接続されているexamのレコードも一緒にとってきてくれるってところですね。
汎用APIView
汎用APIVewでもやります。ほぼ同じですけど。
views
importは省略してます。
class StudentExamRetriveAPIView(generics.RetrieveAPIView):
queryset = Student.objects.all()
serializer_class = StudentSerializer
{"student_id":"0001","class_no":"1","attendance_no":1,"name":"山田太郎","district_no":"4","comment":"にゃ〜","exam":{"japanese_score":100,"math_score":100,"english_score":90}}
終わりです・・・コード少ない。
N+1問題
DjangoやRailsでORMを使用していると良く見る問題です。
OneToOneフィールドやForeignKeyフィールドを用いたORMで注意が必要です。
例えば1対多のForeignKeyを使用している場合、最初にForeignKeyを定義しているmodelのレコードがN件取得され、その1件1件に対してForeignキーで外部キー接続しているテーブルの情報を読み出すので総じてSQLがN+1回流れてしまうという問題です。
今回の例ではOneToOneフィールドを用いていますが、仮にこれがStudent・・・1に対してexam・・・多のForeignKeyフィールドだとしましょう(試験は1回でなく中間テスト、期末テストなどあるはずです)。
そして例えば全件取得の処理を書いたとして、以下のようなイメージになるはずです。
まずはExamの全レコードを取得。そしてその後Examの各レコードのStudentから、一つ一つSQLを流して生徒情報を特定。
イメージはこんな感じです。
赤文字それぞれに関してSQLが一つひとつ流れてしまう感じです。
あまりよろしくない状況です。
仮の話を戻しますが、実際に現在OneToOneで定義しているときのgetのSQLは以下のように流れています。
(0.003) SELECT "students_student"."student_id", "students_student"."class_no", "students_student"."attendance_no", "students_student"."name", "students_student"."district_no", "students_student"."comment" FROM "students_student" WHERE "students_student"."student_id" = '0001' LIMIT 21; args=('0001',)
(0.007) SELECT "students_exam"."id", "students_exam"."japanese_score", "students_exam"."math_score", "students_exam"."english_score", "students_exam"."student_id" FROM "students_exam" WHERE "students_exam"."student_id" = '0001' LIMIT 21; args=('0001',)
しっかり2回流れていますね。
select_related()
対策として、select_relatedを紹介しておきます。
これをつけることで、SQL内でJoinをしてくれるようになります。
これを
# pkからレコード取得
instance = Student.objects.get(pk=pk)
このように書き換えました。
# pkからレコード取得
instance = Student.objects.select_related('exam').get(pk=pk)
流れるSQL
(0.002) SELECT "students_student"."student_id", "students_student"."class_no", "students_student"."attendance_no", "students_student"."name", "students_student"."district_no", "students_student"."comment", "students_exam"."id", "students_exam"."japanese_score", "students_exam"."math_score", "students_exam"."english_score", "students_exam"."student_id" FROM "students_student" LEFT OUTER JOIN "students_exam" ON ("students_student"."student_id" = "students_exam"."student_id") WHERE "students_student"."student_id" = '0001' LIMIT 21; args=('0001',)
JOINしてくれていますね。1つのSQLで済んでいそうです。
この辺の参考記事はいくつかあったので紹介しておきます。
podhmo's diaryさん
akiyokoさん
OneToOneFieldでもForeignKeyFieldでも使えれば使っていって良さそうだと思いました。ORMについてはしっかり理解したほうが良さそうですね。
では、今回はここまでとします。
Author And Source
この問題について([DRF]結合テーブルの取得【N+1問題】), 我々は、より多くの情報をここで見つけました https://qiita.com/aja_min/items/9d6d6cf2ac9495836f5f著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .