カテゴリー
アーカイブ

03.26
2025

[Django] DB設計:リレーションの定義とSQLクエリを理解する(後編)

  • LINE

前回の記事(前編)ではForeignKeyについて見ていきました。

本記事(後編)では、OneToOne、ManyToManyを使ったリレーションの定義方法を見ていきます。

 

一対一のリレーション:OneToOneを使う

一対一のリレーションを定義するOneToOneFieldについて見ていきます。

このリレーションは、例えば「ユーザーとプロフィール」の関係のように、1人のユーザーが1つのプロフィールを持つような場合に使用します。

モデルの定義

class User(models.Model):
    username = models.CharField(max_length=100)

    class Meta:
        db_table = "user"

    def __str__(self):
        return self.username

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField()

    class Meta:
        db_table = "profile"

    def __str__(self):
        return f"Profile (user={self.user_id})"

ここでは、ProfileモデルがUserモデルと一対一のリレーションを持つようにOneToOneFieldを使用しています。この設定により、ユーザーごとに1つのプロフィールが関連付けられます。

なお、OneToOneFieldは付加情報側(未登録でも良い方)に定義します。

生成されるDDL

このモデル設計に基づいたテーブル作成のDDLを確認していきます。

$ python manage.py makemigrations
Migrations for 'exampleapp':
  exampleapp/migrations/0002_user_profile.py
    - Create model User
    - Create model Profile
$ python manage.py sqlmigrate exampleapp 0002
BEGIN;

--
-- Create model User
--
CREATE TABLE "user" (
    "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    "username" varchar(100) NOT NULL
);

--
-- Create model Profile
--
CREATE TABLE "profile" (
    "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    "bio" text NOT NULL,
    "user_id" bigint NOT NULL UNIQUE
);

ALTER TABLE
    "profile"
ADD
    CONSTRAINT "profile_user_id_2aeb6f6b_fk_user_id" FOREIGN KEY ("user_id") REFERENCES "user" ("id") DEFERRABLE INITIALLY DEFERRED;

COMMIT;

外部キーにユニーク制約がついていること以外は、ForeignKeyの場合と同様ですね。

データ取得方法と発行されるSQLクエリ

DDLの実行とサンプルデータの投入を行います。

$ python manage.py migrate exampleapp 0002

exampleapp/fixtures/users_and_profiles.json

[
    {"model": "exampleapp.user", "pk": 1, "fields": {"username": "john_doe"}},
    {"model": "exampleapp.user", "pk": 2, "fields": {"username": "jane_smith"}},
    {"model": "exampleapp.profile", "pk": 1, "fields": {"user": 1, "bio": "Software engineer and tech enthusiast."}},
    {"model": "exampleapp.profile", "pk": 2, "fields": {"user": 2, "bio": "Data scientist with a passion for AI."}}
]
$ python manage.py loaddata users_and_profiles.json

userテーブルとprofileテーブルに2つずつデータが登録されました。

viewを更新し、データ取得時のSQLを確認していきます。ユーザーからプロフィールを参照する場合、関連するオブジェクトは0または1つであるため、select_relatedを使用できます。

from django.http import HttpResponse
from django.template import Context, Template

from exampleapp.models import User, Profile

def index(request):
    users = User.objects.select_related("profile").all()
    template = Template("""
        {% for user in users %}
            <p>{{ user }}: {{ user.profile.bio }}</p>
        {% endfor %}
    """)
    context = Context({"users": users})
    return HttpResponse(template.render(context))

(0.006) SELECT "user"."id", "user"."username", "profile"."id", "profile"."user_id", "profile"."bio" FROM "user" LEFT OUTER JOIN "profile" ON ("user"."id" = "profile"."user_id"); args=(); alias=default

LEFT OUTER JOIN句が使用され、1回のSQLクエリ実行でデータを取得できていることを確認できました。

また、プロフィールからユーザーを参照する場合は関連するオブジェクトが常に1つであり、こちらもselect_relatedを使用できます。

def index(request):
    profiles = Profile.objects.select_related("user").all()
    template = Template("""
        {% for profile in profiles %}
            <p>{{ profile.user }}: {{ profile.bio }}</p>
        {% endfor %}
    """)
    context = Context({"profiles": profiles})
    return HttpResponse(template.render(context))
(0.002) SELECT "profile"."id", "profile"."user_id", "profile"."bio", "user"."id", "user"."username" FROM "profile" INNER JOIN "user" ON ("profile"."user_id" = "user"."id"); args=(); alias=default

プロフィールからユーザーを参照する場合は常にオブジェクトが存在するため、INNER JOIN句が使用されていますね。

 

複数対複数のリレーション:ManyToManyを使う

最後に、ManyToManyリレーションを使って複数対複数の関係を定義する方法を見ていきます。

例えば、「学生とコース」の関係では、一人の学生が複数のコースを受講し、また一つのコースに複数の学生が登録するという関係を定義できます。

モデルの定義

class Student(models.Model):
    name = models.CharField(max_length=100)

    class Meta:
        db_table = "student"

    def __str__(self):
        return self.name

class Course(models.Model):
    title = models.CharField(max_length=200)
    students = models.ManyToManyField(Student)

    class Meta:
        db_table = "course"

    def __str__(self):
        return self.title

ここでは、CourseモデルにManyToManyFieldを使って、複数の学生が複数のコースに登録できるようにしています。

なお、ManyToManyFieldはどちらに定義しても構いませんが、向きによって参照の仕方が変わります。

生成されるDDL

このモデル設計に基づいたテーブル作成のDDLを確認していきます。

$ python manage.py makemigrations
Migrations for 'exampleapp':
  exampleapp/migrations/0003_student_course.py
    - Create model Student
    - Create model Course
