django-orm-cheatsheet - DjangoORMのチートシート

(A cheatsheet for the Django ORM)

Created at: 2022-05-08 03:53:06
Language: NULL
License: MIT

DjangoORMチートシート

たくさんのテキストを読みたい場合は、Djangoのドキュメント、特にこれらのページを読んでいるでしょう。

しかし、あなたはそれを望まないので、あなたはここにいます。要点を説明します。

私たちのモデル

from datetime import date

from django.db import models


class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

    def __str__(self):
        return self.name


class Author(models.Model):
    name = models.CharField(max_length=200)
    email = models.EmailField()

    def __str__(self):
        return self.name


class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateField()
    mod_date = models.DateField(default=date.today)
    authors = models.ManyToManyField(Author)
    number_of_comments = models.IntegerField(default=0)
    number_of_pingbacks = models.IntegerField(default=0)
    rating = models.IntegerField(default=5)

    def __str__(self):
        return self.headline

出発点

Entry.objects.all()

# SELECT "blog_entry"."id", "blog_entry"."blog_id", ...
# FROM "blog_entry" ...;

# [<Entry: Off foot official kitchen another turn.>,
#  <Entry: Yet year marriage yes her she.>,
#  <Entry: Special mission in who son sort.>,
#  ...
# ]

クエリを減らす

これらはあなたの支出に見合う最大の価値です。それらを頻繁に使用します。

select_related()

* 1対1の関係(1対1、多対1)

それなし
select_related()

entry = Entry.objects.first()
# SELECT ... FROM "blog_entry" ...;

# this attribute access runs a second query
blog = entry.blog
# SELECT ... FROM "blog_blog" WHERE ...;

select_related()

entry = Entry.objects.select_related("blog").first()
# SELECT "blog_entry"."id", ... "blog_blog"."id", ...
# FROM "blog_entry"
# INNER JOIN "blog_blog" ...;

blog = entry.blog
# no query is run because we JOINed with the blog table above

prefetch_related()

*対多の関係(1対多、多対多)

それなし
prefetch_related()

entry = Entry.objects.first()
# SELECT ... FROM "blog_entry" ...;

# this related query hits the database again
authors = list(entry.authors.all())
# SELECT ...
# FROM "blog_author" INNER JOIN "blog_entry_authors"...
# WHERE "blog_entry_authors"."entry_id" = 4137;

prefetch_related()

entry = Entry.objects.prefetch_related("authors").first()
# SELECT ... FROM "blog_entry" ...;
# SELECT ...
# FROM "blog_author" INNER JOIN "blog_entry_authors" ...
# WHERE "blog_entry_authors"."entry_id" IN (4137);

authors = list(entry.authors.all())
# no query is run because we have an in-memory
# lookup of the relevant authors from above

update()

それなし
update()

david = (
    Author.objects.filter(name__startswith="David")
    .prefetch_related("entry_set")
    .first()
)
# SELECT ... FROM "blog_author" WHERE "blog_author"."name" LIKE 'David%' ...;
# SELECT ... FROM "blog_entry" INNER JOIN "blog_entry_authors" ...;

# There are many ineffcient ways to update all of David's entries.
# This is one of them.
for entry in david.entry_set.all():
    entry.rating = 5
    entry.save()
# One query for each Entry. Even if we used bulk_update(),
# we're still making more queries than we need.

update()

Entry.objects.filter(authors__name__startswith="David").update(rating=5)
# UPDATE "blog_entry" SET "rating" = 5 WHERE "blog_entry"."id" IN ...;

列以外のクエリ、別名はPythonではなくDBで作業を行います

annotate()
(初見)

annotate()
結果の各行に属性を追加します。これを使用して
F()
、関連オブジェクトから1つのフィールドを追加します(これは、関係の長いチェーンで区切られたオブジェクトがある場合に、より意味があります)。

annotate()
は非常に強力で、多くの状況で使用するのが理にかなっているので、この最初の非現実的な例に耐えてください。

entries = Entry.objects.annotate(blog_name=F("blog__name"))[:5]
# SELECT "blog_entry"."id", ... "blog_blog"."name" AS "blog_name"
# FROM "blog_entry" INNER JOIN "blog_blog" ...;

# Now we have Entry objects with one extra attribute: blog_name
[entry.blog_name for entry in entries]
# ['Hunter-Rhodes',
#  'Mcneil PLC',
#  'Banks, Hicks and Carpenter',
#  'Anderson PLC',
#  'George-Bray']

filter()
Q()

データベースでより多くのフィルタリングを実行し、アプリケーションでより少ないフィルタリングを実行するために使用

Q()
します。

low_engagement_posts = Entry.objects.filter(
    Q(number_of_comments__lt=20) | Q(number_of_pingbacks__lt=20)
)

list(low_engagement_posts)
# SELECT ... FROM "blog_entry"
# WHERE
#   ("blog_entry"."number_of_comments" < 20 OR
#   "blog_entry"."number_of_pingbacks" < 20);

クエリ式

Django ORMの多くの部分は、クエリ式を期待しています。クエリ式には次のものがあります。

  • フィールドへの参照(おそらく関連オブジェクト上)
    F()
  • 、など
    CASE
    のSQL関数
    NOW
  • とのサブクエリ
    Subquery()

F()

古典的な例は増分ですが

F()
、クエリ式が必要な場所ならどこでも使用できることを思い出してください。

Entry.objects.filter(authors__name__startswith="David").first().rating
# 5

Entry.objects.filter(authors__name__startswith="David").update(
    rating=F("rating") - 1
)
# UPDATE "blog_entry"
#   SET "rating" = ("blog_entry"."rating" - 1) WHERE ...;

Entry.objects.filter(authors__name__startswith="David").first().rating
# 4

