← Назад к вопросам
Как сделаешь механизм подписки одних пользователей на других?
1.8 Middle🔥 151 комментариев
#Архитектура и паттерны#Базы данных (SQL)
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как сделаешь механизм подписки одних пользователей на других
Механизм подписки (follow/subscribe) — это база социальных сетей. Рассмотрю несколько подходов реализации, от простой до оптимизированной версии.
Вариант 1: Простая реализация с моделями
Базовая модель для хранения подписок:
from django.db import models
from django.contrib.auth.models import User
class Follow(models.Model):
"""Модель подписки: follower подписан на following"""
follower = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='following' # follower.following.all()
)
following = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='followers' # user.followers.all()
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('follower', 'following')
indexes = [
models.Index(fields=['follower', 'created_at']),
models.Index(fields=['following', 'created_at']),
]
def __str__(self):
return f"{self.follower.username} follows {self.following.username}"
Эта модель имеет проблему: самоподписка возможна. Нужна валидация:
from django.core.exceptions import ValidationError
class Follow(models.Model):
# ... поля ...
def clean(self):
if self.follower == self.following:
raise ValidationError("Нельзя подписаться на себя")
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
Вариант 2: Использование ManyToMany отношения
Альтернативный подход — встроенное отношение:
from django.db import models
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
follows = models.ManyToManyField(
User,
symmetrical=False,
related_name='followers',
through='Follow' # Использовать промежуточную модель
)
def __str__(self):
return self.user.username
class Follow(models.Model):
follower = models.ForeignKey(
UserProfile,
on_delete=models.CASCADE,
related_name='following_relations'
)
following = models.ForeignKey(
UserProfile,
on_delete=models.CASCADE,
related_name='follower_relations'
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('follower', 'following')
Вариант 3: Оптимизированная реализация с кешированием
Для больших объёмов данных нужно кешировать количество подписчиков:
from django.db import models
from django.db.models import Count
import redis
redis_client = redis.Redis()
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
followers_count = models.IntegerField(default=0) # Кешированное значение
following_count = models.IntegerField(default=0)
def get_followers(self, limit=50, offset=0):
# Получить подписчиков с пагинацией
return self.followers.all()[offset:offset+limit]
def get_following(self, limit=50, offset=0):
# Получить подписки
return self.follows.all()[offset:offset+limit]
def is_following(self, user):
# Быстрая проверка подписки (через Redis)
return redis_client.sismember(
f"follows:{self.user_id}",
user.id
)
class Follow(models.Model):
follower = models.ForeignKey(
UserProfile,
on_delete=models.CASCADE,
related_name='following'
)
following = models.ForeignKey(
UserProfile,
on_delete=models.CASCADE,
related_name='followers'
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('follower', 'following')
indexes = [
models.Index(fields=['follower']),
models.Index(fields=['following']),
]
def save(self, *args, **kwargs):
if self.follower == self.following:
raise ValidationError("Нельзя подписаться на себя")
super().save(*args, **kwargs)
# Обновить Redis кеш
redis_client.sadd(
f"follows:{self.follower.user_id}",
self.following.user_id
)
# Обновить счётчики
UserProfile.objects.filter(pk=self.follower.pk).update(
following_count=models.F('following_count') + 1
)
UserProfile.objects.filter(pk=self.following.pk).update(
followers_count=models.F('followers_count') + 1
)
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
# Удалить из Redis
redis_client.srem(
f"follows:{self.follower.user_id}",
self.following.user_id
)
# Обновить счётчики
UserProfile.objects.filter(pk=self.follower.pk).update(
following_count=models.F('following_count') - 1
)
UserProfile.objects.filter(pk=self.following.pk).update(
followers_count=models.F('followers_count') - 1
)
API для подписки/отписки
from django.shortcuts import get_object_or_404
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import login_required
@require_http_methods(["POST"])
@login_required
def follow_user(request, user_id):
"""Подписаться на пользователя"""
target_user = get_object_or_404(User, id=user_id)
if target_user == request.user:
return JsonResponse({"error": "Нельзя подписаться на себя"}, status=400)
follower_profile = UserProfile.objects.get(user=request.user)
target_profile = target_user.userprofile
follow, created = Follow.objects.get_or_create(
follower=follower_profile,
following=target_profile
)
return JsonResponse({
"success": True,
"is_following": True,
"followers_count": target_profile.followers_count,
"following_count": follower_profile.following_count
})
@require_http_methods(["POST"])
@login_required
def unfollow_user(request, user_id):
"""Отписаться от пользователя"""
target_user = get_object_or_404(User, id=user_id)
follower_profile = UserProfile.objects.get(user=request.user)
target_profile = target_user.userprofile
deleted_count, _ = Follow.objects.filter(
follower=follower_profile,
following=target_profile
).delete()
return JsonResponse({
"success": deleted_count > 0,
"is_following": False,
"followers_count": target_profile.followers_count,
"following_count": follower_profile.following_count
})
@login_required
def get_followers(request, user_id):
"""Получить список подписчиков"""
user = get_object_or_404(User, id=user_id)
profile = user.userprofile
page = int(request.GET.get('page', 1))
limit = 20
offset = (page - 1) * limit
followers = profile.followers.all()[offset:offset+limit].values(
'id', 'user__username', 'user__email', 'followers_count'
)
return JsonResponse({
"followers": list(followers),
"total": profile.followers_count,
"page": page,
"limit": limit
})
Использование Celery для асинхронных уведомлений
from celery import shared_task
from django.core.mail import send_mail
@shared_task
def notify_new_follower(follower_id, following_id):
"""Отправить уведомление о новом подписчике"""
follower = User.objects.get(id=follower_id)
following = User.objects.get(id=following_id)
# Отправить email
send_mail(
subject=f"У вас новый подписчик: {follower.username}",
message=f"Пользователь {follower.username} подписался на вас",
from_email="noreply@example.com",
recipient_list=[following.email]
)
class Follow(models.Model):
# ... поля ...
def save(self, *args, **kwargs):
is_new = self.pk is None
super().save(*args, **kwargs)
if is_new:
# Асинхронное уведомление
notify_new_follower.delay(
self.follower.user_id,
self.following.user_id
)
Поиск взаимных подписок (mutual follows)
from django.db.models import Q, Exists, OuterRef
def get_mutual_follows(user_profile):
"""Получить взаимные подписки"""
# Пользователи, на которых подписан текущий пользователь
following_ids = user_profile.following.values_list('id', flat=True)
# Те из них, кто подписан на текущего пользователя
mutual = UserProfile.objects.filter(
id__in=following_ids,
following__id=user_profile.id
).distinct()
return mutual
def get_suggestions(user_profile, limit=10):
"""Предложить пользователей для подписки"""
# Получить пользователей, на которых подписаны мои подписчики
followed_by_followers = UserProfile.objects.filter(
follower_relations__follower__in=(
user_profile.followers.all()
)
).exclude(
id__in=user_profile.following.all()
).exclude(
id=user_profile.id
).annotate(
mutual_count=Count('id')
).order_by('-mutual_count')[:limit]
return followed_by_followers
Оптимизация запросов
from django.db.models import Count, Prefetch
# ❌ Неправильно: N+1 запросы
for user in User.objects.all():
print(user.userprofile.following_count) # Каждый раз запрос!
# ✅ Правильно: select_related / prefetch_related
users = User.objects.select_related('userprofile').all()
for user in users:
print(user.userprofile.following_count) # Нет дополнительных запросов
# Для больших списков подписчиков
profile = UserProfile.objects.prefetch_related(
Prefetch(
'followers',
queryset=UserProfile.objects.select_related('user').order_by('-created_at')[:100]
)
).get(id=1)
Заключение
Механизм подписки можно реализовать:
- Простой вариант: модель Follow с ForeignKey
- С ManyToMany: встроенное отношение Django
- Оптимизированный: с кешированием в Redis и счётчиками
- Асинхронные операции: уведомления через Celery
- Продвинутые операции: взаимные подписки, рекомендации
- Важно: правильные индексы и оптимизация запросов