【Django】ModelFormで更新/update処理|Function Based Viewの注意点

- Python -
2020.02.18
Django

Django学習のためClass Based Generic Viewを使わずにFunction Based Viewで更新/update処理を実装した時にハマったことがあったのでメモしておきます。

結論だけ先に言うと、更新処理時は

form = ArticleForm(request.POST, instance=article)

このように、ModelFormのinstanceキーワード引数にモデルオブジェクトを渡すこと。

ModelFormで更新/update処理|Function Based Viewの場合

こういうブログ記事作成フォームを更新するviewを作ることを想定します。

Djangoの更新処理

まず、Articleモデルはこういう前提です。

DjanoのModel

Articleモデルに対応するModelFormのArticleFormはこうです。

DjanoのModelForm

更新処理のviewはこうです。(これは最終的に上手くいった例。NGだった例は後述します)

def edit(request, slug):
    # 対象の記事情報を取得
    article = get_object_or_404(Article, slug=slug)
    # ログイン済み & 記事作成者判定
    if request.user.is_authenticated and request.user == article.author:
        if request.method == 'POST':
            form = ArticleForm(request.POST, instance=article)
            if form.is_valid():
                form.save()
                messages.success(request, 'Article successfully updated!')
                return redirect(reverse('management:edit', args=(article.slug,)))
        else:
            form = ArticleForm(instance=article)
            return render(request, 'management/edit.html', {'form': form, 'article': article})
    else:
        messages.error(request, 'Fobidden!')
        return redirect(reverse('blog:detail', args=(article.slug,)))

ポイントは 7 行目のinstanceキーワードで、ArticleFormの引数にrequest.POSTinstance=articleの2つを渡すこと。

なんでarticleオブジェクトまで渡すかというと・・・?

DjangoはcreateもupdateもForm.save()

"新規作成/create"なのか"更新/update"なのかをDjangoフレームワーク側で判断させるため。

form = ArticleForm(request.POST)

このようにrequestオブジェクトだけ渡すと、form.save()した時の挙動は新規作成/createになる。

form = ArticleForm(request.POST, instance=article)

のようにすると、form.save()は更新/updateになる。

公式ドキュメントに以下のようにある。

How Django knows to UPDATE vs. INSERT

You may have noticed Django database objects use the same save() method for creating and changing objects. ...(省略)...

If the object’s primary key attribute is set to a value that evaluates to True (i.e., a value other than None or the empty string), Django executes an UPDATE.

公式にこうやってしっかり書いてあったけど、最初に直接的なヒントになった記事は以下。

Model Forms: Clean unique field - djangosnippets.org

しくじった例

instance=articleと渡すのを知らなくて、ArticleForm(request.POST)としつつ新規作成せずにfunction based viewでの更新はどうやるんだろう?と思って最初に考えた案は、以下のようにform.is_valid()をした後にform.cleaned_data.get('slug')でデータを地道に取り出してはarticleの属性に代入していくこと。

def edit(request, slug):
    # 対象の記事情報を取得
    article = get_object_or_404(Article, slug=slug)
    # ログイン済み & 記事作成者判定
    if request.user.is_authenticated and request.user == article.author:
        if request.method == 'POST':
            form = ArticleForm(request.POST)
            if form.is_valid():
                article.slug = form.cleaned_data.get('slug')
                article.title = form.cleaned_data.get('title')
                article.content = form.cleaned_data.get('content')
                article.category = form.cleaned_data.get('category')
                article.status = form.cleaned_data.get('status')
                article.save(update_fields=[
                             'slug', 'title', 'content', 'category', 'status', 'updated_at'])
                messages.success(request, 'Article successfully updated!')
                return redirect(reverse('management:edit', args=(article.slug,)))
        else:
            form = ArticleForm(instance=article)
            return render(request, 'management/edit.html', {'form': form, 'article': article})
    else:
        messages.error(request, 'Fobidden!')
        return redirect(reverse('blog:detail', args=(article.slug,)))

実はこれでも「Modelでunique=Trueを定義したフィールドさえ存在しなければ」うまく更新処理が成功しました。

今回の例では、slug = models.SlugField(unique=True, null=True)とunique制約を設定しているフィールドがあったため、エラーになってしまいました(slugフィールドを追加する前までは更新に成功していた)。

  1. ArticleForm(request.POST)で「新規作成」と判断された
  2. form.is_valid()で、すでに保存済みのslugが存在するためunique制約により「重複している」と判断された
  3. 結果、form.is_valid()がFalse判定になり、if節内が実行されずエラー

というわけでした。