Прежде чем писать код в представлениях, удобно проверять ORM-запросы в интерактивной оболочке. Django предоставляет расширенную версию Python shell, в которой уже настроено окружение проекта:
python manage.py shellВсе примеры этого урока можно запускать прямо в shell. Начнём с импорта модели:
from films.models import Film, GenreПрежде чем разбирать CRUD, важно понять два ключевых объекта Django ORM.
Manager — это точка входа для работы с таблицей через ORM. По умолчанию Django создаёт менеджер с именем objects для каждой модели. Через него мы и делаем все запросы:
Film.objects # это менеджерQuerySet — это ленивый запрос к базе данных. Большинство методов менеджера возвращают QuerySet, а не список объектов. SQL выполняется только в момент обращения к данным: при итерации, срезе или явном приведении к списку.
qs = Film.objects.filter(year__gt=2000) # SQL ещё не выполнен
films = list(qs) # вот здесь выполнился SELECTЭто важно понимать при отладке: напечатать QuerySet в shell — это тоже момент выполнения запроса.
Связь с прошлым курсом. В модуле 4 прошлого курса мы писали классы-репозитории, которые инкапсулировали SQL-запросы к базе. Manager в Django — это встроенный репозиторий: он уже прикреплён к каждой модели и предоставляет стандартный набор методов. Пользовательский менеджер, который мы разберём в конце урока, — это и есть расширение этого репозитория.
Самый короткий способ — метод create(), который создаёт объект и сразу сохраняет его в БД:
film = Film.objects.create(
title='Крёстный отец',
year=1972,
description='История семьи Корлеоне.',
rating=9.2,
slug='krestnyj-otec'
)Создаём объект вручную и явно вызываем save():
film = Film(
title='Список Шиндлера',
year=1993,
rating=8.9,
slug='spisok-shindlera'
)
film.save() # только здесь выполняется INSERTЭтот способ полезен, когда нужно дополнительно обработать объект перед сохранением.
Создаёт запись, если её нет, или возвращает существующую. Возвращает кортеж (объект, создан):
film, created = Film.objects.get_or_create(
slug='pobeg-iz-shoushenko',
defaults={
'title': 'Побег из Шоушенка',
'year': 1994,
'rating': 9.3,
}
)
print(created) # True — если создан, False — если уже существовалПоля в defaults используются только при создании. Поиск идёт по полям вне defaults — в данном случае по slug.
films = Film.objects.all()
# SELECT * FROM films_film ORDER BY -year (с учётом Meta.ordering)film = Film.objects.get(id=1)
film = Film.objects.get(slug='krestnyj-otec')get() возвращает ровно один объект. Если запись не найдена — выбрасывает Film.DoesNotExist. Если найдено больше одной — Film.MultipleObjectsReturned. Поэтому get() используют только с уникальными полями.
# Фильмы после 2000 года
films = Film.objects.filter(year__gt=2000)
# Фильмы с рейтингом выше 8
high_rated = Film.objects.filter(rating__gte=8.0)
# Все фильмы кроме драм (исключение)
not_drama = Film.objects.exclude(genres__name='Драма')filter() возвращает QuerySet с объектами, удовлетворяющими условию. exclude() — инверсия: возвращает объекты, не удовлетворяющие условию.
# По году (возрастание)
films = Film.objects.order_by('year')
# По году (убывание) — минус перед именем поля
films = Film.objects.order_by('-year')
# По нескольким полям
films = Film.objects.order_by('-rating', 'title')order_by() переопределяет сортировку из Meta.ordering.
# Первые 5 фильмов
top5 = Film.objects.all()[:5]
# Записи с 5 по 10
page = Film.objects.all()[5:10]Срез QuerySet транслируется в SQL-конструкцию LIMIT / OFFSET.
# Количество записей
count = Film.objects.count()
count = Film.objects.filter(year__gt=2000).count()
# Проверка существования (быстрее чем count() > 0)
exists = Film.objects.filter(slug='krestnyj-otec').exists()Field Lookups — это синтаксис для задания условий в filter(), exclude() и get(). Он выглядит как поле__оператор:
Film.objects.filter(year__gt=2000)
# ^^^^ ^^
# поле оператор (greater than)Связь с прошлым курсом. В SQL мы писали
WHERE year > 2000,WHERE title LIKE '%отец%'. Field Lookups — это то же самое, но в Python-синтаксисе. Каждый lookup транслируется в соответствующую SQL-конструкцию.
| Lookup | SQL-эквивалент | Пример |
|---|---|---|
exact (по умолчанию) |
= 'значение' |
filter(year=1972) |
gt |
> значение |
filter(year__gt=2000) |
gte |
>= значение |
filter(rating__gte=8.0) |
lt |
< значение |
filter(year__lt=1990) |
lte |
<= значение |
filter(year__lte=2000) |
in |
IN (...) |
filter(year__in=[1972, 1993, 1994]) |
range |
BETWEEN ... AND ... |
filter(year__range=(1990, 2000)) |
| Lookup | SQL-эквивалент | Пример |
|---|---|---|
contains |
LIKE '%значение%' |
filter(title__contains='отец') |
icontains |
ILIKE '%значение%' |
filter(title__icontains='отец') |
startswith |
LIKE 'значение%' |
filter(title__startswith='Кр') |
endswith |
LIKE '%значение' |
filter(title__endswith='ка') |
istartswith |
без учёта регистра | filter(title__istartswith='кр') |
Префикс i означает case-insensitive (без учёта регистра). Для поиска по строкам почти всегда используют icontains вместо contains.
# Фильмы без описания
Film.objects.filter(description__isnull=True)
# Фильмы с описанием
Film.objects.filter(description__isnull=False)Несколько вызовов filter() подряд работают как AND:
# Фильмы с рейтингом выше 8 И годом после 1990
films = Film.objects.filter(rating__gte=8.0).filter(year__gt=1990)
# То же самое — через запятую внутри одного filter()
films = Film.objects.filter(rating__gte=8.0, year__gt=1990)film = Film.objects.get(id=1)
film.rating = 9.5
film.save()save() по умолчанию обновляет все поля. Если нужно обновить только одно — используй update_fields, чтобы не перезаписывать лишнее:
film.rating = 9.5
film.save(update_fields=['rating'])
# UPDATE films_film SET rating = 9.5 WHERE id = 1# Установить рейтинг 0 всем фильмам до 1950 года
Film.objects.filter(year__lt=1950).update(rating=0.0)update() выполняет один SQL-запрос UPDATE и возвращает количество обновлённых строк. Это намного эффективнее, чем загружать все объекты и вызывать save() для каждого.
film = Film.objects.get(id=1)
film.delete()# Удалить все фильмы с рейтингом ниже 5
deleted_count, _ = Film.objects.filter(rating__lt=5.0).delete()
print(deleted_count) # количество удалённых записейdelete() возвращает кортеж: количество удалённых объектов и словарь с детализацией по моделям (актуально при каскадном удалении связанных объектов, о чём подробнее в уроке 10).
Теперь заменим заглушку из словаря на настоящие запросы к базе. Попутно видим, как QuerySet органично встраивается на место списка — шаблоны менять не нужно:
# films/views.py
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from django.urls import reverse
from django.shortcuts import redirect
from .models import Film
def index(request):
context = {
'title': 'Лучшие фильмы всех времён',
}
return render(request, 'films/index.html', context)
def film_list(request):
films = Film.objects.all()
context = {
'films': films,
}
return render(request, 'films/film_list.html', context)
def film_detail(request, film_id):
film = get_object_or_404(Film, id=film_id)
return render(request, 'films/film_detail.html', {'film': film})
def about(request):
context = {
'title': 'О нашем сайте',
'film_count': Film.objects.count(),
}
return render(request, 'films/about.html', context)get_object_or_404() — это именно та функция, которую мы анонсировали в уроке 3. Она делает get() и автоматически возвращает 404, если объект не найден. Больше не нужен словарь FILMS и ручная проверка if film_id not in FILMS.
Стандартный менеджер objects предоставляет все базовые методы. Но если одни и те же запросы повторяются в нескольких местах — логично вынести их в модель.
Пользовательский менеджер — это класс, унаследованный от models.Manager, в котором мы добавляем собственные методы:
# films/models.py
from django.db import models
class FilmManager(models.Manager):
def high_rated(self):
"""Фильмы с рейтингом 8.0 и выше."""
return self.filter(rating__gte=8.0)
def by_year(self, year):
"""Фильмы конкретного года."""
return self.filter(year=year)
def recent(self, count=5):
"""Последние добавленные фильмы."""
return self.order_by('-created_at')[:count]Подключаем менеджер к модели:
class Film(models.Model):
title = models.CharField(max_length=200, verbose_name='Название')
year = models.PositiveIntegerField(verbose_name='Год выпуска')
description = models.TextField(blank=True, verbose_name='Описание')
rating = models.DecimalField(
max_digits=3,
decimal_places=1,
default=0.0,
verbose_name='Рейтинг'
)
slug = models.SlugField(max_length=200, unique=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Дата добавления')
objects = FilmManager() # заменяем стандартный менеджер
class Meta:
ordering = ['-year']
verbose_name = 'Фильм'
verbose_name_plural = 'Фильмы'
def __str__(self):
return f'{self.title} ({self.year})'Теперь в представлениях и шаблонных тегах можно писать выразительно:
# Вместо Film.objects.filter(rating__gte=8.0)
films = Film.objects.high_rated()
# Вместо Film.objects.filter(year=1972)
films = Film.objects.by_year(1972)
# Последние 3 добавленных фильма
latest = Film.objects.recent(3)Помнишь заглушку в inclusion_tag из урока 7? Теперь заменяем её на реальный запрос:
# films/templatetags/film_tags.py
from django import template
from films.models import Film
import datetime
register = template.Library()
@register.simple_tag
def current_year():
return datetime.date.today().year
@register.inclusion_tag('films/includes/latest_films.html')
def latest_films(count=3):
films = Film.objects.recent(count) # используем менеджер
return {'films': films}Шаблон latest_films.html не меняется — он уже ожидает список фильмов с полями id и title.
get() выбрасывает исключение при любом отклонении от «ровно один результат». Никогда не используй get() по неуникальному полю:
# X - Если несколько фильмов с таким годом — MultipleObjectsReturned
film = Film.objects.get(year=1972)
# Правильно: По уникальному полю — безопасно
film = Film.objects.get(slug='krestnyj-otec')
# Правильно: Или через get_object_or_404 — автоматический 404 вместо исключения
film = get_object_or_404(Film, slug='krestnyj-otec')Метод update() выполняет SQL напрямую, минуя Python-объект. Это значит: не срабатывают сигналы Django, не обновляется auto_now, не вызывается кастомный save():
# Поле updated_at с auto_now=True НЕ обновится
Film.objects.filter(id=1).update(rating=9.5)
# Для обновления с вызовом save() — только через объект
film = Film.objects.get(id=1)
film.rating = 9.5
film.save()auto_now — это специальный параметр полей DateField и DateTimeField, который автоматически записывает текущую дату/время при каждом сохранении объекта.
update() выполняет один SQL-запрос UPDATE напрямую, минуя Python-объекты. Это эффективно для массового обновления, но не вызывает кастомный save(), сигналы Django и не обновляет поля с auto_now=True. Загрузка объекта и save() работает через Python — медленнее при массовом обновлении, но поддерживает всю кастомную логику модели.
QuerySet ленивый — SQL выполняется при первом обращении к данным. Но если внутри цикла обращаться к связанным объектам (появятся в уроке 10), каждое обращение порождает новый запрос. Это классическая проблема N+1, которую мы разберём в уроке 11 с Django Debug Toolbar.
- В чём разница между
get()иfilter()? Когда использовать каждый? - Что такое Field Lookup и как он связан с SQL?
- Чем отличается
Film.objects.filter(...).update(...)от загрузки объекта и вызоваsave()? - Зачем нужен пользовательский менеджер? Чем он лучше, чем писать
filter()в каждом представлении? - Почему QuerySet называют «ленивым»? Когда фактически выполняется SQL-запрос?
Тип: расширь проект
Часть 1. Добавь в FilmManager метод search(query), который принимает строку и возвращает фильмы, в названии или описании которых встречается эта строка (без учёта регистра).
Часть 2. Используй этот метод в представлении search_film из урока 3 — вместо заглушки с текстом теперь оно должно возвращать реальные результаты поиска. Передай найденные фильмы в шаблон films/search_results.html и выведи их список. Если фильмов не найдено — покажи сообщение 'По запросу "..." ничего не найдено.'
Пример решение
films/models.py — добавляем метод в менеджер:
class FilmManager(models.Manager):
def high_rated(self):
return self.filter(rating__gte=8.0)
def by_year(self, year):
return self.filter(year=year)
def recent(self, count=5):
return self.order_by('-created_at')[:count]
def search(self, query):
"""Поиск по названию и описанию без учёта регистра."""
return self.filter(
models.Q(title__icontains=query) |
models.Q(description__icontains=query)
)Обрати внимание: в методе
search()используетсяmodels.Q— специальный класс для составных условий с операторомOR(|). Подробно разберём его в уроке 11.
films/views.py — обновляем представление:
def search_film(request):
query = request.GET.get('q', '').strip()
if not query:
return HttpResponse('Введите название фильма для поиска.', status=400)
films = Film.objects.search(query)
context = {
'films': films,
'query': query,
}
return render(request, 'films/search_results.html', context)films/templates/films/search_results.html:
{% extends 'base.html' %}
{% block title %}Поиск: {{ query }}{% endblock %}
{% block content %}
<h1>Результаты поиска: «{{ query }}»</h1>
{% for film in films %}
{% include 'films/includes/film_card.html' %}
{% empty %}
<p>По запросу «{{ query }}» ничего не найдено.</p>
{% endfor %}
<a href="{% url 'films:film_list' %}">← К каталогу</a>
{% endblock %}Строка запроса:
http://127.0.0.1:8000/films/search/?q=Список Шиндлера