【Django】Qオブジェクトを使って検索するサンプルコードを6種類解説

# 概要
Djangoのソースコードで`Q`オブジェクトを使いました。
本記事では実際にサンプルコードを使って実行してみたので、紹介します。

## 前提
`Q`オブジェクトとは、Djangoのクエリを構築する際、複数の条件を組み合わせてデータベースからデータをフィルタリングするために使われます。大体の場合、`filter()`メソッドとセットで使われるはずです（`get`は単一のレコードを取得することが目的のため）。

今回は国名を検索するコードとして、`Q`オブジェクトを使います。
モデル定義は以下の通り。

```models.py
class Country(models.Model):
    name = models.CharField(max_length=100, null=True, blank=True)
    capital = models.CharField(max_length=100, null=True, blank=True)
    population = models.IntegerField(null=True, blank=True)
    continent = models.CharField(max_length=50, null=True, blank=True)
    language = models.CharField(max_length=50, null=True, blank=True)
    currency = models.CharField(max_length=50, null=True, blank=True)

    class Meta:
        db_table = 'country'
```

テーブルには、以下のようにレコードが入っている前提です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710373/f4cefdb0-8044-51b8-a47b-cb4db315a7e4.png)



# サンプルコード解説
以下のサンプルコードを紹介します。

- OR検索
- AND検索
- 複数条件の組み合わせ検索
- 部分文字列の一致検索
- 否定条件の検索

### OR検索を行う

一般的に、`Q`オブジェクトを使うときは以下のようにOR条件を扱いたい場合が多いと思います。

```py
from response.models import Sample, Country

from django.db.models import Q

# 人口が1億以上または南米に属する国を取得するクエリの作成
q_obj = Q(population__gte=100000000) | Q(continent='South America')

# Qオブジェクトを使ってフィルタリングする
results = Country.objects.filter(q_obj)

# 結果の表示
print("人口が1億以上または南米に属する国は...")
for country in results:
    print(f"[{country.id}] 国名: {country.name}, 人口: {country.population}, 地域: {country.continent}")
```

↓出力結果

```
人口が1億以上または南米に属する国は...
[1] 国名: Japan, 人口: 123294513, 地域: Asia
[2] 国名: USA, 人口: 340870790, 地域: North America
[6] 国名: China, 人口: 1425433798, 地域: Asia
[7] 国名: Brazil, 人口: 216422446, 地域: South America
```

`gte`は「以上」を意味しています。ちなみに`gt`だと「〜より大きい」になります。
同じように、`lte`が「以下」、`lt`が「未満」です。

また、上記の`q_obj`の`type`をプリントすると、以下になります。

```
<class 'django.db.models.query_utils.Q'>
```

DjangoのORMでクエリ条件を構築するために使われるQオブジェクトの型であることがわかります。

### OR検索を行う（その２）

```py
# 英語を公用語とする国またはヨーロッパに属する国を取得するクエリの作成
q_obj = Q(language='English') | Q(continent='Europe')

# Qオブジェクトを使ってフィルタリングする
results = Country.objects.filter(q_obj)

# 結果の表示
print("英語を公用語とする国またはヨーロッパに属する国は...")
for country in results:
    print(f"[{country.id}] 国名: {country.name}, 人口: {country.population}, 地域: {country.continent}")
```

↓出力結果

```
英語を公用語とする国またはヨーロッパに属する国は...
[2] 国名: USA, 人口: 340870790, 地域: North America
[3] 国名: France, 人口: 64816918, 地域: Europe
[4] 国名: United Kingdom, 人口: 68000000, 地域: Europe
[5] 国名: Germany, 人口: 83274299, 地域: Europe
```

ちなみに以下のように`startswith`を使用して「Euro」から始まる文字列の一致検索を行うこともできます。

```py
q_obj = Q(language='English') | Q(continent__startswith='Euro')
```

### AND検索を行う

