0

I have a custom user profile model. This model has can_edit property that uses ContentType and Permission objects to determine if the user has permission or not. I'm using this property within serializer and it works fine, but is terribly inefficient, as per each user the ContentType and Permission are queried again.

It seems like prefetch_related could fit here, but I don't know how to apply it, as these objects are not referenced directly through some properties, but kinda queried separately. Is it possible to fetch the ContentType and Permission beforehand and just use the results in further queries?

Here's my model:

class CustomProfile(models.Model):
    user = models.OneToOneField(User, related_name="profile", on_delete=models.CASCADE)

    @property
    def can_edit(self):
        content_type = ContentType.objects.get_for_model(Article)
        permission, _ = Permission.objects.get_or_create(
            codename="edit", name="Can edit", content_type=content_type
        )
        return self.user.has_perm(self._permission_name(permission))

    def _permission_name(self, permission):
        return f"{permission.content_type.app_label}.{permission.codename}"

My current query:

User.objects.order_by("username").select_related("profile")

My serializer:

class UserSerializer(serializers.ModelSerializer):
    can_edit = serializers.ReadOnlyField(source="profile.can_edit")

    class Meta:
        model = User
        fields = (
            "id",
            "username",
            "first_name",
            "last_name",
            "is_active",
            "is_staff",
            "can_edit",
        )

In fact, I have more than one property with similar content to can_edit, so each user instance adds around 6 unnecessary queries for ContentType and Permission.

How can I optimize it?

6
  • not sure if this is what you want but checkout cached_property docs.djangoproject.com/en/3.1/ref/utils/… Commented Oct 5, 2020 at 12:58
  • no, there are multiple objects, each of them will call its own property which might have different value, and all of them are repeating the same queries underneath Commented Oct 5, 2020 at 13:12
  • ", I have more than one property with similar content to can_edit" On this UserSerializer class or some other class? @Djent Commented Oct 7, 2020 at 13:01
  • @ArakkalAbu - in the same class Commented Oct 8, 2020 at 8:08
  • @Djent Can you add an example of it? (maybe I can provide an accurate solution) Commented Oct 8, 2020 at 8:09

4 Answers 4

2
+150

Actually, you don't have to reinvent the wheel, Django already has done it for us.

Here change your can_edit(...) as below.

class CustomProfile(models.Model):
    user = models.OneToOneField(User, related_name="profile", on_delete=models.CASCADE)

    @property
    def can_edit(self):
        return self.user.has_perm("app_name_article.can_edit")

Here, the user.has_perm()--(Django Doc) is beautifully built in an efficient way

Note

  • I don't think we need to find out the <app_label>_<model_name> string in a "programmatic" way.
Sign up to request clarification or add additional context in comments.

3 Comments

This is surely the "recommended" solution! It's much better to cache the result of a check rather than caching the steps necessary to perform said check (especially when it's builtin). Do note that this comes with caveats, for example, you will need to repopulate the cache if you are programmatically adding a permission to a user, then immediately checking it. See the docs on **Permission Caching for more info.
Thanks, it makes sense. I mean, I still kinda need to get the permission names programmatically, but I can cache them as a class attribute for CustomProfile.
@Djent "....I still kinda need to get the permission names programmatically....", Unfortunately, I don't see any such situation in this given context. IMHO, use hard-coded strings instead of programmatic way (in this situation)
0

Use a __init__ special method to initialize any propertys/variables that you need to use more then once.

Example:

class CustomProfile(models.Model):
   user = models.OneToOneField(User, related_name="profile", on_delete=models.CASCADE)
   
   def __init__(self, *args, **kwarg):
       super().__init__(*args, **kwargs)
       self.content_type = ContentType.objects.get_for_model(Article)
       self.permission, _ = Permission.objects.get_or_create(codename="edit", name="Can edit", content_type=content_type)

    @property
    def can_edit(self):
       return self.user.has_perm(self._permission_name(self.permission))

Now you can use these values in your methods.

1 Comment

But it still will be repeated multiple times as the multiple profile objects will be instantiated.
0

Use @cached_property instead of @property to cache can_edit.
https://docs.djangoproject.com/en/3.1/ref/utils/#django.utils.functional.cached_property

Comments

0

For optimizing the query, you can use prefetch_related in combination with Case...When Conditional Expressions

from django.db.models import Case, When, BooleanField

qs = User.objects.prefetch_related('user_permissions').annotate(
      can_edit = Case(
      When(user_permissions__codename='edit_article', then=True),
      default=False, 
      output_field=BooleanField())
     )

# check the results
>>> [(i.can_edit, i.username) for i in qs] 

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.