T.H/ 2022年 3月 31日/ 技術

はじめに

こんにちは。T.H.です。
久しぶりのブログ記載になります。今回はdjangoのお話になります。

結論

djangoのORマッパーは使いやすくて良いですね。

djangoでの多対多表現

djangoにおけるmodelのrelationをあらわすものにManyToManyというものがあります。
これは、多対多の関係性を設定するためのものです。
(ここでは多対多そのものの説明は省きます。)

多対多の関係性はモデル図に書き起こす場合は特に問題ないのですが、
MySQLなどのリレーショナルDBではテーブル間で多対多を直接表現する手段を持たないため、ユーザー側で実現可能なようにテーブルを設計する必要があります。

ところがdjangoのORマッパーはその関係性を直接書き起こすことが出来ます。
これが非常に便利でして、覚えておくとよい、といいますか、使用しないとmodelの記述が大変です。
また、SQLでは直接表現できない概念のため、データの追加やフィルタリング方法も把握しておく必要があります。

作成例

多対多の例として、複数のタグをつけられるブログ記事を考えます。
一つのブログ記事には複数のタグをつけることができ、また、一つのタグは複数のブログ記事につけることができます。
DBはMySQL使用を前提とします。

from django.db import models

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

    def __str__(self):
        return self.name

class Blog(models.Model):
    title = models.CharField(max_length=100)
    content = models.CharField(max_length=5000)
    tags = models.ManyToManyField(Tag, related_name='tags', blank=True)

    def __str__(self):
        return self.title

このように"models.ManyToManyField"を指定するだけで多対多の関係を指定できます。
DB上ではどのようなテーブルが作成されているかというと、

myproject_blog
myproject_blog_tag
myproject_tag
...

BlogとTagに対応するテーブル、それとBlogとTagを連結するための中間テーブルの3つが出来ることになります。
"myproject_blog_tag"のテーブル定義は以下のようになります。

カラム名
id
blog_id
tag_id

見ての通り、blog_id(blogテーブルのキー)とtag_id(tagテーブルのキー)の組み合わせを保管するのみです。
データの追加削除、フィルタリングの際にはORマッパー側で自動的にこのテーブルを参照、更新します。
なお、ManyToManyFieldにthroughを指定することで自作の中間テーブルを作成することも出来ます。(ここでは割愛します)

データ追加

データの追加は下記の用に実施します。ここではあるブログ記事にタグを追加するケースを想定します。

Tagの新規追加(create)

createは作成と保存を同時に行います。

blog = Blog.objects.get(id = 1)
blog.tags.create(name="Python")

Tagの追加(add)

addの場合は、Tagを事前に保存する(例ではこちら)か、保存済みのTagオブジェクトが必要になります。
中間テーブルのために確定済みのTagのidが必要になるからですね。

new_tag = Tag(name = django)
new_tag.save()
blog = Blog.objects.get(id = 1)
blog.tags.add(new_tag)

データ削除

同じようにあるブログ記事からタグを削除するケースを想定します。

Tagの削除(remove)

tag = Tag.objects.get(id = 2)
blog = Blog.objects.get(id = 1)
blog.tags.remove(tag)

Tagの全削除(clear)

blog = Blog.objects.get(id = 1)
blog.tags.clear()

フィルタリング

全データ(all)

all()にてBlogに紐づく全タグを取得できます。通常のクエリセットのobjects.all()と同じ感覚で使用できます。

blog = Blog.objects.get(id = 1)
all_tags = blog.tags.all()

条件指定(filter)

filter()にて条件を指定して取得できます。通常のクエリセットのobjects.filter()と同じ感覚で使用できます。

<pre><code>blog = Blog.objects.get(id = 1)
filtered_tags = blog.tags.filter(name = 'Python')

逆参照

TagからBlogを参照することも可能です。下記ではあるTagに紐づくBlogをすべて取得します。

tag = Tag.objects.get(id = 1)
blog_all = tag.blog.all()

フィルタリング時の注意

少し話題がそれますが、実際にManyToManyの参照先をフィルタリングして取得する場合はprefetch_relatedを使用することをお勧めします。
prefetch_relatedを使用しない場合にManyToManyの参照先を取得する際、ORマッパーが毎回クエリ発行するため、非常に効率が悪くなります。
prefetch_relatedは一度のクエリでデータをキャッシュし、再利用するものです。

blog = Blog.objects.prefetch_related('tags').get(id = 1)
tag_python = blog.tags.filter(tags__name = 'Python')

なお、さらに参照が複雑になる場合や、filterで取得したblog自体をforループで回したい場合はPrefetchオブジェクトを使用しますが、こちらの説明は割愛します。

最後に

途中色々と割愛してばかりで申し訳ないですが、全体的な説明は出来たのでは無いかと思います。

私が初めてdjangoに触れたとき、それまで主にSQLでDBをいじっていたため、ManyToManyのような便利な手段があるとは思い至らず、自力で作成したり、無理のない範囲で非正規化したりしていました。なんと無駄なことを…。

お読みいただきありがとうございました。

About T.H

North Torch株式会社 プログラマ 技術的な経歴は.NETアプリケーションが一番長い。 その他はまだまだ勉強中。