知識の枝

"All is well"

Django ManyToManyフィールドが保存されない

約181日前 2021年5月29日23:43
デジタル
Django

改訂履歴


2021/5/29 投稿

1. 背景


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

今回はフォームで入力したManyToManyフィールドが保存されない問題について解説します。


2. ゴール


ManyToManyフィールドにフォームで入力し、データベースへ登録する。


3. はじめに


タイトルの通りManyToManyフィールドが保存されない。

そんなトラブルがある条件下で発生します。
しかも割とよくあるシチュエーションだと思います。


そのトラブルの発生条件と解決方法を解説します。


4. 問題例と対策


こんな2つのモデルがあったとします。
models.py
class Knowledge(models.Model):
"""得意分野"""

"""フィールド定義"""
name = models.CharField(verbose_name='得意分野', max_length=20)

def __str__(self):
return str(self.name)


class Person(models.Model):
"""人物"""

"""フィールド定義"""
name = models.CharField(verbose_name='名前', max_length=20)
mail_to = models.EmailField(verbose_name='メールアドレス', max_length=240)
good_at = models.ManyToManyField(Knowledge, verbose_name='得意分野')
lucky_num = models.IntegerField(verbose_name='ラッキーナンバー')

def __str__(self):
return str(self.name)
得意分野のモデルを作っておき、Personモデルの中で得意分野をManyToManyで複数選択できるようにしてあります。

またPersonモデルには、モデルの生成時にランダムなラッキーナンバーを生成して保存するフィールドを作成しておきました。


フォームのPOSTを使ってモデルを作成する場合を考えます。

フォームでは
「name」「mail_to」「good_at」
の3項目を入力してもらい、「random_url」はviews.pyの「is_valid()」後で追加する仕様です。


views.pyのPOST処理する記述は下記のようになります。
views.py
"""POST処理"""
if request.method == "POST":
form = CreatePersonForm(request.POST)
if form.is_valid():
new_person = form.save(commit=False)
new_person.lucky_num = randint(1, 100) #1~100からランダムに選択
new_person.save()
form.save_m2m() #ここがキーポイント

return redirect('app:index')
まだ未入力のフォーム「lucky_num」をviews.pyの中で入力する必要があります。

一旦「save(commit=False)」でPersonモデルのインスタンスを変数「new_person」に保存します。
(このタイミングではまだデータベースに保存されていません。)


次にラッキーナンバーを「new_person」「lucky_num」フィールドに入力します。
new_person.lucky_num = randint(1, 100)   #1~100からランダムに選択


これで4つ全てのフィールドが入力された状態になりましたので、モデルをデータベースに保存します。
new_person.save()


ここで終わると今回のトピックになっている問題が発生します。

モデルの保存自体は成功するのですが、「ManyToManyフィールド」の中身が空っぽになります。

is_valid()はTrueで保存処理に移れていますので、POSTデータ自体は問題ありません。


原因は「save(commit=False)」です。
new_person = form.save(commit=False)
この一文が入るとDjangoはManyToManyの中身を保存できなくなるようです。

これについては公式ドキュメントに記載されています。

commit=False を使う際のもう 1 つの副作用は、モデルに他のモデルとの多対多の関係がある場合に見られます。フォームを save するときにモデルに多対多の関係があり commit=False を指定した場合、Django は多対多の関係に対してフォームのデータを即座に保存することができません。これは、インスタンスがデータベース上に存在するようになるまで、インスタンスに対して多対多のデータを保存することが不可能だからです。

To work around this problem, every time you save a form using commit=False, Django adds a save_m2m() method to your ModelForm subclass. After you've manually saved the instance produced by the form, you can invoke save_m2m() to save the many-to-many form data.

Calling save_m2m() is only required if you use save(commit=False).

https://docs.djangoproject.com/ja/3.2/topics/forms/modelforms/#the-save-method


解決策はドキュメントに書いてあるように、save()メソッドでモデルを保存した後に「save_m2m()メソッド」でManyToManyフォームのデータを保存することです。
new_person.save()
form.save_m2m() #formのメソッドです。


この一文を足すだけで問題無くManyToManyがデータベースに保存されます。


5. さいごに


よくあるハマりポイントかもしれません。
ManyToManyとsave(commit=False)がきたら、save_m2m()をお忘れなく!