Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make Django comments use select_related() on "user" field?

I'm using django comments frameworks. All the comments are posted by authenticated users. Near the comment, I'm showing some user profile info using {{ comment.user.get_profile }}

{# custom comment list templates #}
<dl id="comments">
  {% for comment in comment_list %}
    <dt id="c{{ comment.id }}">
        {{ comment.submit_date }} - {{ comment.user.get_profile.display_name }}
    </dt>
    <dd>
        <p>{{ comment.comment }}</p>
    </dd>
  {% endfor %}
</dl>

Problem is that django's comment queries does not use select_related() and for 100 comments I get 101 hit on the database.

Is there a way to make django comments framework to select user profile for each comment in one go?

like image 897
Silver Light Avatar asked Oct 25 '11 09:10

Silver Light


2 Answers

I tested rendering 100 comments for an object with the default {% get_comment_list %} tag and django did 200 comment related queries to list the comments + user + profile because...

  1. Comment.__unicode__ actually calls Comment.user if a user_id exists. +1 query
  2. get_profile +1 query

Ouch!

I went from 203 queries in ~25ms to 3 in ~2ms.

Populate comment_list yourself

I would highly suggest building the comment_list QuerySet yourself using the appropriate select_related() calls. If it's used often, create a utility function called from your other views.

def get_comments_with_user_and_profile(obj):
    content_type =ContentType.objects.get_for_model(obj)
    return (Comment.objects
        .filter(content_type=content_type, object_pk=obj.id)
        .select_related('user__profile'))

If you want the entire framework to behave this way... You'll have to monkey patch.

It's not something I would do lightly. There are other ways around this specific problem but you did ask "in one go".

Put this somewhere in your INSTALLED_APPS models.py files. I actually have a monkey_patch app for modifying django.contrib.auth.User.username lengths and such (which is a last resort unlike here).

from django.contrib.comments.models import Comment
from django.contrib.comments.managers import CommentManager

class CommentManager(CommentManager):
    def get_query_set(self):
        return (super(CommentManager, self)
            .get_query_set()
            .select_related('user__profile'))
Comment.add_to_class('objects', CommentManager())

Gotchas with profiles and select_related()

Note that your UserProfile class needs a OneToOneField to User with a related_name equal to what you pass to select_related(). In my example it's profile and you need django 1.2+. I recall stumbling on that before.

class UserProfile(models.Model):
    user = models.OneToOneField(User, related_name='profile') 
    # example to use User.objects.select_related('profile')
like image 132
Yuji 'Tomita' Tomita Avatar answered Nov 14 '22 17:11

Yuji 'Tomita' Tomita


Assuming that you have a setup like so:

class UserProfile(models.Model):
    user = models.ForeignKey(User, related_name='profile')
    ...

You can use the following select related: Comments.objects.select_related('user__pk','user__profile__pk') and that should do what you want.

You'll have to extend the comments framework. This is fairly straightforward. Basically, create your own comments app. You can look at django-threadedcomments for inspiration (and, actually, in some ways it's already a better implementation to use anyway).

Here's code you can insert into the django-threaded comments app to make sure it always uses the select related (in models.py):

class RelatedCommentManager(CommentManager):
    def filter(self, *args, **kwargs):
        return super(RelatedCommentManager, self).select_related('user__pk','user__profile__pk').filter(*args, **kwargs)

    def exclude(self, *args, **kwargs):
        return super(RelatedCommentManager, self).select_related('user__pk','user__profile__pk').exclude(*args, **kwargs)

    def all(self)
        return super(RelatedCommentManager, self).select_related('user__pk','user__profile__pk').all()

and replace

    objects = CommentManager()

with

    objects = RelatedCommentManager()

Follow the instructions for integrating threadedcomments into your app.

Then, in the template, I think you'll have to reference .profile instead of .get_profile.

It may be that Django automatically factors this in, so get_profile will not generate another db hit so long as .profile is available.

like image 4
Jordan Reiter Avatar answered Nov 14 '22 17:11

Jordan Reiter