Связи¶
ForeignKey (многие-к-одному)¶
class Author(Model):
name = fields.CharField(max_length=100)
class Book(Model):
title = fields.CharField(max_length=200)
author = fields.ForeignKey(Author, related_name="books", on_delete="CASCADE")
Хранится как колонка author_id. Присваивать можно экземпляр или id:
book = Book.objects.create(title="X", author=some_author) # или author_id=1
book.author # лениво загружает Author (кэшируется)
book.author_id # сырое значение FK
Кросс-табличные lookup'ы порождают JOIN'ы:
Book.objects.filter(author__name="Ada")
Book.objects.filter(author__country__code="US") # многоуровневый
Book.objects.order_by("author__name")
Обратный аксессор (related_name, по умолчанию <model>_set):
author.books.all() # QuerySet книг этого автора
author.books.filter(title__icontains="py")
author.books.count()
Варианты on_delete: CASCADE, SET NULL (нужен null=True), RESTRICT,
PROTECT (на уровне БД отображается в RESTRICT), NO ACTION/DO_NOTHING.
OneToOneField¶
Уникальный FK — по одной записи с каждой стороны:
class Profile(Model):
user = fields.OneToOneField(User, related_name="profile")
bio = fields.TextField(default="")
user.profile # единственный связанный Profile или None
ManyToManyField¶
Автоматически создаваемая through-таблица с менеджером связи:
class Tag(Model):
name = fields.CharField(max_length=30)
class Post(Model):
title = fields.CharField(max_length=200)
tags = fields.ManyToManyField(Tag)
post.tags.add(tag1, tag2) # идемпотентно (дубли игнорируются)
post.tags.remove(tag1)
post.tags.set([tag2, tag3]) # заменить множество
post.tags.clear()
post.tags.all() # QuerySet связанных Tag'ов
post.tags.count()
create_all(...) строит through-таблицы вторым проходом, поэтому порядок
объявления связей не важен.
add() остаётся идемпотентным и при конкурентном доступе: если два вызова
гонятся за одной и той же парой, вставка проигравшего упирается в составной
первичный ключ through-таблицы и трактуется как «уже связаны», а не бросается
наружу.
Избегаем N+1: select_related и prefetch_related¶
select_related — для прямых FK / OneToOne — достаёт связанные строки в
том же запросе через JOIN:
for book in Book.objects.select_related("author"):
book.author.name # без дополнительного запроса
Book.objects.select_related("author__country") # многоуровневый
prefetch_related батчево подгружает связь для целой страницы
результатов вместо запроса на каждую строку — но прежде чем тянуться за ним,
проверьте, какие виды связей он реально поддерживает: два поддерживаемых
вида стоят разного числа запросов:
for post in Post.objects.prefetch_related("tags"):
post.tags.all() # из кэша, без запроса на каждую строку
- Имя поля
ManyToManyField(как"tags"выше) — два дополнительных запроса всего: один к through-таблице (группирует id целей по id источника), затем один к целевой модели (pk__in=[...]), чтобы забрать сами строки. Всё равно огромный выигрыш по сравнению с N+1, просто не буквально «один» запрос. - Собственное имя поля прямого
ForeignKey(не егоrelated_name) — один дополнительный запрос (pk__in=[...]к целевой модели). Эквивалентноselect_relatedдля того же поля, только остаётся отдельным SELECT'ом вместо JOIN'а — обычно для прямого FK тянутся именно кselect_related, ноprefetch_relatedтоже работает, если хочется избежать задвоенных колонок базовой таблицы из JOIN'а. - Имя обратной связи (
related_name, или<model>_set, если он не задан) — один дополнительный запрос (fk__in=[...]к исходной модели, сгруппированный по id fk на стороне Python):
for author in Author.objects.prefetch_related("books"):
author.books.all() # из кэша — без запроса на каждого автора
list(author.books) # обычная итерация тоже из кэша
author.books.filter(title__startswith="A") # а вот здесь снова запрос
Из кэша читает только голый .all() (или прямая итерация по связи, без
дополнительных условий) — всё, что дописано после (.filter(),
.exclude(), .order_by(), ...), это уже другой запрос и всегда идёт в
базу, точно как у обычного QuerySet. .count() на обратной связи тоже
всегда запрашивает заново (кэш prefetch он не смотрит — как и .count()
у любого другого QuerySet) — если узкое место именно в подсчёте на
каждой строке, annotate(n=Count("books")) избегает этого (см.
Запросы).
Ссылки на модели по имени¶
Опережающие ссылки (модель, объявленная позже, или ссылка на себя) могут быть строкой — лишь бы класс существовал к моменту вызова; но прямая ссылка на класс предпочтительнее: