Django:this querydict instance is immutable request.data エラーの原因と対策

Django:this querydict instance is immutable request.data エラーの原因と対策

これが出てくるのは、POSTされた値を変更しようとしたときです。

request.POSTはQueryDictというオブジェクトで、書き換え不可になっている

だからエラーが発生するんですね。

querydict instance is immutableエラーの原因

書換できないrequest.POSTを書換ようとしてる事

これが原因です。となれば採用する手段は2つです。

  1. 書き換えなくてもよい値をPOSTする
  2. POSTされた値を別の変数に格納して保存工程に渡す

項目1が楽そうですが、TemplateからFormでPOSTした値が撥ねられることは多々あります。
少し実例を見てみましょう。

The Model名 could not be changed because the data didn't validate.

これは私がCRUD作成の一番最初に苦しめられたエラーです。
エラー発生原は様々ですが、私の場合は下の組合せが原因でした。

  1. Models.py 内で【models.DateField(null=True,default=None)】としたフィールドに対し何も入れずにPOSTした。
  2. Models.py 内で【models.IntegerField(null=True,default=None)】としたフィールドに対し何も入れずにPOSTした。
  3. POSTしたリレーションカラム名がModel内の名称と違っていた

POSTされた値が文字列ではない時やリレーションを張っているカラムが存在する時、このエラーの発生率は高まります。

エラーの原因と対策は別ページにまとめました。

POSTされた値を上書きしようとしてもできない

タイトルの内容に戻ります。
【querydict instance is immutable】エラーはPOSTされた値を変更しようとしたときに発生します。

if request.POST['corporates']=='':
    request.POST['corporates'] = None

例えば上のような場合です。POSTされた値が[ ’’ ] だった時にNoneに置き換えようとしているのですが、
QueryDictであるrequest.POSTは書換え不可
なので【querydict instance is immutable】エラーが発生してしまいます。

POSTされた値を地道に変数に入れてその値を渡してあげる方法であれば【querydict instance is immutable】のエラーは発生しません。例えばこんな実装です。

■Views.py内

  if (request.method == 'POST'):
      #データ適用 NULLに文字列Noneが入ることを回避
      if ((request.POST['corporates'] == 'None')or(request.POST['corporates'] == '')):
          corporates = None
      else:
          corporates = request.POST['corporates']
      if ((request.POST['postal_code'] == 'None')or(request.POST['postal_code'] == '')):
          postal_code = None
      else:
          postal_code = request.POST['postal_code']

      if ((request.POST['prefecture'] == 'None')or(request.POST['prefecture'] == '')):
          prefecture = None
      else:
          prefecture = request.POST['prefecture']
      if ((request.POST['municipalities'] == 'None')or(request.POST['municipalities'] == '')):
          municipalities = None
      else:municipalities = request.POST['municipalities']
      if ((request.POST['buildings'] == 'None')or(request.POST['buildings'] == '')):
          buildings = None
      else:
          buildings = request.POST['buildings']
      if ((request.POST['tel_number'] == 'None')or(request.POST['tel_number'] == '')):
          tel_number = None
      else:
          tel_number = request.POST['tel_number']
      if ((request.POST['fax_number'] == 'None')or(request.POST['fax_number'] == '')):
          fax_number = None
      else:
          fax_number = request.POST['fax_number']
      if ((request.POST['email'] == 'None')or(request.POST['email'] == '')):
          email = None
      else:
          email = request.POST['email']
      if ((request.POST['website'] == 'None')or(request.POST['website'] == '')):
          website = None
      else:
          website = request.POST['website']

      Recode = Company(corporates=corporates,
                       postal_code=postal_code,
                       prefecture=prefecture,
                       municipalities=municipalities,
                       buildings=buildings,
                       tel_number=tel_number,
                       fax_number=fax_number,
                       email=email,
                       website=website)
      Recode.save()

カラム数が少なければ『アリ』かもしれませんが、これだけのレコード数になるとさすがに悠長すぎです。

QueryDictのcopy()メソッドを使ってみる

エラーにあるようにTemplateからPOSTされた値は QueryDictというオブジェクトになっています。
このQueryDictにはcopy()というメソッドがあり、これを使うとPOSTされた値を書き換え可能な姿に複製してくれます。

〇 : 値を変数に格納する ( 例:corporates = request.POST['corporates'] )
〇 : request.POSTのコピーを作る ( 例:cp = request.POST.copy() )
× : 変数にrequest.POSTを格納する ( 例:cp = request.POST )

同じ【コピーする】の様でも上記例の3番目はNGです。理由はQueryDictオブジェクトである事も含めてrequest.POSTをそのまま変数に格納するから。なので、これだとどんなに頑張っても同じエラーが発生してしまいます。

