2025, Nov 11 06:03
Общие друзья в Django при несимметричной ManyToMany: рабочий подход
Как корректно находить общих друзей в Django при несимметричной ManyToMany модели подписок: пересечение подписок, примеры кода, ошибки и рабочее решение.
Когда механика подписок и подписчиков уже работает, логичный следующий шаг — показать общих знакомых — по сути, друзей друзей — между текущим пользователем и другими профилями. Здесь есть тонкая ловушка: легко получить «тех, на кого подписан я» и по ошибке назвать это «общими», так ни разу и не пересёкшись с сетью другого пользователя.
Проблема в контексте
Модель данных — самоссылочная ManyToMany для подписок, явно несимметричная: «A подписан на B» не означает, что «B подписан на A».
class MemberProfile(models.Model):
account = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True)
picture = models.ImageField(upload_to="UploadedProfilePicture/", default="ProfileAvatar/avatar.png", blank=True)
subscribes = models.ManyToManyField(
"MemberProfile",
symmetrical=False,
related_name="subscribers",
blank=True,
)Представление пытается посчитать «общих», но в итоге возвращает только исходящие подписки текущего пользователя.
def show_following(request):
title_text = "Following"
# Другие профили (кроме текущего пользователя)
others = MemberProfile.objects.exclude(Q(account=request.user))
# Текущий профиль пользователя
me = MemberProfile.objects.get(account=request.user)
# Ошибка: это просто «на кого подписан я»
my_follow_ids = me.subscribes.values_list("pk", flat=True)
supposed_mutuals = MemberProfile.objects.filter(pk__in=my_follow_ids)Что именно идёт не так
Переменная, которая должна обозначать общих друзей, фактически заполняется только моими подписками. Сравнения с набором подписок другого пользователя нет, пересечения не вычисляются — значит, «общих» в точном смысле не получается. При несимметричной модели взаимность здесь означает профили, на которые подписаны и текущий пользователь, и конкретный другой профиль. Не проверяя подписки второго профиля, мы получаем не общих друзей, а копию собственных исходящих связей.
Рабочий подход
Поля ManyToMany дают всё необходимое. Один раз получаем набор моих подписок, а затем для каждого кандидата проверяем, какие из них он тоже читает. Это и даёт общий поднабор — список общих друзей для пары.
# файл views.py
def show_following(request):
title_text = "Following"
# Все, кроме меня
others = MemberProfile.objects.exclude(Q(account=request.user))
# Мой профиль и мои исходящие подписки
me = MemberProfile.objects.get(account=request.user)
my_follow_ids = me.subscribes.values_list("pk", flat=True)
# Кандидаты для пересечения (мои подписки)
overlap_pool = MemberProfile.objects.filter(pk__in=my_follow_ids)# шаблон (пример)
{% for person in others %}
<p>{{ person.account }}</p>
{% for common in overlap_pool %}
{% if common in person.subscribes.all %}
<p>{{ common.account.username }}</p>
{% endif %}
{% endfor %}
{% endfor %}Эта логика точно соответствует цели: для каждого человека показать те профили, которые есть сразу в двух множествах — в моих подписках и в его подписках.
Почему это важно
Переименование «на кого подписан я» в «общих друзей» ломает пользовательские ожидания и подрывает функции, зависящие от корректного социального графа. В несимметричной модели подписок пересечения нужно вычислять явно для каждой пары, чтобы получить настоящих «общих».
Выводы
В самоссылочной несимметричной ManyToMany «общие» появляются только как пересечение двух наборов подписок: текущего пользователя и другого профиля. Один раз получите свои подписки, пройдитесь по остальным профилям и для каждого выведите тех, кто присутствует в обоих множествах. Не включайте в выборку себя и опирайтесь на уже существующие связи подписки/подписчики, не вводя лишних допущений.
Статья основана на вопросе на StackOverflow от user30880337 и ответе user30880337.