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 — для выборки. Дальше свяжем модели между собой.