В прошлом уроке мы познакомились с тем, как в Django создаётся связь «многие к одному» (Many-to-One) с помощью ForeignKey.
Теперь пришло время научиться работать с этими связями через ORM, получать связанные объекты, фильтровать их и строить удобные выборки данных.
Мы будем работать в контексте проекта Cinemahub, где у нас уже есть две модели:
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=100, db_index=True)
slug = models.SlugField(max_length=255, unique=True, db_index=True)
def __str__(self):
return self.name
class Movie(models.Model):
title = models.CharField(max_length=255)
description = models.TextField()
release_year = models.PositiveIntegerField()
category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name='movies')
def __str__(self):
return self.titleКаждый фильм (Movie) относится к определённой категории (Category). Например:
- фильм «Inception» — к категории «Science Fiction»,
- фильм «The Godfather» — к категории «Crime».
Для начала откроем интерактивную консоль Django:
python manage.py shellТеперь можно работать напрямую с моделями.
m = Movie.objects.get(pk=1)Теперь объект m — это один конкретный фильм.
Можно проверить его поля:
m.title
m.release_year
m.category_idcategory_id — это значение внешнего ключа, то есть ID категории, к которой относится фильм.
Чтобы получить сам объект категории, можно обратиться к атрибуту category:
m.category.nameЭто вызовет дополнительный SQL-запрос и вернёт имя категории.
Если в проекте включено логирование SQL-запросов, вы увидите, что ORM выполняет запрос SELECT ... FROM category WHERE id = ....
Django автоматически создаёт связь и в обратную сторону.
Теперь мы можем узнать, какие фильмы принадлежат конкретной категории.
Если бы в модели Movie не было параметра related_name, то обращение выглядело бы так:
c = Category.objects.get(pk=1)
c.movie_set.all()Это стандартное имя, которое Django создаёт автоматически:
<имя_модели>_set (в нашем случае — movie_set).
Но мы добавили в модель Movie параметр related_name='movies', поэтому теперь можем писать более удобно:
c.movies.all()Здесь c — это категория, а c.movies — связанный набор фильмов.
Можно фильтровать связанные фильмы, например:
c.movies.filter(release_year__gte=2020)Это вернёт все фильмы данной категории, выпущенные после 2020 года.
Часто нужно отобрать фильмы, принадлежащие к определённой категории.
Movie.objects.filter(category_id=1)c = Category.objects.get(name='Science Fiction')
Movie.objects.filter(category=c)Movie.objects.filter(category__in=[1, 2])или
cats = Category.objects.filter(name__in=['Science Fiction', 'Drama'])
Movie.objects.filter(category__in=cats)Django позволяет фильтровать не только по текущей модели, но и по полям связанных.
-
Фильмы по
slugкатегории:Movie.objects.filter(category__slug='drama')
-
Фильмы по имени категории:
Movie.objects.filter(category__name='Science Fiction')
-
По части названия категории:
Movie.objects.filter(category__name__icontains='fiction')
-
И наоборот — получить категории, в которых есть фильмы, соответствующие определённому условию:
Category.objects.filter(movies__title__icontains='star').distinct()
Метод .distinct() нужен, чтобы исключить дубликаты, ведь в одной категории может быть несколько фильмов с похожими названиями.
Теперь давайте добавим отображение фильмов по категориям в приложении.
Мы сделаем это пошагово.
В файле cinemahub/urls.py добавим маршрут для категории:
from django.urls import path
from . import views
urlpatterns = [
path('category/<slug:cat_slug>/', views.show_category, name='category'),
]Теперь URL вроде /category/drama/ будет показывать фильмы из категории «Драма».
Создадим функцию show_category:
from django.shortcuts import render, get_object_or_404
from .models import Movie, Category
def show_category(request, cat_slug):
category = get_object_or_404(Category, slug=cat_slug)
movies = Movie.objects.filter(category=category)
return render(request, 'movies/category_list.html', {
'category': category,
'movies': movies,
})Здесь мы:
- Получаем объект категории по
slug. - Извлекаем все фильмы, связанные с этой категорией.
- Передаём их в шаблон.
<h1>Фильмы категории: {{ category.name }}</h1>
<ul>
{% for movie in movies %}
<li>
<strong>{{ movie.title }}</strong> ({{ movie.release_year }})
</li>
{% empty %}
<li>В этой категории пока нет фильмов.</li>
{% endfor %}
</ul>Проверяем в браузере:
Переходим по адресу http://127.0.0.1:8000/category/drama/ — видим список фильмов этой категории.
Чтобы генерировать ссылки автоматически, добавим метод:
from django.urls import reverse
class Category(models.Model):
name = models.CharField(max_length=100, db_index=True)
slug = models.SlugField(max_length=255, unique=True, db_index=True)
def get_absolute_url(self):
return reverse('category', kwargs={'cat_slug': self.slug})Теперь в шаблонах можно ссылаться на категорию так:
<a href="{{ category.get_absolute_url }}">{{ category.name }}</a>-
Создайте несколько категорий в админке:
- «Драма»
- «Научная фантастика»
- «Комедия»
-
Добавьте несколько фильмов и назначьте им категории.
-
Перейдите в браузере по адресу:
/category/drama/ /category/science-fiction/Убедитесь, что фильтры работают корректно.
-
Измените
related_nameвMovieи убедитесь, что можно обращаться к фильмам черезcategory.movies.all().
DoesNotExist— если указанный объект не найден. Решается заменой.get()наget_object_or_404().ProtectedError— при попытке удалить категорию, если используетсяon_delete=PROTECT.IntegrityError— если добавляете фильм без категории, а поле не допускаетnull=True.
-
Создайте 3 категории фильмов и 6 фильмов, по 2 на каждую категорию. Попробуйте получить все фильмы одной категории через
category.movies.all(). -
Напишите запрос, который найдёт все фильмы, вышедшие после 2015 года в категории «Драма».
-
Получите все категории, в которых есть хотя бы один фильм с названием, содержащим слово “Love”.
-
Измените
related_nameи проверьте, как меняется способ обращения к связанным данным. -
В шаблоне выведите список категорий и добавьте ссылки на каждую через
get_absolute_url().
- Что такое связь «многие к одному» (Many-to-One)?
- Чем
category_idотличается отcategory? - Для чего нужен параметр
related_nameвForeignKey? - Что делает метод
.distinct()? - Как получить все фильмы категории без использования SQL?
- Как можно фильтровать объекты по полям связанных моделей?
- Почему может возникнуть ошибка
ProtectedError? - Зачем нужен метод
get_absolute_url()? - Что делает
get_object_or_404()и почему он удобнее, чем.get()? - Что произойдёт, если удалить категорию при
on_delete=CASCADE?