Django-Admin.ru

ForeignKey в Django: связи между моделями

ForeignKey в моделях Django: параметры on_delete, related_name, null, to_field, обратная связь, запросы через __, self, миграции, формы и админка.

Редакция Django-Admin.ru
· 20 мин чтения

Содержание · 14 разделов
  1. Как объявить ForeignKey
  2. Параметры ForeignKey
  3. on_delete: что происходит при удалении родителя
  4. DB_CASCADE, DB_SET_NULL и DB_SET_DEFAULT в Django 6.1
  5. related_name и related_query_name
  6. null и blank: необязательная связь
  7. limit_choices_to
  8. to_field
  9. db_constraint и db_index
  10. Обратная связь: _set и related_name
  11. Запросы через связи: двойное подчёркивание
  12. select_related и prefetch_related
  13. ForeignKey, OneToOneField или ManyToManyField
  14. Ссылка на модель строкой и AUTH_USER_MODEL
  15. Связь модели с самой собой (‘self’)
  16. Миграции: ForeignKey в таблице с данными
  17. ForeignKey в формах
  18. ForeignKey в админке: raw_id_fields и autocomplete_fields
  19. Частые ошибки
  20. fields.E304: Reverse accessor clashes
  21. fields.E320: SET_NULL без null=True
  22. ValueError: unsaved related object
  23. IntegrityError: FOREIGN KEY constraint failed
  24. Частые вопросы
  25. Как получить id связанного объекта без запроса к базе?
  26. Срабатывают ли сигналы и delete() при каскадном удалении?
  27. Нужна ли миграция при смене on_delete?
  28. Можно ли сделать ForeignKey на модель без числового первичного ключа?
  29. Чем ForeignKey отличается от простого IntegerField с id?
  30. Итог

ForeignKey в моделях Django описывает связь «многие к одному»: у книги один автор, у автора много книг. Поле объявляют в «зависимой» модели и передают ему модель-родителя и обязательный параметр on_delete:

author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")

В базе появляется колонка author_id с внешним ключом и индексом. В коде book.author возвращает объект автора, а author.books.all() — все его книги. Ниже — все параметры django models foreign key с таблицей вариантов on_delete, обратная связь, запросы через __, выбор между ForeignKey, OneToOneField и ManyToManyField, связь модели с самой собой, миграции для таблиц с данными, формы и админка. Примеры проверены на Django 6.1 и SQLite и работают в Django 5.2 LTS, кроме отмеченных вариантов DB_*. Общие сведения о полях и Meta — в статье про модели Django.

Как объявить ForeignKey

Модели для всех примеров статьи — библиотека с авторами, издательствами и рубриками:

# library/models.py
from django.conf import settings
from django.db import models


class Author(models.Model):
    name = models.CharField(max_length=200)

    def __str__(self):
        return self.name


class Publisher(models.Model):
    name = models.CharField(max_length=200)

    def __str__(self):
        return self.name


class Book(models.Model):
    title = models.CharField(max_length=300)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
    publisher = models.ForeignKey(
        Publisher,
        on_delete=models.PROTECT,
        null=True,
        blank=True,
        related_name="books",
    )
    added_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="added_books",
    )

    def __str__(self):
        return self.title

Первый аргумент — модель, на которую ссылается поле: класс или строка с его именем. Второй — on_delete, его можно передать позиционно или по имени. Без on_delete Django не создаст поле:

TypeError: ForeignKey.__init__() missing 1 required positional argument: 'on_delete'

После makemigrations команда sqlmigrate показывает, что получится в SQLite:

CREATE TABLE "library_book" (
  "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
  "title" varchar(300) NOT NULL,
  "added_by_id" integer NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED,
  "author_id" bigint NOT NULL REFERENCES "library_author" ("id") DEFERRABLE INITIALLY DEFERRED,
  "publisher_id" bigint NULL REFERENCES "library_publisher" ("id") DEFERRABLE INITIALLY DEFERRED
);
CREATE INDEX "library_book_author_id_d9a3b67e" ON "library_book" ("author_id");

