【Django】008. Managerクラスによるレコードの取得

前回までで、モデル/テーブルの作成、管理画面でのテーブルのダミーデータ作成を行いました。

[前回記事：【Django】007. 管理ツール＋管理画面でのデータベースCRUD](https://qiita.com/ys_dirard/items/70d90727de3e70c32c6a)

今回はDjangoのアプリケーションからManagerクラスを利用してデータベースを利用していきます。(今回はレコードの取得)



## Managerクラスとは？

公式ドキュメントにて以下のように記載されています。

> マネージャ (Manager) とは、Django のモデルに対するデータベースクエリの操作を提供するインターフェイスです。Django アプリケーション内の1つのモデルに対して、Manager は最低でも1つは存在します。
>
> Manager クラスの詳細については、クエリを作成する に書かれています。ここでは、特に、Manager の動作をカスタマイズするモデルのオプションについて説明しています。

https://docs.djangoproject.com/ja/5.1/topics/db/managers/

通常はデータベースを操作するのにSQLを使用することが多いと思いますが、Managerクラスを使用することでSQLを避けてデータベースの操作が行えます。

さらに、Djangoのモデルに対し自動でManagerが作成されるため特別な準備は不要！(というように読み取れます)


今回使用する前回までに作成したFriendモデルを再掲しておきます。
```python
class Friend(models.Model):
    name = models.CharField(max_length=100)
    mail = models.EmailField(max_length=200)
    age = models.IntegerField(default=0)
    birthday = models.DateField()
    
    def __str__(self):
        return '<Friend:id=' + str(self.id) + ', ' + \
            self.name + '(' + str(self.age) + ')>'

```

## レコードの表示
helloというアプリケーションを作成したという設定で、helloアプリケーションのviews.pyに以下を追加します。

```python:views.py
from django.shortcuts import render
from .models import Friend

def show_record(request):
    data = Friend.objects.all()
    params = {
        'title': 'Hello',
        'massage': 'all friends',
        'data': data,
    }
    return render(request, 'hello/show_record.html', params)

```

Friendテーブルのレコードをすべて取得する方法になります。
```python
data = Friend.objects.all()
```
の部分で取得を行っています。

ここで登場したobjectsというのがManagerクラスのインスタンスとしてモデルクラスに設定されているものになります。

ManagerクラスのインスタンスであるFriend.objectsを通してallというメソッドを呼び出しレコードをモデルのインスタンス(の集合)として取得するという流れになります。

## 取得した内容の表示
テンプレート(ここではshow_record.htmlとしました)を以下のようにします。

```html:show_record.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">{{ title }}</h1>
    <p class="h5 mt-4">{{ message | safe }}</p>
    
    <table class="table">
        <tr>
            <th>ID</th>
            <th>NAME</th>
            <th>GENDER</th>
            <th>MAIL</th>
            <th>AGE</th>
            <th>BIRTHDAY</th>
        </tr>
        {% for item in data %}
        <tr>
            <td>{{ item.id }}</td>
            <td>{{ item.name }}</td>
            <td>{% if item.gender == False %}male{% endif %}
                {% if item.gender == True %}female{% endif %}</td>
            <td>{{ item.mail }}</td>
            <td>{{ item.age }}</td>
            <td>{{ item.birthday }}</td>
        </tr>
        {% endfor %}
    </table>
</body>
</html>

```

views.pyにてパラメータとして渡したdata(取得したレコードの集合)がイテラブルなオブジェクトとして渡されfor分で取得するという内容です。

アクセス数と以下のように表示され、ちゃんと取得できていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/d5485512-0720-0512-e1cf-5aeef39249fc.png)

## 特定のレコードを取り出す
allメソッドで全てのレコードを取得する方法はわかりましたが、毎回すべてが必要になるわけではありません。

特定の条件に一致するレコードを取得してみます。

今回はブラウザ上でidを指定して一致するものを取り出すということにします。

### Formの準備
まず、id入力用のフォームとして以下を準備します。

```python:forms.py
from django import forms

class FriendForm(forms.Form):
    id = forms.IntegerField(label="ID")

```

### テンプレートの変更
先ほどのshow_record.htmlへ以下のようにformを追加します。

```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">{{ title }}</h1>
    <p class="h5 mt-4">{{ message | safe }}</p>
    <form action="{% url 'show_record' %}" method="post">
        {% csrf_token %}
        {{ form }}
        <input type="submit" value="click">
    </form>
    
    <table class="table">
        <tr>
            <th>ID</th>
            <th>NAME</th>
            <th>GENDER</th>
            <th>MAIL</th>
            <th>AGE</th>
            <th>BIRTHDAY</th>
        </tr>
        {% for item in data %}
        <tr>
            <td>{{ item.id }}</td>
            <td>{{ item.name }}</td>
            <td>{% if item.gender == False %}male{% endif %}
                {% if item.gender == True %}female{% endif %}</td>
            <td>{{ item.mail }}</td>
            <td>{{ item.age }}</td>
            <td>{{ item.birthday }}</td>
        </tr>
        {% endfor %}
    </table>
</body>
</html>

```

### ビュー関数の変更
ビュー関数をpostリクエスト時の動作を追加します。
※これまでの処理をpost以外の時(実質ほぼget)の処理として残す

