【Django】ClassBasedViewで共有変数と初期値を設定する


ClassBasedViewで共有変数を設定して、重複クエリをなくすのが目的です。

## 条件

* id、pk、slugを指定しない
* ClassBasedViewを使う
* DetailView
* FormMixin
* post機能
* 重複したクエリを発生させない

## 前置き

これを利用するシーンとしては、プロフィール編集機能を持たせたマイページのようなものを想定している。

要は、UpdateViewとDetailViewを一緒にしたようなものだ。

ちなみに、`def（関数ベースビュー）`を使えば簡単に実装できる笑

が、可読性を上げるために？あえてClassBasedViewで実装した。

この手の記事が英文でも無かったので、記録として残しておく。

## 悩んだ問題点

初期値を設定する必要があったので、`get_initial()`メソッドでデータベースから初期値を設定するが、`get_context_data()`メソッドでも同様のクエリを発生させなければいけなかったので、どうしてもクエリが重複してしまった。

`def`であれば、一度変数に代入し使い回せるのだが、ClassBaseViewだとそうもいかない。

ClassBasedViewで共有して使える変数を設定することはできないのだろうか？

## setup()で設定できた

`DetailView`には、`setup`メソッドがある。

この`setup`メソッドのおかげで、`self.`をつけさえすれば、request、args、kwargsがどのメソッドでも使える様になっている。

```python:
def setup(self, request, *args, **kwargs):
    """Initialize attributes shared by all view methods."""
    if hasattr(self, "get") and not hasattr(self, "head"):
        self.head = self.get
    self.request = request
    self.args = args
    self.kwargs = kwargs
```

ようはここに`self.HOGE`をオーバーライドすればどのメソッドでも変数として使える。

サンプルとして適当にクラスを作ってみる。

```python
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import DetailView
from django.views.generic.edit import FormMixin

class SampleView(LoginRequiredMixin,　FormMixin, DetailView):
    model = User
    form_class = UserEditForm
    template_name = 'myapp/mypage.html'
    success_url = reverse_lazy('myapp:mypage')

    # 共有の変数を設定
    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        # self.hoge_listが使い回せるようになる。
        if request.user.is_authenticated:
            self.hoge_list = request.user.m2m.all()

    # id等を指定しないのでget_objectメソッドを使う
    def get_object(self):
        return self.request.user

    # フォームにデータベースからの初期値の設定
    def get_initial(self):
        initial = super().get_initial()
        ctx = {
            'username': self.request.user.username,
            # 上で追加したオブジェクト
            'hoges': self.hoge_list
        }
        initial.update(ctx)
        return initial

    # post処理
    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        form = UserEditForm(request.POST, request.FILES, instance=request.user)
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)
    
    # 成功時の処理
    def form_valid(self, form):
        form.save()
        messages.info(self.request, '更新しました')
        return super().form_valid(form)

    # contextの作成
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user = self.request.user
        hoge_list = self.hoge_list
        ...
        ctx = {
            'user': user, 'hoge_list': hoge_list
        }
        context.update(ctx)
        return context
```

`setup`メソッドに、`self.HOGE`を設定しておくと、クエリの重複を防ぐことができるのでなかなか便利。

ちなみに`get_object()`で使いまわしてみたが、クエリが通常の倍近くになった（当たり前か笑）

ClassBaseViewは便利ではあるが、構造を理解してないと予期しないエラーを吐きそうで怖い。

複雑な処理をする場合は、defの方が安心感があるのはご愛嬌。

## おまけ

* `super()`を最初で呼び出すか？
* return後に`super()`を呼び出すか？
* 親コードを持ってきて改変する
* `hoge = super().method()`で代入するか？
* `initial['hoge']`の形か？
* `ctx = {'hoge': hoge}`の形か？

initial['hoge']で初期値を設定すると、タプルで返ってくるので注意。

あと、`　FormMixin`と`DetailView`を組み合わせる場合は、formとviewを別にして、別で合体させるのが良いらしい。

https://docs.djangoproject.com/en/4.0/topics/class-based-views/mixins/#a-better-solution