Django добавляет к имени поля суффикс _id, создаёт ограничение внешнего ключа (REFERENCES) и индекс. В Python доступны оба имени:

war = Book.objects.create(title="Война и мир", author=tolstoy)
anna = Book.objects.create(title="Анна Каренина", author_id=tolstoy.pk)

print(war.author, war.author_id)
# → Лев Толстой 1

war.author_id читает значение из уже загруженной строки, а первое обращение к war.author выполняет отдельный запрос за объектом автора. Если нужен только идентификатор, используйте author_id.

Параметры ForeignKey

ПараметрПо умолчаниюЧто делает
on_deleteобязательныйчто делать со ссылающимися записями при удалении родителя
related_name<модель>_setимя обратной связи у родителя: author.books
related_query_nameкак related_nameимя связи в фильтрах родителя: Author.objects.filter(book__...)
nullFalseразрешает NULL в колонке — связь становится необязательной в базе
blankFalseразрешает пустое значение в формах и админке
limit_choices_toнетограничивает варианты выбора в ModelForm и админке
to_fieldпервичный ключполе родителя, на которое ссылается ключ; оно должно быть unique=True
db_constraintTrueсоздавать ли ограничение внешнего ключа в базе
db_indexTrueсоздавать ли индекс по колонке
db_column<поле>_idимя колонки в таблице
defaultнетзначение по умолчанию — первичный ключ родителя, а не объект

on_delete: что происходит при удалении родителя

on_delete срабатывает в одну сторону: при удалении автора Django решает, что делать с его книгами. Удаление книги на автора не влияет.

ВариантЧто происходит со ссылающимися записямиТребованияКогда выбирать
CASCADEудаляются вместе с родителем—запись бессмысленна без родителя: комментарии, строки заказа
PROTECTудаление родителя запрещено, исключение ProtectedError—справочники: издательства, валюты, статусы
RESTRICTудаление запрещено (RestrictedError), если только ссылающаяся запись не удаляется в той же операции каскадом по другой связи—сложные графы связей, где PROTECT слишком строг
SET_NULLссылка становится NULLnull=Trueнеобязательная связь: книги удалённого пользователя
SET_DEFAULTссылка получает значение defaultdefaultесть запись-заглушка с известным ключом
SET(...)ссылка получает переданное значение или результат вызова функции—заглушка, которую нужно найти или создать
DO_NOTHINGDjango ничего не делаетсвоё ON DELETE в базецелостность обеспечивает сама СУБД
DB_CASCADEудаляет база через ON DELETE CASCADEDjango 6.1большие объёмы, сигналы не нужны
DB_SET_NULLNULL проставляет базаDjango 6.1, null=Trueто же, что SET_NULL, без загрузки объектов в Python
DB_SET_DEFAULTбаза проставляет db_defaultDjango 6.1, db_default; нет в MySQL и MariaDBзаглушка на уровне базы

PROTECT в действии:

pub = Publisher.objects.create(name="Азбука")
Book.objects.create(title="Война и мир", author=tolstoy, publisher=pub)

pub.delete()
# → ProtectedError: ("Cannot delete some instances of model 'Publisher' because they
#   are referenced through protected foreign keys: 'Book.publisher'.", {<Book: Война и мир>})

CASCADE возвращает число удалённых объектов по моделям:

dostoevsky.delete()   # у автора три книги
# → (4, {'library.Book': 3, 'library.Author': 1})

При CASCADE Django сам находит и удаляет зависимые записи. Сигналы pre_delete и post_delete отправляются для каждой из них, но метод delete() связанных моделей не вызывается: логику удаления, которая должна срабатывать и при каскаде, переносите в обработчики сигналов.

Разница между PROTECT и RESTRICT видна на трёх моделях из приложения lab: песня ссылается на исполнителя (CASCADE) и на альбом (RESTRICT):

class Artist(models.Model):
    name = models.CharField(max_length=10)


class Album(models.Model):
    artist = models.ForeignKey(Artist, on_delete=models.CASCADE)


class Song(models.Model):
    artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
    album = models.ForeignKey(Album, on_delete=models.RESTRICT)
album.delete()
# → RestrictedError: Cannot delete some instances of model 'Album' because they
#   are referenced through restricted foreign keys: 'Song.album'.

artist.delete()
# → (3, {'lab.Song': 1, 'lab.Album': 1, 'lab.Artist': 1})

Альбом напрямую удалить нельзя, а исполнителя можно: песня всё равно удаляется каскадом через Song.artist. С PROTECT удаление исполнителя тоже завершилось бы ошибкой.

Для SET() передавайте функцию, а не готовый объект: иначе запрос к базе выполнится при импорте models.py.

from django.contrib.auth import get_user_model


def get_deleted_user():
    return get_user_model().objects.get_or_create(username="deleted")[0]


class Comment(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET(get_deleted_user))

DB_CASCADE, DB_SET_NULL и DB_SET_DEFAULT в Django 6.1

Варианты DB_* передают работу базе данных: Django прописывает ON DELETE в ограничение внешнего ключа и не загружает зависимые объекты в Python. Пример из того же приложения lab:

class Board(models.Model):
    name = models.CharField(max_length=100)


class Post(models.Model):
    board = models.ForeignKey(Board, on_delete=models.DB_CASCADE, related_name="posts")
"board_id" bigint NOT NULL REFERENCES "lab_board" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
board.delete()
# → (1, {'lab.Board': 1})
# SQL: DELETE FROM "lab_board" WHERE "lab_board"."id" IN (1)

Посты удалены базой, поэтому их нет в счётчике, а сигналы pre_delete и post_delete для них не отправляются. Ограничения:

  • В одной модели нельзя смешивать варианты DB_* и обычные (кроме DO_NOTHING) — ошибка models.E050.
  • Если Post удаляется базой, внешние ключи на Post тоже должны быть DB_* — иначе ошибка fields.E323.
  • Переход с CASCADE на DB_CASCADE меняет схему: миграция пересоздаёт ограничение. Смена обычных вариантов между собой (CASCADE → PROTECT) даёт миграцию без SQL.

related_name задаёт имя, по которому родитель обращается к зависимым записям. Без него Django использует <модель>_set: author.book_set.all(). Явное имя читается лучше и обязательно, когда в модели несколько ключей на одну и ту же модель:

class Match(models.Model):
    home = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="home_matches")
    away = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="away_matches")
    winner = models.ForeignKey(
        Team, on_delete=models.SET_NULL, null=True, blank=True, related_name="+"
    )

Значение "+" отключает обратную связь: у Team не будет атрибута для winner.

related_query_name меняет имя связи только в фильтрах. По умолчанию оно совпадает с related_name:

class Player(models.Model):
    team = models.ForeignKey(
        Team, on_delete=models.CASCADE, related_name="players", related_query_name="player"
    )


team.players.all()                            # атрибут — related_name
Team.objects.filter(player__name="Иванов")    # фильтр — related_query_name

Для всех ключей модели сразу имя задают через Meta.default_related_name. В абстрактных моделях используйте шаблон related_name="%(app_label)s_%(class)s_set", чтобы у наследников не возникло одинаковых имён.

null и blank: необязательная связь

null=True разрешает NULL в колонке, blank=True — пустое значение в формах и админке. Для необязательной связи нужны оба:

publisher = models.ForeignKey(
    Publisher, on_delete=models.PROTECT, null=True, blank=True, related_name="books"
)

Только null=True даст необязательное поле в базе, но обязательное в форме. Только blank=True пропустит пустую форму и упадёт на IntegrityError при сохранении.

limit_choices_to

Ограничивает список объектов, которые предлагают ModelForm и админка. Принимает словарь, объект Q или функцию, которая их возвращает:

added_by = models.ForeignKey(
    settings.AUTH_USER_MODEL,
    on_delete=models.SET_NULL,
    null=True,
    blank=True,
    limit_choices_to={"is_staff": True},
)

В форме останутся только сотрудники. На ORM-запросы и прямое присваивание book.added_by = user параметр не влияет. Функцию Django вызывает при создании каждой формы — так можно ограничить выбор, например, по текущей дате. Изменение limit_choices_to создаёт миграцию без SQL.

to_field

По умолчанию ключ ссылается на первичный ключ родителя. to_field указывает другое поле, и оно должно быть уникальным:

class Team(models.Model):
    code = models.CharField(max_length=3, unique=True)


class Player(models.Model):
    team = models.ForeignKey(Team, on_delete=models.CASCADE, to_field="code")


Player.objects.create(name="Иванов", team=zenit).team_id
# → 'ZEN'

Колонка team_id получает тип поля code — varchar(3). Пригодится при интеграции со старой базой, где связи построены на кодах или артикулах.

db_constraint и db_index

db_constraint=False создаёт колонку и индекс без ограничения REFERENCES. База перестаёт проверять, что родитель существует, и при обращении к несуществующему родителю Django выбросит DoesNotExist:

class Log(models.Model):
    team = models.ForeignKey(Team, on_delete=models.DO_NOTHING, db_constraint=False)


log = Log.objects.create(team_id=999)   # база не возражает
log.team
# → Team.DoesNotExist: Team matching query does not exist.

Применяйте это для заведомо «грязных» исторических данных или при шардинге базы. Индекс создаётся автоматически; db_index=False отключает его, если вы строите свой составной или частичный индекс через Meta.indexes.

У родителя появляется менеджер связанных объектов (RelatedManager). Он умеет всё, что умеет обычный менеджер, и ещё несколько методов для управления связью:

tolstoy.books.all()
# → <QuerySet [<Book: Война и мир>, <Book: Анна Каренина>]>
tolstoy.books.count()
# → 2

idiot = dostoevsky.books.create(title="Идиот")   # author проставится сам
idiot.author
# → Фёдор Достоевский
МетодЧто делает
create(**kwargs)создаёт объект, уже привязанный к родителю
add(*objs)привязывает существующие объекты (UPDATE); несохранённые — с bulk=False
remove(*objs)отвязывает объекты, проставляя NULL
clear()отвязывает все объекты
set(objs)приводит набор к переданному списку; при null=False только добавляет

add() переносит объект от прежнего родителя. Несохранённый объект add() сохранит только с bulk=False:

book = Book(title="Бесы", author=tolstoy)
dostoevsky.books.add(book, bulk=False)
book.author
# → Фёдор Достоевский

remove() и clear() есть только у связей с null=True: ключ становится NULL, сами записи остаются в базе. У tolstoy.books этих методов нет, у pub.books — есть:

pub.books.remove(war)
war.refresh_from_db()
war.publisher
# → None

Запросы через связи: двойное подчёркивание

В базе для примеров раздела: у Льва Толстого «Война и мир» и «Анна Каренина», у Фёдора Достоевского «Идиот» и «Бесы», у Джейн Остин книг нет. Поля родителя в фильтрах указывают через __ — Django сделает JOIN:

Book.objects.filter(author__name="Лев Толстой")
# → <QuerySet [<Book: Война и мир>, <Book: Анна Каренина>]>

Book.objects.order_by("author__name", "title").values_list("title", flat=True)
# → <QuerySet ['Анна Каренина', 'Война и мир', 'Бесы', 'Идиот']>

filter(author=tolstoy), filter(author_id=1) и filter(author__id=1) дают одинаковый SQL без JOIN: условие проверяется по колонке author_id.

В обратную сторону фильтр идёт через related_query_name — здесь это books:

Author.objects.filter(books__title__startswith="Война")
# → <QuerySet [<Author: Лев Толстой>]>

Author.objects.filter(books__isnull=True)          # авторы без книг
# → <QuerySet [<Author: Джейн Остин>]>

Фильтр по обратной связи возвращает родителя столько раз, сколько у него подходящих записей. Уберите дубли через distinct():

Author.objects.filter(books__title__contains="а")
# → <QuerySet [<Author: Лев Толстой>, <Author: Лев Толстой>]>
Author.objects.filter(books__title__contains="а").distinct()
# → <QuerySet [<Author: Лев Толстой>]>

