djangoのsave_userメソッドの引数commitについて調べてみた

今回のお題
-
今回はdjangoのユーザー認証で用いられているcommitという引数について調べてみました。

この引数は必要に応じて初期値を切り替える必要があるのですが、そもそもこの変数が何をしているのか、そしてなぜ初期値を切り替えないといけないのかを自分なりに調べたのでこの場をお借りしてまとめておこうと思います。

事の起こり
-
事の起こりは[前回](https://qiita.com/keishi04hrikzira/items/0c6f6343cce567a00a07)の記事でCustomUserを実装している時のことでした。
>CustomUserにフィールドを追加した場合、デフォルトのsave_userメソッドでは対応できないのでadapterを上書きする必要がある。

ふむふむ。

>adapterは以下のように記述する。

```python:adapterのソースコード
from allauth.account.adapter import DefaultAccountAdapter

class AccountAdapter(DefaultAccountAdapter):
  def save_user(self, request, user, form, commit):
    user = super().save_user(request, user, form, commit=False)
    user.age = form.cleaned_data.get("age") # "age"が追加したフィールド
    user.save()
```
なるほど。

>引数のcommitはfalseにすること

そしてここで躓くわけですね。

commitってなんだ？と。

というわけで、DefaultAccountAdapterを除いてきました。

DefaultAccountAdapterの中身
-

```python:allauth.account.adapter.py
class DefaultAccountAdapter(object):
    # ...省略...
    def save_user(self, request, user, form, commit=True):
        """
        Saves a new `User` instance using information provided in the
        signup form.
        """
        from .utils import user_email, user_field, user_username

        data = form.cleaned_data
        first_name = data.get("first_name")
        last_name = data.get("last_name")
        email = data.get("email")
        username = data.get("username")
        user_email(user, email)
        user_username(user, username)
        if first_name:
            user_field(user, "first_name", first_name)
        if last_name:
            user_field(user, "last_name", last_name)
        if "password1" in data:
            user.set_password(data["password1"])
        else:
            user.set_unusable_password()
        self.populate_username(request, user)
        if commit:
            # Ability not to commit makes it easier to derive from
            # this adapter by adding
            user.save()
        return user
```
基本的にcleaned_dataの中身をuserインスタンスに入れていますね。

その作業が終わった後に書かれている以下の部分に注目してください。

```python
        if commit:
            # Ability not to commit makes it easier to derive from
            # this adapter by adding
            user.save()
```
`commit == true`であるかどうかでユーザーインスタンスを保存するかどうかを分岐させています。

つまり`save_user`メソッドとは

- allauthにあらかじめ用意されている属性の値をuserインスタンスにセットする
- commit == trueであれば保存を行う

　というものになります。

そして今回adapterでこれを上書きして作った`save_user`メソッドは

- allauthにあらかじめ用意されている属性の値をuserインスタンスにセットする
- commit == trueであれば保存を行う
- CustomUserで追加したフィール値をセットする
- commitの値によらず保存を行う

という内容になります。

つまり、`commit==true`だと2回保存が入るわけですね。

この2回というのが曲者で、特に1回目はまだ追加したフィールド(今回で言えば"age")の値をセットしていないため、バリデーションの状況によってはエラーになります。

こういった状況を回避するために、`commit=false`にすることが推奨されているということですね。

```python:adapterのソースコード（再掲）
from allauth.account.adapter import DefaultAccountAdapter

class AccountAdapter(DefaultAccountAdapter):
  def save_user(self, request, user, form, commit):
    user = super().save_user(request, user, form, commit=False) # ここでは"age"の値をセットしていないため、保存まで進むとエラーになる(ageがnull=falseの場合）。
    user.age = form.cleaned_data.get("age") # "age"の値をセット
    user.save() # "age"の値がセット済みなら保存して大丈夫
```

終わりに
-
以上で引数commitのお話を終わります。

今後もわからないことがあったときにはなるべく参照元のコードを見る癖をつけていきたいですね。