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

- Python -
2020.07.22
Django

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

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

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

このように、ModelFormのinstanceキーワード引数にモデルオブジェクト(この場合article)を渡してからform.save()すること。

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 行目で、ArticleFormの引数にrequest.POSTだけでなくinstance=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

The whole trick is using the instance parameter when creating the form instance from the post. django will load and know about the unique field, ie the line:

form = MyModelForm(request.POST, instance=obj)

is essential.

 

しくじった例

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節内が実行されずエラー

というわけでした。

更新処理のときは、

form = ArticleForm(request.POST, instance=<モデルオブジェクト>)

としてform.save()すること。

以上!

↑TOP