CASE
および条件付きロジック

Djangoドキュメントページ

Value()
以下は、文字列、整数、ブール値などのリテラル値のクエリ式です。値として、または条件の代わりに、
F()
または別の式を使用できます。
then
rating=

entries = Entry.objects.annotate(
    coolness=Case(
        When(rating=5, then=Value("super cool")),
        When(rating=4, then=Value("pretty cool")),
        default=Value("not cool"),
    )
)
# SELECT ...
# CASE
#   WHEN "blog_entry"."rating" = 5 THEN 'super cool'
#   WHEN "blog_entry"."rating" = 4 THEN 'pretty cool'
#   ELSE 'not cool'
# END AS "coolness"
# FROM "blog_entry" LIMIT 5;

[f"Entry {e.pk} is {e.coolness}" for e in entries[:5]]
# ['Entry 4137 is super cool',
#  'Entry 4138 is not cool',
#  'Entry 4139 is not cool',
#  'Entry 4140 is pretty cool',
#  'Entry 4141 is not cool']

Subquery()
OuterRef()

最新のエントリの見出しで各ブログに注釈を付けます。

このパターンは、私が今までに見つけた唯一の用途です

Subquery()
。*対多の関係をクエリし
OuterRef("pk")
、行を「結合」し、
values()
1つの列
[:1]
を返し、サブクエリから1つの行を返すために使用します。これは基本的に、Djangoドキュメントの例のコピーです。

blogs = Blog.objects.annotate(
    most_recent_headline=Subquery(
        Entry.objects.filter(blog=OuterRef("pk"))
        .order_by("-pub_date")
        .values("headline")[:1]
    )
)
[(blog, str(blog.most_recent_headline)) for blog in blogs[:5]]
# SELECT
#   "blog_blog"."id",
#   "blog_blog"."name",
#   "blog_blog"."tagline",
#   (
#     SELECT
#       U0."headline"
#     FROM
#       "blog_entry" U0
#     WHERE
#       U0."blog_id" = ("blog_blog"."id")
#     ORDER BY
#       U0."pub_date" DESC
#     LIMIT
#       1
#   ) AS "most_recent_headline"
# FROM
#   "blog_blog"
# LIMIT
#   5;


# [(<Blog: Robinson-Wilson>, 'Three space maintain subject much.'),
#  (<Blog: Anderson PLC>, 'Rock authority enjoy hundred reduce behavior.'),
#  (<Blog: Mcneil PLC>, 'Visit beyond base.'),
#  (<Blog: Smith, Baker and Rodriguez>, 'Tree look culture minute affect.'),
#  (<Blog: George-Bray>, 'Then produce tree quality top similar.')]

より少ないデータを選択

values()
(パート1)

指定したフィールドのみを辞書として選択します。次のように注釈を選択することもできます。

Entry.objects.annotate(num_authors=Count("authors")).values(
    "rating", "num_authors"
)[:5]
# SELECT
#   "blog_entry"."rating",
#   COUNT("blog_entry_authors"."author_id") AS "num_authors"
# FROM "blog_entry" LEFT OUTER JOIN "blog_entry_authors" ...;

# [{'num_authors': 3, 'rating': 1},
#  {'num_authors': 4, 'rating': 4},
#  {'num_authors': 4, 'rating': 2},
#  {'num_authors': 3, 'rating': 2},
#  {'num_authors': 3, 'rating': 5}]

exists()

何かが存在するというyes/noの答えが必要な場合、これは行全体をフェッチするよりも高速です。

unrealistic_data_exists = Entry.objects.filter(
    mod_date__lt=F("pub_date")
).exists()
# SELECT
#  (1) AS "a"
#   FROM "blog_entry"
#   WHERE "blog_entry"."mod_date" < ("blog_entry"."pub_date")
#   LIMIT 1;

unrealistic_data_exists
# True

only()

と同様

values()
ですが、辞書の代わりにモデルインスタンスが返されます。ただし、注意が必要です。最初にフェッチしなかったフィールドのいずれかにアクセスすると、追加のDBクエリが作成されます。

データのグループ化

values()
(パート2)と
GROUP BY

Djangoのドキュメントにはこの隠された宝石が含まれています:

「通常、アノテーションはオブジェクトごとに生成されます。アノテーションが付けられたQuerySetは、元のQuerySetのオブジェクトごとに1つの結果を返します。ただし、

values()
句を使用して結果セットに返される列を制約すると、...元の結果は、句で指定されたフィールドの一意の組み合わせに従ってグループ化されます
values()
。次に、一意のグループごとに注釈が提供されます。注釈は、グループのすべてのメンバーに対して計算されます。」

「グループ」という言葉がそこにあるのは、これが

GROUP BY
DjangoのORMで句を作成する方法だからです。レポートに非常に役立ちます。

ブログエントリの平均評価が最も高いのはどの日ですか。

[
    (str(e["pub_date"]), e["avg_rating"])
    for e in Entry.objects.values("pub_date").annotate(avg_rating=Avg("rating"))
]
# SELECT
#   "blog_entry"."pub_date",
#   AVG("blog_entry"."rating") AS "avg_rating"
# FROM "blog_entry"
# GROUP BY "blog_entry"."pub_date";

# [
#  ('2022-04-07', 2.235294117647059),
#  ('2022-04-08', 2.8157894736842106),
#  ('2022-04-09', 2.0285714285714285),
#  ('2022-04-10', 2.96875),
#  ('2022-04-11', 2.3636363636363638),
#  ('2022-04-12', 2.725),
#  ('2022-04-13', 2.5),
#  ('2022-04-14', 2.761904761904762),
#  ...
# ]

aggregate()

と同様

annotate()
ですが、各行に値を追加する代わりに、クエリを1行に減らします。