```python
from django.shortcuts import render
from .forms import FriendForm
from .models import Friend

def show_record(request):
    params = {
        'title': 'Hello',
        'massage': 'all friends',
        'form': FriendForm(),
        'data': [],
    }
    if (request.method == 'POST'):
        id = request.POST['id']
        item = Friend.objects.get(id=id)
        params['data'] = [item]
        params['form'] = FriendForm(request.POST)
    else:
        params['data'] = Friend.objects.all()
    return render(request, 'hello/show_record.html', params)

```

Managerクラスのgetというメソッドを使用しています。
引数でid=idとして条件を指定しています。

### 動作確認
アクセスすると以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/da113f7a-9af9-b611-e21e-eb181fa7e5e4.png)

試しに2としてクリックすると以下のようにちゃんと抽出されていることが確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/5fa4bf7f-b0e0-fa69-4e99-7960ba004022.png)

## allメソッドで取得されたモデル/レコードの集合を少し詳しく見る
テンプレートを以下に変更してみます。

```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">{{ title }}</h1>
    <p class="h5 mt-4">{{ data }}</p>
    
    <table class="table">
        <tr>
            <th>data</th>
        </tr>
        {% for item in data %}
        <tr>
            <td>{{ item }}</td>
        </tr>
        {% endfor %}
    </table>
</body>
</html>

```

すると以下のような表示になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/16e3d6ba-b00a-7aab-4379-3fcee5679ea3.png)

allメソッドで取得したレコードの集合はQuerySetというクラスのインスタンスであったことがわかります。

```<Friend:id=1, taro(20)>```等の表記はモデルクラスに定義した```__str__```メソッドの内容なので、一つ一つの要素はモデルクラスのインスタンスだとわかります。

QuerySetには様々なメソッドが用意されています。
例：
|メソッド名|詳細|
|-|-|
|values|モデルの中から値だけを取り出す|
|values_list|値をリストとして取り出す|

### valuesメソッド
ビュー関数を以下のように変更します。
```python
def show_record(request):
    data = Friend.objects.all().values()
    params = {
        'title': 'Hello',
        'data': data,
    }
    return render(request, 'hello/show_record.html', params)
    
```

アクセスすると以下のようにモデルクラスとしてではなく、dictとして取得できていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/3fd76ba6-b357-395e-a1c8-b7c3e6448549.png)

### valuesメソッドで特定項目を取り出す
ビュー関数を以下のように変更します。
```python
def show_record(request):
    data = Friend.objects.all().values('id', 'name')
    params = {
        'title': 'Hello',
        'data': data,
    }
    return render(request, 'hello/show_record.html', params)
```
values関数の引数にて取得したい項目名を指定しています。

アクセスすると以下のように指定した項目のみ取得できていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/c6caff9e-ccdc-fa99-d351-8dd633429aa4.png)


### values_listメソッド
ビュー関数を以下のように変更します。
```python
def show_record(request):
    data = Friend.objects.all().values_list('id', 'name')
    params = {
        'title': 'Hello',
        'data': data,
    }
    return render(request, 'hello/show_record.html', params)
```

すると以下のようになります。
リストと言いつつタプルで返ってきていることがわかります。(細かいことは気にしない…。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/13801682-392c-3c4e-f6fd-f7d7223a4ce3.png)

### QuerySetのその他のメソッド
ここでは以下3つを使ってみます。

|メソッド名|詳細|
|-|-|
|first|allなどで得られたレコードのうち最初のものだけを返す|
|last|allなどで得られたレコードのうち最後のものだけを返す|
|count|allなどで得られたレコード数を返す|

```python
def show_record(request):
    num = Friend.objects.all().count()
    first = Friend.objects.all().first()
    last = Friend.objects.all().last()
    data = [num, first, last]
    params = {
        'title': 'Hello',
        'data': data,
    }
    return render(request, 'hello/show_record.html', params)
```

表示は以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/56706a07-ab2f-e2a2-ca89-411f424a32ce.png)

上から順にcountによる要素数、firstによるレコード、lastによるレコードが確認できました。

ここで見てきたようにallメソッドとメソッドチェーンでvalues / values_list / first / last / countメソッド等をつなぐことで幅広い処理を行うことが可能になります。

## QuerySetの表示をカスタマイズする
```python
from django.shortcuts import render
from .models import Friend
from django.db.models import QuerySet

def __new_str__(self):
    result = ''
    for item in self:
        result += '<tr>'
        for k, v in item:
            result += '<td>' + str(k) + '=' + str(v) + '</td>'
        result += '</tr>'
    return result

QuerySet.__str__ = __new_str__

def show_record(request):
    data = Friend.objects.all().values('id', 'name', 'age')
    params = {
        'title': 'Hello',
        'data': data,
    }
    return render(request, 'hello/show_record.html', params)
```

Pythonの言語仕様を悪用するパターンです。
もともと用意されているクラスでも__str__メソッドを上書きすることができ、それにより好きなように表示させることができます。

確認のためテンプレートを以下に変更します。(bodyのみ記載)
```html
<body class="container">
    <h1 class="display-4 text-primary">{{ title }}</h1>
    <table class="table">
        {{ data | safe }}
    </table>
</body>
```

アクセスすると以下のように表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/aa21761b-3c35-6ebd-0fd7-dfd5a9376092.png)

__new_str__メソッドで指定した通りの記載になっていることが確認できました。

既存の特殊メソッドを書き換えるのはなんとなくアンチパターンな気もしますが、使い方次第では良いのかもしれませんね。