👋

Djangoのモデルで結合関連(多対1)

2024/08/21に公開

DjangoのモデルでQuerySetの結合関連(多対1)

すぐに忘れてしまうので、モデルを利用したクエリ処理をを備忘録としてまとめておく

前提となるモデル

models.py
class Category(models.Model):
  class Meta:
    db_table = "category"

  name = models.CharField(
    max_length=100,
  )
  
  def __str__(self):
    return self.name
models.py
class Author(models.Model):
  class Meta:
    db_table = "author"

  name = models.CharField(
    max_length=100,
  )
  
  age = models.IntegerField(
  )
  
  def __str__(self):
    return self.name
models.py
class Book(models.Model):
  class Meta:
    db_table = "book"

  title = models.CharField(
    max_length=100,
  )
  
  price = models.IntegerField(
  )
  
  pub_date = models.DateField(
  )
  
  author = models.ForeignKey(
    Author,
    on_delete=models.CASCADE,
  )
  
  category = models.ForeignKey(
    Category,
    on_delete=models.CASCADE,
  )

  def __str__(self):
    return self.title

categoryのレコード

+----+----------+
| id | name     |
+----+----------+
|  1 | sf       |
|  2 | love     |
|  3 | business |
|  4 | mystery  |
|  5 | history  |
+----+----------+

authorのレコード

+----+-------+-----+
| id | name  | age |
+----+-------+-----+
|  1 | Bob   |  20 |
|  2 | Tom   |  21 |
|  3 | Nancy |  25 |
|  4 | Meg   |  20 |
|  5 | Amy   |  24 |
|  6 | Cindy |  20 |
+----+-------+-----+

authorのレコード

+----+-------+-------+------------+-----------+-------------+
| id | title | price | pub_date   | author_id | category_id |
+----+-------+-------+------------+-----------+-------------+
|  1 | aaa   |  1000 | 2020-04-10 |         1 |           1 |
|  2 | bbb   |  1200 | 2020-05-05 |         1 |           2 |
|  3 | ccc   |  1500 | 2021-10-10 |         2 |           3 |
|  4 | ddd   |  1000 | 2020-06-20 |         3 |           2 |
|  5 | eee   |  1800 | 2021-08-15 |         3 |           4 |
|  6 | fff   |  1400 | 2020-07-20 |         3 |           2 |
|  7 | ggg   |  1200 | 2022-03-10 |         4 |           1 |
|  8 | hhh   |  1200 | 2022-04-20 |         5 |           4 |
|  9 | iii   |  1500 | 2022-08-10 |         5 |           1 |
| 10 | jjj   |  1200 | 2022-09-10 |         5 |           1 |
| 11 | kkk   |  1000 | 2022-04-05 |         5 |           4 |
| 12 | lll   |  1500 | 2022-10-10 |         5 |           2 |
+----+-------+-------+------------+-----------+-------------+

多対1、1対1の関係

多対1の場合、select_related(1側のモデル名) を利用します。

「select_related」は必須でないので、記載しなくても1側を取得可能だが、キャッシュが有効になるのでパフォーマンスが向上します。ループする場合、1+N問題が解決するので、必ず利用します

多側のモデル名.objects.select_related("1側のモデル名").all()

多対1の結合

select_relatedで1側のモデル名を指定します。「JOIN」になります。「author」のみを結合しているので、「category」は結合していない

Book.objects.select_related("author")
SELECT
    `book`.`id`,
    `book`.`title`,
    `book`.`price`,
    `book`.`pub_date`,
    `book`.`author_id`,
    `book`.`category_id`,
    `author`.`id`,
    `author`.`name`,
    `author`.`age`
FROM
    `book`
INNER JOIN
    `author`
ON
    (`book`.`author_id` = `author`.`id`)

多対1で複数の1側を結合

select_relatedで複数の1側のモデル名を指定します。「JOIN」になります。「author」「category」を結合します

Book.objects.select_related("author","category")
SELECT
    `book`.`id`,
    `book`.`title`,
    `book`.`price`,
    `book`.`pub_date`,
    `book`.`author_id`,
    `book`.`category_id`,
    `author`.`id`,
    `author`.`name`,
    `author`.`age`,
    `category`.`id`,
    `category`.`name`
FROM
    `book`
INNER JOIN
    `author`
ON
    (`book`.`author_id` = `author`.`id`)
INNER JOIN
    `category`
ON
    (`book`.`category_id` = `category`.`id`)

結合して多側のカラムで検索

多対1で結合して、多側のカラムで検索します。「JOIN」と「WHERE」です。select_relatedとfilterを併用します。順番はどちらでもよい

Book.objects.select_related("author").filter(price=1000)
SELECT
    `book`.`id`,
    `book`.`title`,
    `book`.`price`,
    `book`.`pub_date`,
    `book`.`author_id`,
    `book`.`category_id`,
    `author`.`id`,
    `author`.`name`,
    `author`.`age`