Количество связанных записей считают через annotate():

from django.db.models import Count

Author.objects.annotate(n=Count("books")).values_list("name", "n")
# → <QuerySet [('Лев Толстой', 2), ('Фёдор Достоевский', 2), ('Джейн Остин', 0)]>

Lookups, Q, F и поведение цепочек filter() на связях «один-ко-многим» подробно разобраны в статье о фильтрации в Django ORM.

Каждое обращение к book.author в цикле — отдельный запрос. На четырёх книгах замер дал 5 запросов вместо одного:

for book in Book.objects.all():
    book.author.name
# → 5 запросов: 1 за книги + 4 за авторов

for book in Book.objects.select_related("author"):
    book.author.name
# → 1 запрос с JOIN

for author in Author.objects.prefetch_related("books"):
    list(author.books.all())
# → 2 запроса: авторы и все их книги через IN

select_related подходит для прямого ForeignKey и OneToOneField, prefetch_related — для обратных связей и ManyToManyField. Как находить такие лишние запросы и использовать Prefetch(), разобрано в статье о проблеме N+1 в Django ORM.

ForeignKey, OneToOneField или ManyToManyField

КритерийForeignKeyOneToOneFieldManyToManyField
Тип связимногие к одномуодин к одномумногие ко многим
Примеркнига → авторпрофиль → пользователькнига ↔ жанры
Что в базеколонка <поле>_idколонка <поле>_id с UNIQUEотдельная промежуточная таблица
Обратная сторонаменеджер: author.books.all()один объект: user.profileменеджер с обеих сторон
on_deleteобязателенобязателенне нужен
Дополнительные данные о связив самой моделив самой моделичерез модель through
Подгрузка без N+1select_relatedselect_relatedprefetch_related

ForeignKey(unique=True) работает как OneToOneField, и Django предупреждает об этом (fields.W342). Разница в обратной стороне: OneToOneField возвращает объект, а не менеджер, поэтому выбирайте его. Если у записи может быть несколько родителей одного типа (у книги несколько жанров), нужен ManyToManyField. Если связь «многие ко многим» должна хранить дату или роль, сделайте промежуточную модель с двумя ForeignKey и укажите её в through.

Ссылка на модель строкой и AUTH_USER_MODEL

Вместо класса можно передать строку — Django найдёт модель после загрузки всех приложений:

class Car(models.Model):
    # модель из этого же приложения, объявленная ниже
    manufacturer = models.ForeignKey("Manufacturer", on_delete=models.CASCADE)
    # модель из другого приложения: "метка_приложения.Модель"
    dealer = models.ForeignKey("sales.Dealer", on_delete=models.PROTECT)

Строка решает две задачи: ссылку на модель, которая объявлена ниже в файле, и циклический импорт, когда два приложения ссылаются друг на друга.

Для связи с пользователем всегда используйте settings.AUTH_USER_MODEL, а не from django.contrib.auth.models import User:

from django.conf import settings

added_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)

Так связь продолжит работать после перехода на свою модель пользователя, а миграции будут ссылаться на настройку, а не на конкретный класс. В коде вне models.py модель пользователя получают через get_user_model(). Как заменить стандартного пользователя — в статье про кастомную модель пользователя.

Связь модели с самой собой (‘self’)

Рубрики с подрубриками, сотрудники с руководителями, ветки комментариев описывают ключом на свою же модель. Первым аргументом передают строку "self":

class Category(models.Model):
    name = models.CharField(max_length=100)
    parent = models.ForeignKey(
        "self",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name="children",
    )

У корневых рубрик parent пустой, поэтому null=True обязателен.

prog = Category.objects.create(name="Программирование")
py = Category.objects.create(name="Python", parent=prog)
dj = Category.objects.create(name="Django", parent=py)

prog.children.all()
# → <QuerySet [<Category: Python>]>
dj.parent.parent
# → Программирование
Category.objects.filter(parent__isnull=True)        # корневые рубрики
# → <QuerySet [<Category: Программирование>]>
Category.objects.filter(parent__parent=prog)        # «внуки»
# → <QuerySet [<Category: Django>]>

Каждый уровень вложенности — новый JOIN или запрос. Для дерева из двух-трёх уровней этого хватает. Для глубоких деревьев с выборкой всех потомков используют рекурсивные CTE в SQL или пакеты вроде django-treebeard.

Миграции: ForeignKey в таблице с данными

Если добавить обязательный ForeignKey в таблицу, где уже есть строки, makemigrations спросит, что записать в существующие записи:

It is impossible to add a non-nullable field 'series' to book without specifying a default.
This is because the database needs something to populate existing rows.
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit and manually define a default value in models.py.

Вариант 1 опасен: введённый идентификатор (например, 1) должен существовать в таблице родителя, иначе миграция упадёт на IntegrityError. Надёжнее добавить связь в три шага.

  1. Добавьте поле с null=True и создайте миграцию:

    series = models.ForeignKey(Series, on_delete=models.PROTECT, null=True)
    python manage.py makemigrations library -n book_series
  2. Создайте пустую миграцию и заполните ключ в RunPython:

    python manage.py makemigrations library --empty -n fill_book_series
    from django.db import migrations
    
    
    def fill_series(apps, schema_editor):
        Series = apps.get_model("library", "Series")
        Book = apps.get_model("library", "Book")
        default, _ = Series.objects.get_or_create(name="Без серии")
        Book.objects.filter(series__isnull=True).update(series=default)
    
    
    class Migration(migrations.Migration):
        dependencies = [("library", "0002_book_series")]
    
        operations = [
            migrations.RunPython(fill_series, migrations.RunPython.noop),
        ]
  3. Уберите null=True и снова выполните makemigrations. На вопрос о существующих NULL выберите 2) Ignore for now — данные уже заполнены предыдущей миграцией. Затем python manage.py migrate.

На Windows вместо python можно использовать py, на macOS и Linux — python3, если python не указывает на Python 3. Подробнее о миграциях данных и откате — в статье про миграции Django.

ForeignKey в формах

В ModelForm поле ForeignKey превращается в ModelChoiceField — выпадающий список объектов родителя. Поле обязательно, если у модели нет blank=True. Если прислать несуществующий идентификатор, форма вернёт ошибку Select a valid choice. That choice is not one of the available choices.

Список вариантов сужают через limit_choices_to в модели или через queryset в конструкторе формы, когда условие зависит от запроса:

from django import forms

from .models import Author, Book


class BookForm(forms.ModelForm):
    class Meta:
        model = Book
        fields = ["title", "author", "added_by"]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["author"].queryset = Author.objects.order_by("name")
        self.fields["author"].label_from_instance = lambda obj: obj.name.upper()

label_from_instance меняет подпись варианта, не трогая __str__ модели. Валидация, виджеты и обработка формы во view — в статье про формы Django.

ForeignKey в админке: raw_id_fields и autocomplete_fields

По умолчанию админка рисует для ForeignKey выпадающий список со всеми объектами родителя. На тысячах записей страница начинает грузиться медленно. Есть два решения:

  • raw_id_fields — текстовое поле для идентификатора и кнопка поиска во всплывающем окне;
  • autocomplete_fields — список с поиском, который подгружает варианты по мере ввода. Работает, только если в админке родителя заданы search_fields.
# library/admin.py
from django.contrib import admin

from .models import Author, Book


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    search_fields = ["name"]          # нужно для autocomplete_fields


@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "publisher"]
    list_select_related = ["author", "publisher"]
    list_filter = ["author"]
    autocomplete_fields = ["author"]
    raw_id_fields = ["publisher"]

Без search_fields у AuthorAdmin проверка Django остановит запуск:

<class 'library.admin.BookAdmin'>: (admin.E040) AuthorAdmin must define "search_fields",
because it's referenced by BookAdmin.autocomplete_fields.

list_select_related убирает N+1 в списке объектов, где выводится автор. limit_choices_to в виде объекта Q не действует на поле из raw_id_fields. Чтобы редактировать книги прямо на странице автора, используйте инлайны в Django Admin.

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

