【Django】010. 検索をマスターする

前回はCRUDをプログラムから行う方法を見ていきました。
[前回記事：【Django】009. CRUDをプログラムから行う](https://qiita.com/ys_dirard/items/7bd46b6aa05fec87ba13)

今回はデータベースの検索について見ていきます。

具体的にはManagerクラスのfilterを使って条件の絞り込み方を見ます。

フィルター自体は以下のように簡単に使うことができます。

```python
変数 = <Model>.objects.filter(フィルター内容)
```

今回も前回までと同様、helloアプリケーションのFriendモデルを使用します。

```python
class Friend(models.Model):
    name = models.CharField(max_length=100)
    mail = models.EmailField(max_length=200)
    gender = models.BooleanField()
    age = models.IntegerField(default=0)
    birthday = models.DateField()
    
    def __str__(self):
        return '<Friend:id=' + str(self.id) + ', ' + \
            self.name + '(' + str(self.age) + ')>'

```

## 検索ページの作成
検索用のページを準備します。

以下の順番で行います。
1. urlpatternsの修正
1. 検索用Formクラスの作成
1. 検索用テンプレートの作成
1. 検索用ビュー関数の作成

### urlpatternsの修正
urls.pyのurlpatternsに検索用ページを追加します。
```python
urlpatterns = [
    ...(略)...
    path('find', views.find, name='find'), # 追加
]
```

### 検索用Formクラスの作成
forms.pyへ以下のFindFormクラスを追加します。
```python
class FindForm(forms.Form):
    find = forms.CharField(label='Find', required=False, widget=forms.TextInput(attrs={'class': 'form-control'}))
```

findというCharFieldを1つ持つだけです。

ここに入力っされた値をもとに検索を行います。

### 検索用テンプレートの作成
find.htmlを作成します。

```html:find.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>{{ message | safe }}</p>
    <form action="{% url 'find' %}" method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="click" class="btn btn-primary">
    </form>
    <table class="table">
        <tr>
            <th>id</th>
            <th>name</th>
            <th>mail</th>
        </tr>
        {% for item in data %}
        <tr>
            <td>{{ item.id }}</td>
            <td>{{ item.name }}({{ item.age }})</td>
            <td>{{ item.mail }}</td>
        </tr>
        {% endfor %}
    </table>
</body>
</html>

```

messageという名前でメッセージを受け取り表示、dataという名前でモデルのレコードのリストを受け取り表示するようにしています。


### 検索用ビュー関数の作成
views.pyにビュー関数としてfind関数を追加します。

```python:views.py
from .forms import FindForm

def find(request):
    if (request.method == 'POST'):
        form = FindForm(request.POST)
        find = request.POST['find']
        data = Friend.objects.filter(name=find)
        msg = 'Result: ' + str(data.count())
    else:
        msg = 'search words...'
        form = FindForm()
        data = Friend.objects.all()
    params = {
        'title': 'Hello',
        'message': msg,
        'form': form,
        'data': data,
    }
    return render(request, 'hello/find.html', params)
    
```

Managerのfilterはシンプルな使い方としては以下のようになります。
```python
        find = request.POST['find'] # 検索ワードの取得
        data = Friend.objects.filter(name=find) # 検索の実行
```
今回はname項目の値がfindであるものを検索してください！ということにしています。

### 動作確認
```localhost:8000/hello/find```にアクセスすると以下のように表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/8e2690f4-5a42-2132-4616-fc03a4827e0e.png)

Find欄に入力してClickを押すと

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/47616116-9e08-33f2-ec47-39e8aabfbc1c.png)

検索が実行された様子が確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/414a8e2e-6aff-1ef9-1774-f4bfda8f0cd6.png)

```filter(項目名=値)```では完全一致での検索になるようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/d400e405-03b9-f5c5-128f-2576765b1596.png)

完全一致以外の検索を行う場合はfilter関数の引数の「項目名＝値」の書き方に少し手を加えることで実現可能です。

## LIKE検索
値を含むとかある値から始まる/終わるものを探したいときに使われます。

* 値を含むものを検索
```python
項目名__contains=値
```
* 値で始まるものを検索
```python
項目名__startswith=値
```
* 値で終わるものを検索
```python
項目名__endswith=値
```

例えばname__contains='taro'とするとtaroはもちろん、shotaroやtarobee等も検索に罹るようになります。

## 大文字小文字の区別
これまでの検索は大文字と小文字を区別して検索します。
大文字と小文字の区別をしない検索は検索条件の部分の先頭に「i」を追加します。

* 大文字小文字を区別しない検索(完全一致)
```python
項目名__iexact=値
```
* 大文字小文字を区別しないLIKE検索
```python
項目名__icontains=値
項目名__istartswith=値
項目名__iendswith=値
```

数値の比較
* 値と等しい (equal)
```python
項目名=値
```
* 値よりも大きい (greater than)
```python
項目名__gt=値
```
* 値と等しいか大きい (greater than or equal)
```python
項目名__gte=値
```
* 値よりも小さい (less than)
```python
項目名__lt=値
```
* 値と等しいか小さい (less than or equal)
```python
項目名__lte=値
```

## 複数条件での検索

### A以上B未満 (AND)
AND検索は以下のように実行可能
```python
変数 = <Model>.objects.filter(1つ目の条件, 2つ目の条件)
```

年齢(age)が20代の人のみを検索する場合は20以上30未満とすればよいので以下のように検索可能です。

```python
data = Friend.objects.filter(age__gte=20, age__lt=30)
```

AND検索は以下のようにfilter関数を繰り返し行うことでも実現可能です。

```python
data = Friend.objects
    .filter(age__gte=20)
    .filter(age__lt=30)
```

### AまたはB (OR)
AND検索は以下のように実行可能
```python
from django.db.models import Q

変数 = <Model>.objects.filter(Q(1つ目の条件) | Q(2つ目の条件))
```
OR検索を実行するためには検索条件からQというモデルそれぞれ作成し「|」でつなぐということになります。

入力された値をnameかmailが含むものを検索する場合は以下となる。
```python
val = 検索する値
data = Friend.objects.filter(Q(name__contains=val) | Q(mail__contails=val))
```

### Listに含まれるものすべて (IN)
検索したい値が複数ある場合に一つずつOR検索をする等はとても面倒です。そういう時に便利です。

```python
変数 = <Model>.objects.filter(項目名__in=リスト)
```

## まとめ
今回はManagerのfilter関数によるデータベースの検索を見ていきました。

基本的な検索はできるようになれたかと思います。

もっと複雑な検索が必要になったらその時に頭を使うなり調べるなりします。