FROM
    `book`
INNER JOIN
    `author`
ON
    (`book`.`author_id` = `author`.`id`)
WHERE
    `book`.`price` = 1000

結合して1側のカラムで検索

多対1で結合して、1側のカラムで検索します。「JOIN」と「WHERE」です。select_relatedとfilterを併用します。順番はどちらでもよい。filetrの引数で「1側のモデル名__フィールド名=値」を指定します

Book.objects.select_related("author").filter(author__id=1)
SELECT
    `book`.`id`,
    `book`.`title`,
    `book`.`price`,
    `book`.`pub_date`,
    `book`.`author_id`,
    `book`.`category_id`,
    `author`.`id`,
    `author`.`name`,
    `author`.`age`
FROM
    `book`
INNER JOIN
    `author`
ON
    (`book`.`author_id` = `author`.`id`)
WHERE
    `book`.`author_id` = 1

結合して1側のカラムで検索

1側のカラムが「name」で「Amy」と等しい。filterの引数を「1側のモデル名__フィールド名=値」にします

Book.objects.select_related("author").filter(author__name="Amy")
SELECT
    `book`.`id`,
    `book`.`title`,
    `book`.`price`,
    `book`.`pub_date`,
    `book`.`author_id`,
    `book`.`category_id`,
    `author`.`id`,
    `author`.`name`,
    `author`.`age`
FROM
    `book`
INNER JOIN
    `author`
ON
    (`book`.`author_id` = `author`.`id`)
WHERE
    `author`.`name` = 'Amy'

結合して複数の検索条件

「category」と「author」を結合します。filterで「category」の「name」と「book」の「price」で検索します。「price」はルックアップタイプの「gte」を利用します

Book.objects \
    .select_related("author","category") \
    .filter(category__name="mystery",price__gte=1400)
SELECT
    `book`.`id`,
    `book`.`title`,
    `book`.`price`,
    `book`.`pub_date`,
    `book`.`author_id`,
    `book`.`category_id`,
    `author`.`id`,
    `author`.`name`,
    `author`.`age`,
    `category`.`id`,
    `category`.`name`
FROM
    `book`
INNER JOIN
    `category`
ON
    (`book`.`category_id` = `category`.`id`)
INNER JOIN
    `author`
ON
    (`book`.`author_id` = `author`.`id`)
WHERE
    (
        `category`.`name` = 'mystery'
    AND
        `book`.`price` >= 1400
    )

結合に利用したフィールドで検索

結合に利用しているフィールドで検索します。1側のカラムが不要であれば、select_relatedは不要です。当然ですが…。

Book.objects.filter(author=2)
SELECT
    `book`.`id`,
    `book`.`title`,
    `book`.`price`,
    `book`.`pub_date`,
    `book`.`author_id`,
    `book`.`category_id`
FROM
    `book`
WHERE
    `book`.`author_id` = 2

以下は同じこと

Book.objects.filter(author=2)
Book.objects.filter(author__id=2)
Book.objects.filter(author__pk=2)

filterで1側のカラムで検索

select_relatedしなくても、「1側のモデル名__カラム名=値」でfilterすると結合される。ここでは、「1側のモデル名__カラム名__ルックアップタイプ=値」でfilterしています

 Book.objects.filter(author__age__lte=20)
SELECT
    `book`.`id`,
    `book`.`title`,
    `book`.`price`,
    `book`.`pub_date`,
    `book`.`author_id`,
    `book`.`category_id`
FROM
    `book`
INNER JOIN
    `author`
ON
    (`book`.`author_id` = `author`.`id`)
WHERE
    `author`.`age` <= 20

結合と件数

結合して、検索して、件数を行います

Book.objects.select_related("author").filter(author__name="Amy").count()
SELECT
    COUNT(*) AS `__count`
FROM
    `book`
INNER JOIN
    `author`
ON
    (`book`.`author_id` = `author`.`id`)
WHERE
    `author`.`name` = 'Amy'

valuesでカラム名を指定して取り出す

「author」を結合します。filterで「author」の「name」で検索します。valuesで特定のカラムを取り出します。「book.id」「book.title」「book.price」「author.name」を取り出します。

valuesの引数で各フィールドを指定します。多側のカラム名のみを指定します。1側は「モデル名__フィールド名」で指定します

valuesなので、ディクショナリ形式のQuerySetが返ります

Book.objects \ 
    .select_related("author") \
    .filter(author__name="Amy") \
    .values("id","title","price","author__name")
SELECT
    `book`.`id`,
    `book`.`title`,
    `book`.`price`,
    `author`.`name`
FROM
    `book`
INNER JOIN
    `author`
ON
    (`book`.`author_id` = `author`.`id`)
WHERE
    `author`.`name` = 'Amy'

Discussion