【Django】005. セッションとミドルウェア

前回はDjangoのフォームを使ったデータの送信について見ていきました。
[前回記事：【Django】004. フォームのさまざまなフィールド](https://qiita.com/ys_dirard/items/8367abada50cd1892a46)

今回はDjangoのセッションとミドルウェアについて見ていきます。

今回も以下の本を参考にしています。

https://www.shuwasystem.co.jp/book/9784798062419.html

## セッション

## セッションとは？
フォームでクライアント送られてきた情報をもとに処理は行えますが、次にアクセスしたときにはこの情報は失われています。

こういう状況で役に立つのがセッションです。

以下雑な説明ですが…(少し間違っているかも…)
セッションはクライアントとサーバー間の接続を維持するための仕組みで「今、アクセスしているのはこの人だ！」とサーバー側が把握し、そのクライアントが持っている情報を保管できるようにするものです。


### Djangoでのセッション利用の準備
Djangoではセッションは元からプロジェクトに組み込まれています。

settings.pyのINSTALLED_APPSを見ると既に使用できるようになっていることがわかります。

```python
INSTALLED_APPS = [
    ...(略)...
    "django.contrib.sessions",
    ...(略)...
]
```

というわけで特別な準備は不要です。


### セッションの基本操作

セッションはHttpRequestインスタンスのsessionという属性に保管されている。

これはSessionBaseというクラスを継承したものとして定義されているらしい。

#### セッションに値を保管する
```python
<HttpRequest>.session["キー"] = 値
```

#### セッションから値を取り出す
```python
変数 = <HttpRequest>.session["キー"]
```

ただし、この値の取得方法は通常しないらしく、getとpopというメソッドを使って行っていくようです。

* 値を取り出す
```python
<HttpRequest>.session.get(キー, デフォルト値)
```

* 値を取り出し、セッションから取り除く
```python
<HttpRequest>.session.pop(キー, デフォルト値)
```

これらは取り出す際に指定したキーの値が存在しなければ、代わりにデフォルト値が返されます。これにより値がなかった場合もエラーにならない。


### セッションを利用する

実際にセッションを使ってみます。

#### Formクラスの準備
セッション用のフォームを以下として用意します。
```python
class SessionForm(forms.Form):
    session = forms.CharField(label="session", required=False, widget=forms.TextInput(attrs={"class": "form-control"}))

```
ここではsessionというCharFieldを用意。
ここに入力して送信した値をセッションに保管する。
※これ自体はセッションではありません。

#### views.pyの準備

ビューを以下のように準備します。
```python
class SessionView(TemplateView):
    def __init__(self):
        self.params = {
            "title": "Hello",
            "form": SessionForm(),
            "message": None,
        }
        
    def get(self, request):
        self.params["message"] = request.session.get("last_msg", "No message.")
        return render(request, "hello/session.html", self.params)
    
    def post(self, request):
        sess = request.POST["session"] # フォームからの取得
        self.params["message"] = "send: '" + sess + "'."
        request.session["last_msg"] = sess # セッションに保管
        self.params["form"] = SessionForm(request.POST)
        return render(request, "hello/session.html", self.params)
```

テンプレートはsession.htmlとして以下としています。
```html:session.html
{% load static %}
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>{{ title }}</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">

</head>
<body class="container">
    <h1 class="display-4 text-primary mb-4">{{ title }}</h1>
    <p class="h6 my-3">{{ message|safe }}</p>
    
    <form action="{% url 'session' %}" method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="click" class="btn btn-primary">
    </form>
</body>
</html>

```
初回アクセス時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/26448671-fba4-5c25-22fb-05c1dc49b25c.png)

送信時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/60112f0c-c7b1-b92a-93d2-8697f88994ee.png)

再アクセス時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/109f17e2-7511-d716-9066-599f1279102f.png)

再アクセス時も情報が消えることなく表示されていることが確認できます。

:::note info
以下のエラー
no such table: django_session
が出た場合は一度サーバーを止めてmigrateすると良いみたいです。
$ python manage.py migrate
:::

#### セッションの機能で覚えておいた方が良いもの

##### セッションの全キー / 値を取得する
```python
<SessionBase>.keys()
<SessionBase>.items()
```
セッションに保管されているすべてのキー / 値を配列として取得する。
keysはキーをitemsは値を取得するもの。

