知識の枝

"All is well"

Django create()でManyToManyを割り当て

約67日前 2021年8月22日14:08
デジタル
Django

改訂履歴


2021/8/22 投稿

1. 背景


Djangoを使った開発中に覚えたことを備忘録として残します。

今回はModel.objects.create()でモデルインスタンスを新規作成する際にManyToManyフィールドに他モデルのインスタンスを割り当てる方法を解説します。


2. ゴール


ManyToManyフィールドにインスタンスを割り当てる方法を習得する。


3. はじめに


やりたいことのイメージ共有から始めます。

下記の2つモデルを考えましょう。

1つは「人物」を表す「Person」モデル

もう1つは「部活」を表す「Club」モデルです。
【models.py】
# 人物モデル
class Person(models.Model):
name = models.CharField(verbose_name='氏名', max_length=20)
age = models.IntegerField(verbose_name='年齢')

# 部活モデル
class Club(models.Model):
club_name = models.CharField(verbose_name='部活名', max_length=20)
member = models.ManyToManyField(Person, verbose_name='所属メンバー')
田中くんという人物がいたとします。

この学校は兼部が許されていますので、田中くんはテニス部と茶道部に所属しています。


田中くんはカフェが好きなので、新しく「カフェ部」を作りその初期メンバーとして兼部しようとしています。

カフェ部を作るにはどんなコードを書けば良いでしょうか?
※初期メンバーとして田中くんをアサインしたいです。



モデルインスタンスを新しく作る場合、下記コードを実行すれば可能でしたね。
# 新しく「カフェ部」を作る
Club.objects.create(club_name = "カフェ部", member = ???)
あれ?どうやってManyToManyフィールドにモデルを割り当てるのでしょうか?

member = Person.objects.get(name="田中") でしょうか?


いや、これだとエラーが出てしまいます。
# エラー内容
TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use club.set() instead.
どうやらcreate()するときに直接ManyToManyに割り当てることは禁止されているようです。

ではどうすれば良いのでしょうか?

その解決策を次項で解説していきます。



4. ManyToManyの割り当て


4.1 - 保存してから割り当て


この現象は「フォームを使ってManyToManyフィールドを保存する場合」と似ていますね。

フォームで入力したManyToManyフィールドが保存されない問題

上記の記事でも書きましたが、以下公式ドキュメントからの抜粋です。

「Djangoはインスタンスがデータベース上に存在するようになるまで、インスタンスに対して多対多のデータを保存することが不可能」



今回の件も同様の理由でエラーが発生しているのだと考えられます。


そこで解決策として下記の手順でManyToManyの割り当てを行います。

①ManyToManyフィールドの割り当てを行わないでインスタンスを生成。

②生成したインスタンスのManyToManyに後からPersonを割り当て。



インスタンスの保存タイミングとManyToManyの保存タイミングをズラします。

具体的には下記のコードで実現可能です。
# 新しく「カフェ部」を作る
# memberは割り当てない
new_club = Club.objects.create(club_name = "カフェ部")

# memberにPersonを割り当てる
new_club.member.add(Person.objects.get(name="田中"))
これで保存可能です。


今回紹介した方法はDjangoの 公式ドキュメント にも記載されいます。

以下抜粋
# 記事Articleと出版物Publicationの関連付けの例

# Create an Article:
# Articleインスタンスを生成
>>> a1 = Article(headline='Django lets you build Web apps easily')


# You can’t associate it with a Publication until it’s been saved:
# Articleの保存前にPublicationインスタンスをManyToManyに関連付けることは出来ない
# 実行するとエラーが出る
>>> a1.publications.add(p1)
Traceback (most recent call last):
...
ValueError: "<Article: Django lets you build Web apps easily>" needs to have a value for field "id" before this many-to-many relationship can be used.


# エラーが出ないように先にArticleを保存する
>>> a1.save()


# Associate the Article with a Publication:
# その後Publicationを関連付ける
>>> a1.publications.add(p1)

やっている流れとしては全く同じで、途中で起きているエラーの根本原因は同じです。


ManyToManyを持つモデルインスタンスを作る際に覚えておきたいのは、

「保存してから割り当てを行う」

これです。


4.2 - 田中以外も入りたい


さっきは田中くん一人しかアサインされませんでしたが、実は他の人もカフェ部に入りたかったみたいです。

カフェ部に入れたあげたいので、入部予定者のリストをfilter()やall()で先に用意しておきます。
# 全員入りたがっている場合
prospective_member = Person.objects.all()

# 16歳以上が全員強制入部の場合
prospective_member = Person.objects.filter(age__gte=16)


リストのメンバーをカフェ部に入部させます。
# リストを*でアンパックしてカフェ部のmemberに関連付け
new_club.member.add(*prospective_member)


複数人を割り当てる場合は上記の方法で可能です。



5. さいごに


覚えておくと今後役に立つ方法だと思います。

とにかく「保存してから割り当てを行う」

行き詰まったらこれを思い出して下さい。

お疲れ様でした。