$ python manage.py sqlmigrate exampleapp 0003
BEGIN;

--
-- Create model Student
--
CREATE TABLE "student" (
    "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    "name" varchar(100) NOT NULL
);

--
-- Create model Course
--
CREATE TABLE "course" (
    "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    "title" varchar(200) NOT NULL
);

CREATE TABLE "course_students" (
    "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    "course_id" bigint NOT NULL,
    "student_id" bigint NOT NULL
);

ALTER TABLE
    "course_students"
ADD
    CONSTRAINT "course_students_course_id_student_id_5d61deb9_uniq" UNIQUE ("course_id", "student_id");

ALTER TABLE
    "course_students"
ADD
    CONSTRAINT "course_students_course_id_a3f48577_fk_course_id" FOREIGN KEY ("course_id") REFERENCES "course" ("id") DEFERRABLE INITIALLY DEFERRED;

ALTER TABLE
    "course_students"
ADD
    CONSTRAINT "course_students_student_id_36bb682d_fk_student_id" FOREIGN KEY ("student_id") REFERENCES "student" ("id") DEFERRABLE INITIALLY DEFERRED;

CREATE INDEX "course_students_course_id_a3f48577" ON "course_students" ("course_id");

CREATE INDEX "course_students_student_id_36bb682d" ON "course_students" ("student_id");

COMMIT;

course_studentsという中間テーブルが生成され、course_idとstudent_idの組み合わせで一意の行を持つように定義されていることがわかります。また、両方の外部キーにはON DELETE CASCADEが設定されています。

データ取得方法と発行されるSQLクエリ

DDLの実行とサンプルデータの投入を行います。

$ python manage.py migrate exampleapp 0003 

exampleapp/fixtures/students_and_courses.json

[
    {"model": "exampleapp.student", "pk": 1, "fields": {"name": "Alice Johnson"}},
    {"model": "exampleapp.student", "pk": 2, "fields": {"name": "Bob Smith"}},
    {"model": "exampleapp.student", "pk": 3, "fields": {"name": "Charlie Brown"}},
    {"model": "exampleapp.course", "pk": 1, "fields": {"title": "Introduction to Python"}},
    {"model": "exampleapp.course", "pk": 2, "fields": {"title": "Data Structures and Algorithms"}},
    {"model": "exampleapp.course", "pk": 3, "fields": {"title": "Machine Learning Basics"}},
    {"model": "exampleapp.course_students", "pk": 1, "fields": {"course_id": 1, "student_id": 1}},
    {"model": "exampleapp.course_students", "pk": 2, "fields": {"course_id": 1, "student_id": 2}},
    {"model": "exampleapp.course_students", "pk": 3, "fields": {"course_id": 2, "student_id": 2}},
    {"model": "exampleapp.course_students", "pk": 4, "fields": {"course_id": 2, "student_id": 3}},
    {"model": "exampleapp.course_students", "pk": 5, "fields": {"course_id": 3, "student_id": 1}},
    {"model": "exampleapp.course_students", "pk": 6, "fields": {"course_id": 3, "student_id": 3}}
]
$ python manage.py loaddata students_and_courses.json

studentテーブル、courseテーブルそれぞれ3レコードずつと、中間テーブルに6レコードが登録されました。

viewを更新し、データ取得時のSQLを確認していきます。関連するオブジェクトが複数になる可能性があるため、prefetch_relatedを使用します。

今回は参照名を指定していないため、studentからcourseを参照する場合は"course_set"で参照する必要がありますが、related_name="courses"のように指定しておくとより直感的に操作でき、対称性が保たれて良いでしょう。

from django.http import HttpResponse
from django.template import Context, Template

from exampleapp.models import Student, Course

def index(request):
    students = Student.objects.prefetch_related("course_set").all()
    template = Template("""
        {% for student in students %}
            <p>{{ student }}</p>
            <ul>
                {% for course in student.course_set.all %}
                    <li>{{ course }}</li>
                {% endfor %}
            </ul>
        {% endfor %}
    """)
    context = Context({"students": students})
    return HttpResponse(template.render(context))

(0.009) SELECT "student"."id", "student"."name" FROM "student"; args=(); alias=default
(0.008) SELECT ("course_students"."student_id") AS "_prefetch_related_val_student_id", "course"."id", "course"."title" FROM "course" INNER JOIN "course_students" ON ("course"."id" = "course_students"."course_id") WHERE "course_students"."student_id" IN (1, 2, 3); args=(1, 2, 3); alias=default

courseからstudentを参照する場合は次のようになります。

def index(request):
    courses = Course.objects.prefetch_related("students").all()
    template = Template("""
        {% for course in courses %}
            <p>{{ course.student }}</p>
            <ul>
                {% for course in courses %}
                    <li>{{ course }}</li>
                {% endfor %}
            </ul>
        {% endfor %}
    """)
    context = Context({"courses": courses})
    return HttpResponse(template.render(context))

(0.002) SELECT "course"."id", "course"."title" FROM "course"; args=(); alias=default
(0.003) SELECT ("course_students"."course_id") AS "_prefetch_related_val_course_id", "student"."id", "student"."name" FROM "student" INNER JOIN "course_students" ON ("student"."id" = "course_students"."student_id") WHERE "course_students"."course_id" IN (1, 2, 3); args=(1, 2, 3); alias=default

 

まとめ

DjangoではORMを使用して簡単にリレーションの管理を行える一方で、効率的にSQLクエリを発行するためにはselect_relatedやprefetch_relatedを適切に使用する必要があるということを確認できました。

ForeignKey、OneToOne、ManyToManyの仕様を理解し、効率的なDB設計・データ取得を行いましょう。

別の機会に、filterやannotate、limit等を使用する際のSQLクエリについても深掘りしていければと思います。