1番目はサンプルスクリプトで活用した方法です。泥臭い方法ですがココをベースにして簡略化していきます。
その時に使う手段がcopy()メソッドです。

コードを短くするのために見るべき場所

長いコードを短くするためには3つの箇所に目を向けます。

  1. 同じ(似たような)コードが繰り返し発生していないか
  2. コードが長くなる原因のデータはどのような形になっているか(今回だったらrequest.POST)
  3. 簡略化できる命令セットはないか

同じ(似たような)コードが繰り返しが発生していないか

上のコード例にはバッチリありますね。

if ((request.POST['corporates'] == 'None')or(request.POST['corporates'] == '')):
    corporates = None 
else:
    corporates = request.POST['corporates']

[ '' ]と[ 'None' ]で渡された値をNone(Null)に変更している工程が毎回繰り返し発生しています。
「forループで対応しよう」と思うのですが、この時障害になるのが  QueryDictであるrequest.POSTは書換え不可 というルールです。

コードが長くなる原因のデータはどのような形になっているか

request.POSTは次のような姿で渡されてきます。

request.POST ={
    'corporates':'会社名',
    'postal_code':'123-4567', 
    'prefecture':'',
    'municipalities':'',
    'buildings':'',
    'tel_number':'',
    'fax_number':'',
    'email':'',
    'website':''
 }

辞書型といわれる形です。これはKeyを取り出したい場合には注意が必要です。

簡略化できる命令セットはないか

request.POST は for in ループで値を取り出せるが【書き換え】ができないため毎回変数を用意して格納している

#NGなコード
for ps in request.POST:
    if (ps == '')or(ps == 'None')
        ps = None

辞書型でセットされているrequest.POSTも for in ループに入れれば値を取り出すことができます。

なので、値の上書きも可能...と思うのですがQueryDictの規制で書き換え出来ずにエラーが発生します。

ここで活躍するのがQueryDict.copy()です。

#OKなコード
copy_post = request.POST.copy()

for ps in copy_post:
    if (ps == '')or(ps == 'None')
        ps = None

copy()で複製した辞書はQueryDictというカテゴライズは継承しますが上書き可能になります。これでForm/ModelFormに投げる『入力のないカラムをNoneに置き換えた値』を作ることができます。

置換なのにReplace関数は使えない?

Replaceできないかコード書いて実験してみます。

copy_post = request.POST.copy()

new_post = copy_post.replace('',None).replace('None',None)

これを走らせると下のようなエラーが発生します。

'QueryDict' object has no attribute 'replace'
訳:QueryDict' オブジェクトには 'replace' 属性がありません。

Copy()で変更できるようにしていてもQueryDictである事に変わりはないのでreplaceは使えないと。
QueryDictで使えるコマンドについてはこちらでご確認を。

最終的に出来上がった形

POSTされた空文字や ’None’ を置換するシーンは頻繁に起こり得ます。
なので、このパーツを外部ライブラリ独自関数として保存しViewから呼び出して利用する形に実装します。

■Project/project/Libraries/library.py

def BlankChanger(data):
    #''や’None’でPOSTされた値をNoneに置き換える
    copy_dict = data.copy()
    for cd in copy_dict:
        if (cd == '')or(cd == 'None'):
            cd = None
    return copy_dict

■Project/project/manager/views.py

from project.Libraries.library import BlankChanger 
(中略)
def CompanyDetailView(request, pk):
    object = Company.objects.get(pk=pk)
    #更新の流れ
    if (request.method == 'POST'):
        #独自関数呼び出し
        p_data = BlankChanger(request.POST)

        Recode = CompanyCreateForm(p_data, instance=object)
        Recode.save()
        messages.success(request, 'レコードを更新しました。')
        return redirect(to='/managedcontents/company/'+ str(pk) +'/edit')
    #POSTがない時
    params = {
        'object': object,
        'url': '/managedcontents/company/'+str(pk)+'/edit/'
    }
    return render(request, 'CRUD/Company_edit.html', params)

■Project/project/manager/forms.py

from django import forms
from .models import Company

class CompanyCreateForm(forms.ModelForm):
    class Meta:
        model = Company
        fields = (
            'corporates',
            'postal_code',
            'prefecture',
            'municipalities',
            'details',
            'buildings',
            'tel_number',
            'fax_number',
            'email',
            'website',
            'lang',
        )
    def __int__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'

■Project/project/manager/models.py

from django.db import models
from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _

class Company(models.Model):
    """ 企業情報 """
    corporates = models.CharField(verbose_name='企業名', max_length=200, blank=False, null=True)
    postal_code_regex = RegexValidator(regex=r'^[0-9]+$', message = _("Postal Code must be entered in the format: '1234567'. Up to 7 digits allowed."))
    postal_code = models.CharField(validators=[postal_code_regex], max_length=7, verbose_name='郵便番号', null=True)
    prefecture = models.CharField(verbose_name='県名', max_length=10, blank=True, null=True)
    municipalities = models.CharField(verbose_name='市町村', max_length=30, blank=True, null=True)
    details = models.CharField(verbose_name='番地詳細', max_length=50, blank=True, null=True)
    buildings = models.CharField(verbose_name='建物名', max_length=150, blank=True, null=True)
    tel_number_regex = RegexValidator(regex=r'^[0-9]+$', message = _("Tel Number must be entered in the format: '09012345678'. Up to 15 digits allowed."))
    tel_number = models.CharField(validators=[tel_number_regex], max_length=15, verbose_name='電話番号', null=True)
    fax_number_regex = RegexValidator(regex=r'^[0-9]+$', message = _("Tel Number must be entered in the format: '09012345678'. Up to 15 digits allowed."))
    fax_number = models.CharField(validators=[fax_number_regex], max_length=15, verbose_name='FAX番号', blank=True, null=True)
    email = models.EmailField(verbose_name='代表メール', blank=True, null=True)
    website = models.URLField(verbose_name='ホームページ', blank=True, null=True)
    lang = models.CharField(verbose_name='言語', max_length=10, blank=True, null=True)
    created_at = models.DateTimeField(verbose_name='作成日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)

    class Meta:
        verbose_name_plural = 'Company'

        def __str__(self):
            return self.corporates

■Project/project/manager/templates/CRUD/company_edit.html

{% extends "base.html" %}
{% if user.is_authenticated %}
{% block title %}企業情報{% endblock title %}
{% block stylesheets %}
  {{ block.super }}
{% endblock stylesheets %}
{% load humanize %}
{% block content %}
  <div class="right_col" role="main">
    <div class="container">
      <div class="row">
        <h1 class="card-header">企業情報編集</h1>
        <hr>
        <form action="{{url}}" method="POST">
        {% csrf_token %}
        <div class="col-md-6 col-sm-12">
          <div class="col-md-4">採用言語</div>
          <div class="col-md-8">
            <select class="form-control" name="lang">
              <option value="{{object.lang}}" selected="selected">
              {% if object.lang == 'ja' %}
                日本語
              {% elif object.lang == 'en' %}
                English
              {% else %}
                選択ください
              {% endif %}
              </option>
              <option value='ja'>日本語</option>
              <option value='en'>English</option>
            </select>
          </div>
          <div class="col-md-4">社名</div>
          <div class="col-md-8">
            <input type="text" name="corporates" class="form-control" value="{{object.corporates}}">
          </div>
          <div class="col-md-4">電話番号</div>
          <div class="col-md-8">
            <input type="text" name="tel_number" class="form-control" value="{{object.tel_number}}">
          </div>
          <div class="col-md-4">FAX番号</div>
          <div class="col-md-8">
            <input type="text" name="fax_number" class="form-control" value="{{object.fax_number}}">
          </div>
          <div class="col-md-4">代表mail</div>
          <div class="col-md-8">
            <input type="text" name="email" class="form-control" value="{{object.email}}">
          </div>
          <div class="col-md-4">WEBSITE</div>
          <div class="col-md-8">
            <input type="text" name="website" class="form-control" value="{{object.website}}">
          </div>
        </div>
        <div class="col-md-6 col-sm-12">
          <div class="col-md-4">郵便番号</div>
          <div class="col-md-8">
            <input type="text" name="postal_code" class="form-control" value="{{object.postal_code}}">
          </div>
          <div class="col-md-4">都道府県</div>
          <div class="col-md-8">
            <input type="text" name="prefecture" class="form-control" value="{{object.prefecture}}">
          </div>
          <div class="col-md-4">市町村</div>
          <div class="col-md-8">
            <input type="text" name="municipalities" class="form-control" value="{{object.municipalities}}">
          </div>
          <div class="col-md-4">地区番地</div>
          <div class="col-md-8">
            <input type="text" name="details" class="form-control" value="{{object.details}}">
          </div>
          <div class="col-md-4">ビル/建物</div>
          <div class="col-md-8">
            <input type="text" name="buildings" class="form-control" value="{{object.buildings}}">
          </div>
        </div>
        <div class="col-sm-offset-2 col-sm-10">
          <button type="submit" class="btn btn-primary">送信</button>
        </div>
      </form>
      </div>
    </div>
  </div>
{% endblock content %}

html以外はだいぶ軽くなりました。

まとめ

Templateがまだボリューム満載ですが、forms.pyも極めていけばこれくらいのレスポンシブを{{ form }}で呼び出せるようになるのかな。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください