ITの窓辺から

三流IT技術者の日常

Django ManyToManyFieldはどちらのモデルで指定すべきか

多対多関係のテーブルを作る場合、DjangoではManyToManyFieldを使用します。するとDjangoは元々の多対多関係のテーブルと一対多関係になるような中間テーブルを自動的に生成します。この時、どちらのテーブルにManyToManyFieldを指定するかいつも悩みます。それについてちょっと調べてみましたので、この記事ではその結果を書いておきます。

結論から言うと、基本的にどちらに書いても変わらないような気がします。影響が出るとしてもテーブル構造やインデックス次第、という身も蓋もない話です。

実は、ManyToManyFieldの使い方をすぐに忘れてしまうので言語化しておこうと思い、ついでに動作を確認した結果を書いているだけだったりします。

環境は以下のとおりです。
OS : macOS(intel CPU) Big Sur
Python : バージョン3.8.5 (venv環境)
Django : バージョン3.2.5
データベース : sqlite3

調査

Djangoのsettingsにログの設定を行い、発行されるSQLを見てみました。

モデル

以下のようなシンプルなモデルを用意してみました。

class Menu(models.Model):
    name = models.CharField(max_length=64)
    ing = models.ManyToManyFiled(Ingredient, related_name='Ing')

class Ingredient(models.Model):
    name = models.CharField(max_length=64)

食事メニューのテーブルと材料のテーブルです。
食事メニューのテーブルは、例えばシチュー、カレー、豚汁といったものがレコードになり、材料のテーブルは豚肉、じゃがいも、味噌といったものがレコードになります。この例では食事メニューの方にManyToManyFieldを指定しています。

マイグレーションを行うと中間テーブルが生成されます。これで食事メニューテーブルと材料テーブルの関連付けができているわけです。中間テーブルのカラムには双方のテーブルの主キーに対する外部キー制約がかかります。また、中間テーブルのインデックスは双方のテーブルの主キーに対応するカラムどちらに対しても張られます。

ここまで来たら、マネージャを使うことでモデルの操作が可能になります。モデルの操作は食事メニューからも材料からも可能です。操作に関して覚えておくことは、ManyToManyFieldを指定したテーブルともう片方のテーブルで若干マネージャでの指定の仕方が異なることです。

下準備です。

# 食事メニューインスタンスを作成
m1 = Menu.objects.create(name='Curry') #カレー
m2 = Menu.objects.create(name='Misoshiru') #豚汁

# 材料インスタンスを作成
i1 = Ingredient.objects.create(name='Pork') #豚肉
i2 = Ingredient.objects.create(name='Tofu') #豆腐
i3 = Ingredient.objects.create(name='Miso') #味噌

ManyToManyFieldを指定した方からの操作

つまり食事メニューを1個選んで、そこに含まれる材料を取り扱う場合です。豚汁(m2)に材料を追加してみます。この場合、マネージャの使い方としては素直にManyToManyを指定したカラムを指定するだけです。具体的には以下のようにします。

m2.ing.add(i1) #豚汁の材料として豚肉を追加
m2.ing,add(i2) #豚汁の材料として豆腐を追加
m1.ing.add(i1) #カレーの材料として豚汁を追加

人間の理解としてはコメントで書いたとおりですが、Django的には中間テーブルにINSERTする動作をします。

豚汁の材料を全て取得するには以下のようにします。

m2.ing.all() #豚汁の材料を全て表示

こうするとQuerySetとして中間テーブルで豚汁に相当するレコードの材料カラムに対応する材料テーブルのレコードが返されます。今回は豚肉と豆腐です。SQL的には食事メニューと中間テーブルの内部結合したものから、豚汁のレコードのみ切り出したものを返す動作をします。

マネージャには他のメソッドとして削除や置換が用意されています。いずれも中間テーブルに対するレコード操作であり、中間テーブルは各カラムにインデックスが張られているので通常はパフォーマンスは十分発揮できると考えられます。

ManyToManyFieldを指定していない方からの操作

逆からの操作もできます。材料の方を起点に、この材料を使って作るメニューを操作するような場合です。基本的にはManyToManyFieldを指定した方と同じ操作が可能ですが、マネージャでの指定の仕方が違います。具体的には以下のようにします。

i1.Ing.all() #豚肉を使用する食事メニューを全て表示

ManyToManyFieldを指定した時に設定したrelated_nameの値を使用します。related_nameを指定しない場合、<モデル名>_setという値を使用します。個人的にはrelated_nameをちゃんと指定しておく方が後々に分かりやすくなると思います。

上記のように指定するとSQL的には材料と中間テーブルを内部結合したものから、豚肉のレコードのみ切り出したものを返す動作になります。先ほどとの違いは内部結合するテーブルと切り出し(WHERE)に使うテーブルです。

まとめ

さて、どちらのケースであっても中間テーブルのレコードに登録される値は各テーブルの主キーではありますが、標準ではDjangoはインデックスを張ってくれないので必要に応じてインデックスを作っておかないとデータベース操作性能に影響が出る可能性があります。ただ、ManyToManyFieldをどちらに設定すべきかという当初の疑問の解答としてはどちらでも良い、ということになりそうです。

強いて言うとコードの保守性を考えた時に、どちらに作成するのか一定の基準が設計書やコーディング規約で示されていることが望ましいような気はします。