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.