SE(たぶん)の雑感記

一応SEやっている筆者の思ったことを書き連ねます。会計学もやってたので、両方を生かした記事を書きたいと考えています。 でもテーマが定まってない感がすごい。

Django - ジェネリックビューで楽してみた(UpdateView編)

前回記事でCreateViewを扱いました。今回はUpdateViewについて書きます。CRUDUに相当します。

hiroronn.hatenablog.jp

バージョン等

前回と同様です。

ツール バージョン
Python 3.7.2
Django 2.1.7
bootstrap 4.1.3

前提:Model層

  • models.py
from django.db import models

class Table(models.Model):
    """テーブル一覧"""
    name = models.CharField(max_length=100)

class Column(models.Model):
    """カラム定義(物理名格納)"""
    table = models.ForeignKey(Table, on_delete=models.CASCADE)
    name = models.CharField(max_length=100)

前回と同様ですが、上記二つのModelを使います。

UpdateViewとは

UpdateViewは、既に登録されているデータを開き、更新するためのビューです。DBでいうとSELECTした後UPDATEを行います。

基本的には、対象は1行のみです。

UpdateViewの効果

自分で作るとしたら

特定の1行を更新しようとする場合、以下のような作業(ロジック)が必要になります。

  • 更新対象のレコードを何らかの形で選択する
  • 指定レコードがあるかどうかチェックする
  • レコードを取得する
  • 値を表示するためのHTML(form等)を作る
  • POSTが来たら入力された値を検証する(文字形式とか)
  • 再び、指定レコードを取得する*1
  • (アプリケーション要件に応じて)更新可否を判定する*2
  • 取得レコードに入力値を反映
  • 保存処理を呼び出す
  • ブラウザの画面を遷移させる

作成時より、考慮事項が多いです。

UpdateViewを使う場合

最も単純な場合、

  • 対象modelを指定する
  • 入力したいフィールドを指定する
  • URLに主キーを含める
  • 描画用HTMLを指定する(省略可)
  • 更新後の遷移先を指定する

'CreateView'とほとんど変わりませんが、主キーの指定が必要です。ただ本当にこれだけでよいです。

主キーの指定

UpdateViewを使う場合、URLに主キーを含めると楽です。以下のようになります。

  • urls.py
urlpatterns = [
    # table
    path('table/<int:pk>', table.TableDetailView.as_view(), name='table'),
    path('table/<int:pk>/update', table.TableUpdateView.as_view(), name='table_upd'), # UpdateView
]

主キーを受けるパラメータ名は、pkまたはslugにしましょう*3

根拠

UpdateViewの継承元、SingleObjectMixinに、以下の記述があります。

  • django/views/generic/detail.py
class SingleObjectMixin(ContextMixin):
    # 中略
    def get_object(self, queryset=None):
        """
        Return the object the view is displaying.

        Require `self.queryset` and a `pk` or `slug` argument in the URLconf.
        Subclasses can override this to return any object.
        """

ソースを見ると分かりますが、両方指定した場合はpkが優先して使用されます。

ビューの作成

例1:単体で完結するモデル

URLは上記の通りです。

  • views/table.py
class TableUpdateView(UpdateView):
    """テーブル更新"""
    model = Table
    fields = ('name', )
    template_name = 'inputs/create.html'

    def get_success_url(self):
        return reverse('inputs:table', kwargs={'pk': self.object.id})

前回記事で書いたget_success_urlは、UpdateViewでも同じように利用できます。これにより、更新後に更新が終わった明細を開けます。

テンプレートを作る必要はある*4ものの、これだけ指定するとデータ取得から更新まですべて行えます。

例2:リレーションを持つモデル

今回でいうところのColumnモデルです。

例えば、テンプレートにColumnに紐づいているテーブル名を表示したい、とします。その場合、テンプレートで

<p>
    <span>{{ column.table.name }}</span>
</p>

のような感じで書けばよいのですが、これだとテーブル名を取得する際に再度クエリが発行されてしまいます。

それを避けるために、前回も出てきたget_querysetをオーバーライドする方法があります。

class ColumnUpdateView(UpdateView):
    """名称変更"""
    model = Column
    template_name = "inputs/create.html"
    fields = ('name',)

    def get_queryset(self):
        return super().get_queryset().select_related('table')

    def get_success_url(self):
        return reverse('inputs:col', kwargs={'pk': self.object.id})

継承元のget_querysetで一行だけ取得するクエリが発行されるので、それと一緒に取得してしまおう、という考え方です。

まあ、たった一回増えるだけだしいいや、という考え方もあります。

リレーションの列を変える場合は、

class ColumnUpdateView(UpdateView):
    """名称変更"""
    model = Column
    template_name = "inputs/create.html"
    fields = ('name', 'table')

    def get_queryset(self):
        return super().get_queryset().select_related('table')

    def get_success_url(self):
        return reverse('inputs:col', kwargs={'pk': self.object.id})

と、fieldsに追加すればよいです。テンプレートでselect要素が作成され、選択できるようになります。

説明してみたものの、UpdateViewに関しては、単純である限りリレーションがあっても無くても、ほとんど同じように書けます。

疑問:レコードの有無はチェックされるのか

気になったので調べました。三点検証しています。

  1. ビューを開くとき、存在しないキーを指定したらどうなるのか
  2. 更新するとき、対象レコードが消されていたらどうなるのか
  3. 更新するとき、対象レコードが変更されていたらどうなるのか

f:id:hiroronn:20190314205018p:plainf:id:hiroronn:20190314205226p:plain
1.と2.の結果

結果として、画像の通り1.と2.はきちんと異常検知してくれます。3.は、そのまま更新されました。

他のクライアントが更新した場合のチェックは、必要なら実装する必要があります。更新日付列を持たせてチェックすると、多少堅牢になります。ただ、実装はアプリケーションによって異なるので、ここでは省略します。

おわりに

単一レコードの更新なら、UpdateViewを使うと実装が本当に楽です。ほとんど手組みする必要がありません。

Djangoの機能で最低限のチェックは行ってくれるので、ささっと作るだけならあっという間です。慣れると10分ぐらいでできます。本当に。モデルもテンプレートも無いと、そこまで素早くはできませんが…

次はDeleteViewなのですが、たぶん短くなるので、きっと別のことも一緒に書きます。

*1:更新対象のレコードが、他のクライアントから削除されている可能性がある

*2:何もしない、楽観的、悲観的というもの。参考:https://docs.microsoft.com/ja-jp/dotnet/framework/data/adonet/optimistic-concurrency

*3:変えることもできるが、特別な事情が無い限りそのままのほうが楽

*4:見た目にこだわらなければ使いまわしできる