Как получить связанные записи в Django через обратный внешний ключ
Здесь новичок в Django! Я пришел из среды .NET и не могу понять, как сделать следующую простую вещь:
Мои упрощенные модели выглядят следующим образом
class Circle(BaseClass):
name = models.CharField("Name", max_length=2048, blank=False, null=False)
active = models.BooleanField(default=False)
...
class CircleParticipant(BaseClass):
circle = models.ForeignKey(Circle, on_delete=models.CASCADE, null=True, blank=True)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
status = models.CharField("Status", max_length=256, blank=False, null=False)
...
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(verbose_name="Email", unique=True, max_length=255, validators=[email_validator])
first_name = models.CharField(verbose_name="First name", max_length=30, default="first")
last_name = models.CharField(verbose_name="Last name", max_length=30, default="last")
...
Моя цель - получить один circle
с participants
, которые включают также users
. С дополнительным требованием сделать все это в одной поездке по БД.
В терминах SQL я хочу выполнить следующее:
SELECT circle.name, circle.active, circle_participant.status, user.email. user.first_name. user.last_name
FROM circle
JOIN circle_participant on circle.id = circle_participant.id
JOIN user on user.id = circle_participant.id
WHERE circle.id = 43
Я пробовал следующее:
Circle.objects.filter(id=43) \
.prefetch_related(Prefetch('circleparticipant_set', queryset=CircleParticipant.objects.prefetch_related('user')))
Это должно работать, но когда я проверяю свойство query
на этом утверждении, оно возвращает
SELECT "circle"."id", "circle"."created", "circle"."updated", "circle"."name", "circle"."active", FROM "circle" WHERE "circle"."id" = 43
(дополнительные поля опущены для краткости.)
Я что-то упускаю или свойство query
неверно?
Более того, как я могу добиться получения всех этих данных с помощью одного обращения к БД.
Для справки вот как это сделать в .NET Entity Framework
dbContext.Circle
.Filter(x => x.id == 43)
.Include(x => x.CircleParticipants) // This will exist in the entity/model
.ThenInclude(x => x.User)
.prefetch_related
будет использовать второй запрос для уменьшения пропускной способности, иначе он будет повторять данные для одних и тех же Circle
и CircleParticipant
несколько раз. Однако ваша CircleParticipant
действует как таблица перекрестка, поэтому вы можете использовать:
Circle.objects.filter(id=43).prefetch_related(
Prefetch('circleparticipant_set', queryset=CircleParticipant.objects.select_related('user')
)
)
Я что-то упускаю или свойство
query
неверно?
В Django есть два способа решения проблемы SELECT N+1. Первый - prefetch_related(), который создает два запроса и соединяет результат в памяти. Вторая - select_related(), которая создает объединение, но имеет несколько больше ограничений. (Вы также не установили related_name
ни для одного из ваших внешних ключей. IIRC это требуется перед использованием select_related().)
Более важно, как я могу добиться получения всех этих данных с помощью одной поездки в БД.
Я бы посоветовал вам не слишком беспокоиться о том, чтобы сделать все это в одном запросе. Одним из недостатков выполнения этого в одном запросе, как вы предлагаете, является то, что многие данные, которые возвращаются, будут избыточными. Например, столбец circle.name будет одинаковым для каждой строки таблицы, которая будет возвращена.
Вам абсолютно небезразлично, сколько запросов вы делаете - но только в той степени, чтобы избежать проблемы SELECT N+1. Если вы делаете по одному запросу для каждого класса модели, это очень хорошо.
Если вас сильно волнует производительность SQL, я также рекомендую инструмент Django Debug Toolbar, который может показать вам количество запросов, точный SQL и время, затраченное на каждый из них.
в терминах SQL я хочу добиться следующего:
Есть несколько способов добиться этого.
Использовать многие ко многим
В Django есть поле, которое можно использовать для создания отношений "многие-ко-многим". Оно называется ManyToManyField. Оно неявно создает таблицу "многие ко многим" для представления отношения, а также некоторые вспомогательные методы, позволяющие вам легко запрашивать все круги, в которых состоит пользователь, или всех пользователей, которые есть в круге.
Вы также прикрепляете некоторые метаданные к каждому отношению пользователь/кружок. Это означает, что вам нужно определить явную таблицу с помощью ManyToManyField.through
.
Примеры есть в документации здесь.
Использовать запрос связанной модели
Если бы я специально хотел получить объединение, а не подзапрос, я бы запросил пользователей следующим образом:
Users.objects.filter(circleparticipant_set__circle_id=43)
Использовать подзапрос
Здесь также создается только один запрос, но вместо него используется подзапрос.
Users.objects.filter(circleparticipant_set=CircleParticipant.objects.filter(circle_id=43))