Django models - как отслеживать поле на объекте, когда значение поля зависит от разных пользователей?

Я хочу иметь объект Book с полем is_read, но значение is_read зависит от user. Когда я впервые создавал это приложение, я думал только об одном пользователе (обо мне).

class Book(models.Model):
    title = models.CharField(max_length=50)
    author = models.CharField(max_length=50)
    is_read = models.BooleanField(default=False)

Но если у приложения несколько пользователей, то, конечно, is_read должен меняться в зависимости от пользователя.

новые модели

class Book(models.Model):
    title = models.CharField(max_length=50)
    author = models.CharField(max_length=50)

class IsRead(models.Model):
    user = models.ForeignKey(User, on_delete=CASCADE)
    book = models.ForeignKey(Book, on_delete=CASCADE)
    is_read = models.BooleanField(default=False)

Я думаю, что добавление класса IsRead поможет, но мне нужно, чтобы объект IsRead автоматически создавался каждый раз, когда создается новый user или book. Не только это, но каждый раз, когда создается новый пользователь, мне приходится перебирать все книги. Или если добавляется новая книга, я должен перебрать всех пользователей. Кажется, что это очень много работы с базами данных только для того, чтобы отследить, кто какие книги прочитал.

Даже если вышеописанное является правильной стратегией, я не знаю, как бы я это сделал. Я попытался перезаписать AdminModel, чтобы сохранить IsRead, но это не сработало. Я не получил никаких ошибок, но IsRead не сохранился.

class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'author')

    def save_model(self, request, obj, form, change):
        obj.user = request.user
        IsRead.objects.update_or_create(book=id, user=request.user, defaults={'book': self.book, 'user': obj.user, 'my_status': False})
        super(BookAdmin, self).save_model(request, obj, form, change

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

Вы можете сделать это примерно так:

@receiver(post_save, sender=Book)
def on_book_create(sender, instance, created, *args, **kwargs):
    if not created or kwargs.get("raw"):
        return
    
    # fetch all users
    # loop through users and create an IsRead object

и сделать аналогичный сигнал для on_create_user тоже, где вы получаете все книги и создаете объект IsRead, связанный с этим пользователем.

Лучшим способом является использование возможностей отношения Many-to-Many в django. Начните с добавления поля для этого отношения

class Book(models.Model):
    ...
    readers = models.ManyToManyField(User)

Затем вы можете добавить читателей к экземпляру книги следующим образом:

# 1, 2, 3 are user pks
book.readers.add(1, 2, 3)
# or a user instance
book.readers.add(user)

После этого мы можем добавить пользовательские функции к моделям User и Book, чтобы узнать, была ли книга прочитана пользователем или прочитал ли пользователь определенную книгу.

class User(models.Model):
    ...
    def has_read(self, book_id):
        return self.book_set.filter(pk=book_id).exists()


class Book(models.Model):
    ...
    def is_read(self, user_id):
        return self.readers.filter(pk=user_id).exists()

Чтобы проверить, мы можем сделать это так:

user.has_read(book.pk)

# or like this

book.is_read(user.pk)

В качестве бонуса вы можете получить книги, прочитанные пользователем user.book_set.all(), а также всеми читателями этой книги book.readers.all()

Последнее, для этого:

Я больше думал о is_read - это статус прочитанной книги. A m2m отношения между книгой и пользователем были бы больше похожи на то, что пользователь владеет книгой.

Если вы хотите представить право собственности пользователя на определенную книгу, вы можете добавить другое поле "многие-ко-многим", например owners или что-то подобное. Как и в реальной жизни, ваше отношение к книге не ограничивается вашим правом собственности на нее.

Добавление другого отношения "многие ко многим" с использованием той же модели требует установки параметра related_name. Это делается для того, чтобы не запутать django в том, какое отношение вы хотите получить.

class Book(models.Model):
    ...
    readers = models.ManyToManyField(User, related_name="read_books")
    owners = models.ManyToManyField(User, related_name="owned_books")

Вы можете иметь запрос book->user отношения, используя имя поля book.readers.all() или book.owners.all(), но для обратного, вам нужно ссылаться на связанное имя для каждого: user.read_books.all() или user.owned_books.all().

Это не так уж далеко от вашего оригинального класса IsRead, поскольку django создает поворотную таблицу для моделей книги и пользователя за сценой, которая выглядит примерно так:

many-to-many-pivot-table

Вместо специального поля is_read для проверки статуса чтения, существование записи подразумевает это, поэтому мы используем .exists()

Сложив их вместе, можно получить что-то вроде:

class User(models.Model):
    name = models.CharField(max_length=50)

    def has_read(self, book_id):
        return self.read_books.filter(pk=book_id).exists()

    def owns_book(self, book_id):
        return self.owned_books.filter(pk=book_id).exists()

class Book(models.Model):
    title = models.CharField(max_length=50)
    author = models.CharField(max_length=50)
    readers = models.ManyToManyField(User, related_name="read_books")
    owners = models.ManyToManyField(User, related_name="owned_books")

    def is_read(self, user_id):
        return self.readers.filter(pk=user_id).exists()

    def is_owned(self, user.id):
        return self.owners.filter(pk=user.id).exists()

Вернуться на верх