ORM и QuerySet: запросы к базе на Python

ORM — главное оружие Django. Через менеджер objects вы пишете запросы к базе на чистом Python, а Django переводит их в оптимальный SQL.
Суть: QuerySet — ленивая коллекция объектов. filter/exclude/order_by строят запрос, но SQL выполняется только при обращении к данным. Это даёт мощную композицию запросов.

Менеджер objects и QuerySet

У каждой модели есть менеджер objects — точка входа в базу. Через него вы получаете QuerySet — объект, представляющий выборку строк. Базовые операции читаются почти как английский язык:

Post.objects.all()                       # все записи
Post.objects.filter(is_published=True)   # с условием
Post.objects.exclude(is_published=True)  # с отрицанием
Post.objects.get(pk=42)                  # ровно одна запись
Post.objects.order_by("-created_at")     # сортировка (- = убыв.)
Post.objects.filter(title__icontains="django")  # поиск
Post.objects.count()                     # количество

Lookups — язык условий

Двойное подчёркивание — это синтаксис «лукапов», операторов сравнения. field__gt — больше, field__lt — меньше, field__icontains — содержит (без учёта регистра), field__in — входит в список, field__startswith — начинается с. Это покрывает почти все нужды без единой строчки SQL.

Ленивость QuerySet

Ключевая особенность: QuerySet ленив. Когда вы пишете qs = Post.objects.filter(...), запрос в базу ещё не уходит. SQL выполнится только когда вы реально обратитесь к данным: в цикле for, при list(qs), при срезе или len(). Поэтому фильтры можно накапливать и комбинировать без лишних обращений к базе:

qs = Post.objects.all()
if only_published:
    qs = qs.filter(is_published=True)
if author:
    qs = qs.filter(author=author)
# SQL выполнится здесь, один раз:
for post in qs:
    print(post.title)

Как это работает под капотом

QuerySet — это, по сути, накопитель условий, который при необходимости превращается в один SQL-запрос. Логика filter/exclude/order_by языко-независима: это фильтрация и сортировка списка словарей. Вот наглядная модель того, что делает ORM:

# Попробуй сам ▶ — мини-QuerySet над списком словарей
posts = [
    {"title": "Django ORM", "views": 1200, "published": True},
    {"title": "Черновик",   "views": 5,    "published": False},
    {"title": "Шаблоны",    "views": 800,  "published": True},
    {"title": "Формы",      "views": 300,  "published": True},
]

def query(data, **filters):
    result = data
    for key, value in filters.items():
        if key.endswith("__gt"):
            field = key[:-4]
            result = [r for r in result if r[field] > value]
        elif key.endswith("__icontains"):
            field = key[:-11]
            result = [r for r in result if value.lower() in r[field].lower()]
        else:
            result = [r for r in result if r[key] == value]
    return result

# аналог Post.objects.filter(published=True, views__gt=500)
hot = query(posts, published=True, views__gt=500)
for p in sorted(hot, key=lambda r: -r["views"]):  # order_by('-views')
    print(f'{p["views"]:>5}  {p["title"]}')

Django делает то же самое, но вместо перебора списка строит SQL с WHERE и ORDER BY и отдаёт работу базе — это в тысячи раз быстрее на больших объёмах.

get против filter

Важное различие: filter() возвращает QuerySet (ноль и более объектов), а get() возвращает ровно один объект. Если get() ничего не найдёт — бросит DoesNotExist, если найдёт несколько — MultipleObjectsReturned. Поэтому get() используют только когда уверены в единственности (по pk или unique-полю).

Частые ошибки

  • Использовать get() там, где данных может не быть. Получите исключение. Используйте filter().first() или get_object_or_404.
  • Думать, что filter сразу бьёт в базу. QuerySet ленив — запрос откладывается.
  • Делать запрос в цикле (проблема N+1). Об этом — отдельный урок про оптимизацию.
  • Путать exclude и filter. exclude — это «всё, КРОМЕ».

Best practices

  • Стройте запросы постепенно, пользуясь ленивостью QuerySet.
  • Для «найти или 404» используйте get_object_or_404(Post, pk=pk).
  • Берите только нужные поля через .only() или .values() на больших таблицах.
  • Логируйте SQL в разработке (Django Debug Toolbar), чтобы видеть реальные запросы.

Итоги

ORM даёт читаемый Python-язык запросов поверх SQL. filter, exclude, order_by и лукапы покрывают большинство задач. QuerySet ленив — это позволяет комбинировать условия без лишних запросов. get — для единственного объекта, filter — для выборки. Дальше свяжем модели между собой.

Проверьте себя
1. Когда QuerySet реально выполняет SQL-запрос?
AСразу при вызове filter()
BПри обращении к данным: итерация, list(), срез
CТолько при migrate
DНикогда, это всегда Python
2. Что произойдёт, если get() не найдёт ни одной записи?
AВернёт None
BВернёт пустой список
CБросит исключение DoesNotExist
DСоздаст новую запись