【Django】クエリセットの和集合を使ってみたのでサンプルコード付で解説

# 概要
Djangoにおけるクエリセットの和集合「` | `」を使ってみました。
サンプルコードで解説します。

## 前提
以下のように、国に関するDBテーブルを作成。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710373/6647194f-e8f8-ec9b-66e5-d8906a65ba8e.png)


# サンプルコード

以下のようにして使います。
ここでは、アジアとヨーロッパの国を取得するクエリセットを作成し、その後に和集合を利用して結合しています。

```py
# from django.db import models
from response.models import Country

asia_countries = Country.objects.filter(continent='Asia')
europe_countries = Country.objects.filter(continent='Europe')

countries = asia_countries | europe_countries

print(countries)

## 出力結果 ##
## <QuerySet [<Country: Country object (1)>, <Country: Country object (3)>, <Country: Country object (4)>, <Country: Country object (5)>, <Country: Country object (6)>]>

for country in countries:
    print(country.name)

## 出力結果 ##
# Japan
# France
# United Kingdom
# Germany
# China
```

上記の通り、クエリセットの出力だけでは、国名などのデータは表せないので、`country.name`のようにしてあげれば出力できます。

### 和集合とは？
和集合とは 2 つの集合の少なくともどちらか 1 つに含まれる要素の集合のこと。

Djangoのクエリセットに対する和集合（ユニオン）操作は、2つ以上のクエリセットから要素を取得する必要がある場合に便利です。上記のように「アジアとヨーロッパ」の両方の国を一度に取得したい場合など。

ちなみに、

```py
countries = asia_countries.union(europe_countries)
```

という書き方でも可能です。

同様に、以下の集合演算では下記のように使用します。

積集合：`&`または`intersection()`
差集合：`-`または`difference()`
対称差集合：`^`または`symmetric_difference`


### クエリセットを利用する事例

クエリセットのままの方が使いやすい、という簡単な事例として、`Country`モデルと関連づけられたモデルのデータを取得したいケースがあります。

```py
data = countries.select_related('capital', 'language').values('capital__area', 'language__language_abbreviation_name')
```

`select_related`は、データベースのJOIN操作を行い、関連するオブジェクトを一度のクエリで取得するメソッド。私は、外部キーを通じて関連付けられたモデルに対して使用するのが多いです。DBへのクエリ数も減少しますので。

`values`メソッドは、指定したフィールドの値を取得できます。指定したフィールドは、`Country`モデルまたは`Capital`、`Language`モデルのフィールドである必要があります。返すのは、モデルのインスタンスではなく、辞書のリストなので、取ってきたものを何か処理したい場合に使えます。

これにより、`Country`モデルと関連する`Capital`と`Language`モデルから、それぞれ指定したフィールドの値を取得できるようになります。

`select_related`（と`values`メソッド）を紹介した記事は以前書いたのでご参考までに。

https://qiita.com/Ryo-0131/items/dd4b9dce699508d6740b


あと、`values`や`values_list`、`first`や`distinct`がそれぞれ何を返すんだ？みたいになることがあったので、以下記事でもそれらをまとめました。

https://qiita.com/Ryo-0131/items/5f7721d980587b1520c8


## （補足）和集合を活用できるケース
上記サンプルコードでは、和集合に限らずとも実現できると思います。

しかし、とある条件の`for`文でぐるぐるクエリセットを回しながら、さらに指定した条件に該当したもののみを、空のクエリセットに入れてあげる、という処理の時には役に立つと思いました。

`append()`や`+=`のイメージです。
これらはクエリセットに対しては使えないので、和集合が活用できます。

```py
asia_and_europe_countries = Country.objects.filter(continent__in=['Asia', 'Europe'])
query_set = Country.objects.none()

for country in asia_and_europe_countries.values('language', 'currency').distinct():
    selected_countries = asia_and_europe_countries.filter(
        language=country['language'],
        currency=country['currency'],
    )

    ng_countries = selected_countries.filter(xx_flag='NG')
    ok_countries = selected_countries.filter(xx_flag='OK').exists()

    if ng_countries.exists() and not ok_countries:
        query_set = query_set | ng_countries

    else :
        # 略....
```

上記の場合、
「アジアおよびヨーロッパにある国々」の中で、「同じ言語と通貨を使った国々」のうち、「`xx_flag`がNGの国」しか存在しない場合、その国（国々）を`query_set`に追加していっています。つまり、とあるグルーピングの中で条件指定を行い、さらにそのグループの中のレコードのフラグが全て立っていた場合のみ、それ（それら）を抽出しています。

例えば、「アジアおよびヨーロッパ」にある「hoge語」と「foo通貨」を使っている国々が、全て`xx_flag`NGの場合、それ（それら）が`query_set`に代入されます。「hoge語」と「foo通貨」を使っている国々の中で一つでも`OK`の国があれば、それらはいずれも代入されません。「hoge語」と「foo通貨」の国々は全てNGの国なのか！？を判断した上で必要な国のみを抽出しています。