Как спроектировать модель данных для системы подписок пользователей

Ответ

Для реализации механизма подписок используется классический паттерн "многие-ко-многим" (Many-to-Many) через промежуточную (связующую) таблицу.

Эта таблица хранит пары идентификаторов: кто подписался (subscriber) и на кого подписались (target).

Пример реализации на Django ORM

Модель Subscription выступает в роли связующей таблицы между пользователями (User).

from django.db import models
from django.contrib.auth.models import User

class Subscription(models.Model):
    """Модель для хранения подписок пользователей друг на друга."""

    # Пользователь, который подписывается
    subscriber = models.ForeignKey(
        User, 
        on_delete=models.CASCADE,
        related_name='following'  # user.following -> на кого он подписан
    )

    # Пользователь, на которого подписываются
    target = models.ForeignKey(
        User, 
        on_delete=models.CASCADE,
        related_name='followers'  # user.followers -> кто на него подписан
    )

    created_at = models.DateTimeField(
        auto_now_add=True, 
        verbose_name="Дата подписки"
    )

    class Meta:
        # Гарантирует, что пользователь не может подписаться на другого дважды
        unique_together = ('subscriber', 'target')
        ordering = ['-created_at']

    def __str__(self):
        return f"{self.subscriber} follows {self.target}"

Ключевые аспекты этой архитектуры:

  1. Промежуточная таблица: Subscription связывает двух пользователей.
  2. Внешние ключи: subscriber и target ссылаются на одну и ту же модель User.
  3. related_name: Позволяет удобно получать связанные объекты. Например, user.followers.all() вернет всех подписчиков пользователя.
  4. Ограничение уникальности: unique_together предотвращает дублирование записей о подписке.
  5. Масштабируемость: Этот подход легко расширяется, например, добавлением поля для уведомлений.

Ответ 18+ 🔞

А, ну это ж классика, блядь! Подписки, лента, друзья-подписчики — вся эта социальная хуйня строится на одном и том же костыле, который называется «многие-ко-многим».

Представь себе, два пользователя. Один — подписчик, другой — тот, на кого подписались. И между ними надо как-то связать, кто на кого вешается, чтобы потом не было, как в том анекдоте: «А я тебя не подписывал, блядь!».

Вот для этого и делают промежуточную таблицу-связку. Она просто тупо хранит пары айдишников: кто подписался и на кого. Всё, ебушки-воробушки, больше от неё нихуя не нужно. Ну, кроме даты, чтобы знать, когда эта ебля началась.

Смотри, как это на Django ORM выглядит, прям как в жизни:

from django.db import models
from django.contrib.auth.models import User

class Subscription(models.Model):
    """Модель для хранения подписок пользователей друг на друга."""

    # Пользователь, который подписывается
    subscriber = models.ForeignKey(
        User, 
        on_delete=models.CASCADE,
        related_name='following'  # user.following -> на кого он подписан
    )

    # Пользователь, на которого подписываются
    target = models.ForeignKey(
        User, 
        on_delete=models.CASCADE,
        related_name='followers'  # user.followers -> кто на него подписан
    )

    created_at = models.DateTimeField(
        auto_now_add=True, 
        verbose_name="Дата подписки"
    )

    class Meta:
        # Гарантирует, что пользователь не может подписаться на другого дважды
        unique_together = ('subscriber', 'target')
        ordering = ['-created_at']

    def __str__(self):
        return f"{self.subscriber} follows {self.target}"

А теперь, сука, разжёвываю, что тут к чему:

  1. Промежуточная таблица: Это и есть наша Subscription. Она как сводница, блядь, знакомит subscriber с target. Больше она нихуя не делает.
  2. Внешние ключи: Оба поля — subscriber и target — тычут пальцем в одну и ту же модель User. Потому что пользователь может быть и тем, и другим. Он сегодня подписчик, а завтра на него уже овердохуища народу подписалось.
  3. related_name: Вот это, блядь, магия! Благодаря этому можно писать user.followers.all() и получить всех, кто на него вешается. Или user.following.all() — чтобы увидеть, на кого он сам вешается. Удобно, ёпта!
  4. Ограничение уникальности: unique_together — это чтобы один мудак не мог дважды подписаться на другого мудака. Защита от долбоёбов, которые кнопку «Подписаться» как обезьяна тыкают.
  5. Масштабируемость: А это самое охуенное. Захотел добавить, например, поле notify = models.BooleanField() — чтобы подписчик выбирал, присылать ему уведомления или нет. И всё, блядь, добавляешь одно поле в модель — и функционал расширился. Красота, в рот меня чих-пых!

Вот и вся философия. Ничего сложного, просто табличка, которая спаривает пользователей. Как в зоопарке.