Say I have a Post model like this:
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
text_content = models.CharField()
@property
def comment_count(self):
return self.comments.count()
Say I need to get all the data for the post with ID 3. I know that I can retrieve the user along with the post in one query, by doing Post.objects.select_related('user').get(pk=3)
, allowing me to save a query. However, is there a way to automatically fetch comment_count
as well?
I know I could use Post.objects.annotate(comment_count=Count('comments'))
(which will require me to remove the property or change the name of the annotation, as there would be an AttributeError
), however is it possible to make the ORM do that part automatically, since the property is declared in the model?
Although I could just add the .annotate
, this can get very tedious when there are multiple properties and foreign keys that need to be fetched on multiple places.
CodePudding user response:
Maybe this is not a perfect solution for you but you might find something similar that works. You could add a custom object manager, see docs
Then you could add a method like with_comment_count()
. OR if you always want to annotate, then you can modify the initial queryset.
class PostManager(models.Manager):
def with_comment_count(self):
return self.annotate(
comment_count=Count('comments')
)
# Or this to always annotate
def get_queryset(self):
return super().get_queryset().annotate(
comment_count=Count('comments')
)
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
text_content = models.CharField()
objects = PostManager()
Then you could query like this.
post = Post.objects.get(pk=1).with_comment_count()
post.comment_count
CodePudding user response:
Thanks to Felix Eklöf I managed to get it to work! As specified in the question, this was in the case in which there were multiple properties/foreign keys that need prefetching, and as such the ability to 'chain' prefetching was needed. This isn't possible by chaining PostManager
methods, since they don't return a PostManager
but a QuerySet
, however I came up with a function that caches whatever it is asked to cache all at once, avoiding the need for chaining methods:
class PostManager(models.Manager):
def caching(self, *args):
query = self.all()
if 'views' in args:
query = query.annotate(view_count=Count('views'))
if 'comments' in args:
query = query.annotate(comment_count=Count('comments'))
if 'user' in args:
query = query.select_related('user')
if 'archives' in args:
query = query.prefetch_related('archives')
return query
class Post(models.Model):
objects = PostManager()
...
# caching can then be added by doing, this, for instance:
Post.objects.caching('views', 'user', 'archives').get(id=3)
Note that for this to work, I also had to change @property
to @cached_property
, thus allowing the ORM to replace the value, and allow proper caching with the same name.