```py
# 人口が7000万人以上かつヨーロッパに属する国を取得するクエリの作成
q_obj = Q(population__gte=70000000, continent='Europe')

# Qオブジェクトを使ってフィルタリングする
results = Country.objects.filter(q_obj)

# 結果の表示
print("人口が7000万人以上かつヨーロッパに属する国は...")
for country in results:
    print(f"[{country.id}] 国名: {country.name}, 人口: {country.population}, 地域: {country.continent}")
```

↓出力結果

```
人口が7000万人以上かつヨーロッパに属する国は...
[5] 国名: Germany, 人口: 83274299, 地域: Europe
```

AND条件も可能ですが、`filter()`メソッドだけでも複数の条件をカンマでつなげれば可能ですので、`Q`オブジェクトを使う旨味はあまりなさそう。ですので、AND条件を使う必要がある場合は、以下の「複数の条件を組み合わせたい」時になるかと思います。

### 複数の条件を組み合わせた検索を行う

以下のように`&`を使うことで、異なる検索条件の`Q`オブジェクトをAND検索で指定することが可能になります。

```py
# 「人口が3億以上」または「人口が5000万から1億未満かつヨーロッパに属する」国を取得するクエリの作成
q_obj = (Q(population__gte=300000000) |
         (Q(population__range=(50000000, 99999999)) & Q(continent='Europe')))

# Qオブジェクトを使ってフィルタリングする
results = Country.objects.filter(q_obj)

# 結果の表示
print("「人口が3億以上」または「人口が5000万から1億未満かつヨーロッパに属する」国は...")
for country in results:
    print(f"[{country.id}] 国名: {country.name}, 人口: {country.population}, 地域: {country.continent}")

```

↓出力結果

```
「人口が3億以上」または「人口が5000万から1億未満かつヨーロッパに属する」国は...
[2] 国名: USA, 人口: 340870790, 地域: North America
[3] 国名: France, 人口: 64816918, 地域: Europe
[4] 国名: United Kingdom, 人口: 68000000, 地域: Europe
[5] 国名: Germany, 人口: 83274299, 地域: Europe
[6] 国名: China, 人口: 1425433798, 地域: Asia
```


### 部分文字列の一致検索を行う

`icontains`を使って部分文字列の一致を行うことができます。

```py
# continentフィールドで"America"を含む国
q_obj = Q(continent__icontains='America')

# Qオブジェクトを使ってフィルタリングする
results = Country.objects.filter(q_obj)

# 結果の表示
print("アメリカ大陸にある国は...")
for country in results:
    print(f"[{country.id}] 国名: {country.name}, 人口: {country.population}, 地域: {country.continent}")
```

↓出力結果

```
アメリカ大陸にある国は...
[2] 国名: USA, 人口: 340870790, 地域: North America
[7] 国名: Brazil, 人口: 216422446, 地域: South America
```

ちなみに、`icontains`は大文字と小文字を区別せずに部分文字列を検索するためのもので、`contains`は完全に指定した文字列を含んでいるかをチェックする際に使用されます。

上記の例だと例えばブラジルのレコードで大陸を「South america」にした状態で`contains`にして検索を行うと、「america」と「America」で区別されるのでブラジルがヒットしません。


### 否定条件で検索を行う

最後に。
否定条件で検索を行う場合は、`Q`オブジェクトに`~`を指定します。
特定の条件を満たさないレコードをフィルタリングしたいときに使えますね。

```py
# 貨幣がEuroの国を否定（NOT）
not_q_obj = ~Q(currency='Euro')

# 否定条件を使ってフィルタリングする
results = Country.objects.filter(not_q_obj)

# 結果の表示
print("貨幣がEuroではない国は...")
for country in results:
    print(f"[{country.id}] 国名: {country.name}, 人口: {country.population}, 地域: {country.continent}")
```

↓出力結果

```
貨幣がEuroではない国は...
[1] 国名: Japan, 人口: 123294513, 地域: Asia
[2] 国名: USA, 人口: 340870790, 地域: North America
[4] 国名: United Kingdom, 人口: 68000000, 地域: Europe
[6] 国名: China, 人口: 1425433798, 地域: Asia
[7] 国名: Brazil, 人口: 216422446, 地域: South America
```