fields.E304: Reverse accessor clashes

Два ForeignKey на одну модель без related_name претендуют на одно имя match_set:

lab.Match.away: (fields.E304) Reverse accessor 'Team.match_set' for 'lab.Match.away'
clashes with reverse accessor for 'lab.Match.home'.
	HINT: Add or change a related_name argument to the definition for 'lab.Match.away' or 'lab.Match.home'.

Задайте каждому полю свой related_name или отключите обратную связь через "+".

fields.E320: SET_NULL без null=True

lab.Match.winner: (fields.E320) Field specifies on_delete=SET_NULL, but cannot be null.
	HINT: Set null=True argument on the field, or change the on_delete rule.

Добавьте null=True (и blank=True для форм) или выберите другой on_delete.

ValueError: save() prohibited to prevent data loss due to unsaved related object 'author'.

Родителя создали через Author(name=...), но не сохранили, поэтому у него нет первичного ключа. Сначала вызовите author.save() или создайте его через Author.objects.create().

IntegrityError: FOREIGN KEY constraint failed

База не нашла родителя с переданным идентификатором: Book.objects.create(author_id=999) при отсутствии автора 999. Проверьте значение, которое пришло из формы или API, и порядок создания записей. В SQLite это сообщение FOREIGN KEY constraint failed, в PostgreSQL — violates foreign key constraint.

Частые вопросы

Как получить id связанного объекта без запроса к базе?

Читайте атрибут <поле>_id: book.author_id. Он хранится в строке книги, тогда как book.author.id сначала загрузит автора. То же для присваивания: book.author_id = 5 не требует объекта автора.

Срабатывают ли сигналы и delete() при каскадном удалении?

При CASCADE Django отправляет pre_delete и post_delete для каждого удаляемого объекта, но не вызывает переопределённый delete() связанных моделей. QuerySet.delete() тоже не вызывает delete() у объектов. При DB_CASCADE удаляет база, и сигналы для зависимых записей не отправляются.

Нужна ли миграция при смене on_delete?

makemigrations создаст миграцию в любом случае, потому что параметры поля записываются в историю миграций. Для обычных вариантов (CASCADE, PROTECT, SET_NULL и других) sqlmigrate покажет (no-op) — схема не меняется. Переход на DB_CASCADE или DB_SET_NULL пересоздаёт ограничение внешнего ключа, а в SQLite — всю таблицу.

Можно ли сделать ForeignKey на модель без числового первичного ключа?

Да. Ключ получает тип первичного ключа родителя: для UUIDField(primary_key=True) колонка будет хранить UUID. Чтобы ссылаться на другое уникальное поле, например код или артикул, используйте to_field.

Чем ForeignKey отличается от простого IntegerField с id?

IntegerField хранит число, но база не проверяет, что такой родитель существует, нет on_delete, обратной связи, фильтров через __, select_related и выпадающего списка в формах. Обычное число оправдано только для ссылок на данные в другой базе или внешнем сервисе.

Итог

ForeignKey — основной способ связать модели Django по схеме «многие к одному». Минимальный набор решений для каждого ключа:

  • выберите on_delete осознанно: CASCADE для зависимых данных, PROTECT для справочников, SET_NULL для необязательных связей, DB_* в Django 6.1, когда важна скорость удаления больших объёмов;
  • задайте понятный related_name — особенно если ключей на одну модель несколько;
  • для необязательной связи ставьте null=True и blank=True;
  • ссылайтесь на пользователя через settings.AUTH_USER_MODEL, а на модели других приложений — строкой "app.Model";
  • добавляйте обязательный ключ в таблицу с данными в три миграции;
  • подгружайте связи через select_related и prefetch_related, в админке включайте autocomplete_fields или raw_id_fields.

Остальные методы QuerySet — get(), update(), агрегаты и транзакции — собраны в шпаргалке по Django ORM. Полный список параметров поля — в справочнике полей Django.

  • #Django
  • #Django ORM
  • #ForeignKey
  • #Модели
  • #Связи моделей
↑ ↓ выборEnter открытьEsc закрыть