##### セッションの全値を消去する
```python
<SessionBase>.clear()
<SessionBase>.flush()
```
セッションに保管されているすべての値を消去します。
clearはただ値を削除するだけ、flushはセッション用のクッキーも削除する。

##### 保存期間を設定する
```python
<SessionBase>.set_expiry(整数)
```
セッションは永久的に値を保管するわけではなく、一定時間経過すると自動的に消えます。
最後にセッションを使ってからセッションが消えるまでの期間をset_expiryで設定します (単位は秒)。

## ミドルウェア

### ミドルウェアとは？
ミドルウェアはクライアントからの要求を受けて処理を行う過程に割り込んで何らかの処理を実行させるためのプログラム。

実は、セッションのミドルウェアを追加するとクライアントがアクセスした際、リクエストにセッションのための機能を作成して使えるようにしていたということのようです。

settings.pyのMIDDLEWAREで確認できます。
```python
MIDDLEWARE = [
    ...(略)...
    "django.contrib.sessions.middleware.SessionMiddleware",
    ...(略)...
]
```

ミドルウェアはクライアントからのリクエストとレスポンスの間に割り込んで様々な処理と行います。

これはアクセスがあると常に呼び出され実行されます。

ミドルウェアを用意することでDjangoのアクセス処理に独自の機能を追加することも可能です。

処理のフローは以下の図のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/96ff0d3f-b44f-3a8e-27cd-2e387d5ae9ce.png)

### ミドルウェアの基本形

ミドルウェアは関数またはクラスとして定義します。
ここでは関数の例を見ていきます。

* ミドルウェアの基本形
```python
def funcA(get_responce):
    ...(初期化処理)...

    def funcB(request):
        ...(ビューを実行する前の処理)...
        response = get_response(request)
        ...(ビューを実行した後の処理)...
        return response

    return funcB
    
```

ミドルウェアは関数名で識別されます(ここではfuncA)。

引数のget_responseにはリクエストからレスポンスを取得するための**関数**が渡されます。

ミドルウェアの関数内にさらに関数 (ここではfuncB) が定義され、これがミドルウェアの本体部分となります。

この内部の関数には引数としてHttpRequestが渡されます。

funcAではfuncBをreturnすることでミドルウェアの処理が設定されるようになっています。

処理は先ほどの図を見ていただけるとイメージしやすいかと思います。

### ミドルウェアを作成する

実際にミドルウェアを作成してみます。
```python:views.py
def sample_middleware(get_response):
    
    def middleware(request):
        counter = request.session.get("counter", 0)
        request.session["counter"] = counter + 1
        response = get_response(request)
        print("count:", str(counter))
        return response
    
    return middleware
    
```

今回のミドルウェアではセッションからcounterの値を取得し、1増やし、printしています。

アクセスするたびにセッションのcounterの値が1ずつ増えていくものになっています。

### ミドルウェアの登録
作成したミドルウェアは以下のようにsettings.pyのMIDDLEWAREに登録する必要があります。

```python:settings.py
MIDDLEWARE = [
    ...(略)...
    "hello.views.sample_middleware",
]
```

ミドルウェアはアクセスするたびに常に動作するので、アクセスするページに限らず実行されます。
その様子が以下のように確認できました。

```
count: 0
[25/Jul/2024 15:42:10] "GET /hello/session HTTP/1.1" 200 871
count: 1
[25/Jul/2024 15:42:15] "POST /hello/session HTTP/1.1" 200 876
count: 2
[25/Jul/2024 15:42:15] "POST /hello/session HTTP/1.1" 200 876
count: 3
[25/Jul/2024 15:42:16] "POST /hello/session HTTP/1.1" 200 876
count: 4
[25/Jul/2024 15:42:17] "POST /hello/session HTTP/1.1" 200 876
count: 5
[25/Jul/2024 15:42:17] "POST /hello/session HTTP/1.1" 200 876
count: 6
[25/Jul/2024 15:42:18] "POST /hello/session HTTP/1.1" 200 876
count: 7
[25/Jul/2024 15:42:52] "GET /hello/ HTTP/1.1" 200 521
count: 8
[25/Jul/2024 15:43:01] "GET /hello/form HTTP/1.1" 200 858
```