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 создает поворотную таблицу для моделей книги и пользователя за сценой, которая выглядит примерно так:
Вместо специального поля 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()