最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

Should I reach into the Django _prefetched_objects_cache to solve an N+1 query? - Stack Overflow

programmeradmin2浏览0评论

I have the following Django template code with an N+1 query:

{% for theobject in objs %}
  {% for part in theobject.parts_ordered %}
      <li>{{ part }}</li>
  {% endfor %}
{% endfor %}

Here is parts_ordered on TheObject:

  class TheObject:
    # ...
    def parts_ordered(self) -> list["Part"]:
        return self.parts.all().order_by("pk")

And here is the Part object:

  class Part:
    # ...
    theobject = models.ForeignKey(
        TheObject, on_delete=models.CASCADE, related_name="parts"
    )

and here is the prefetch getting objs:

    ofs = ObjectFormSet(
        queryset=TheObject.objects
        .filter(objectset=os)
        .prefetch_related("parts")
    )

I think the order_by("pk") disrupts the prefetch.

This is what chatgpt recommends, and it works (no more N+1 queries, results seem the same):

  class TheObject:
    # ...
    def parts_ordered(self) -> list["Part"]:
        if (
            hasattr(self, "_prefetched_objects_cache")
            and "parts" in self._prefetched_objects_cache
        ):
            # Use prefetched data and sort in Python
            return sorted(
                self._prefetched_objects_cache["parts"], key=lambda cc: cc.pk
            )

        # Fallback to querying the DB if prefetching wasn’t used
        return self.parts.all().order_by("pk")

Should I rely on _prefetched_objects_cache? Is there a better way?

I have the following Django template code with an N+1 query:

{% for theobject in objs %}
  {% for part in theobject.parts_ordered %}
      <li>{{ part }}</li>
  {% endfor %}
{% endfor %}

Here is parts_ordered on TheObject:

  class TheObject:
    # ...
    def parts_ordered(self) -> list["Part"]:
        return self.parts.all().order_by("pk")

And here is the Part object:

  class Part:
    # ...
    theobject = models.ForeignKey(
        TheObject, on_delete=models.CASCADE, related_name="parts"
    )

and here is the prefetch getting objs:

    ofs = ObjectFormSet(
        queryset=TheObject.objects
        .filter(objectset=os)
        .prefetch_related("parts")
    )

I think the order_by("pk") disrupts the prefetch.

This is what chatgpt recommends, and it works (no more N+1 queries, results seem the same):

  class TheObject:
    # ...
    def parts_ordered(self) -> list["Part"]:
        if (
            hasattr(self, "_prefetched_objects_cache")
            and "parts" in self._prefetched_objects_cache
        ):
            # Use prefetched data and sort in Python
            return sorted(
                self._prefetched_objects_cache["parts"], key=lambda cc: cc.pk
            )

        # Fallback to querying the DB if prefetching wasn’t used
        return self.parts.all().order_by("pk")

Should I rely on _prefetched_objects_cache? Is there a better way?

Share Improve this question asked Mar 14 at 15:05 dfrankowdfrankow 21.5k44 gold badges162 silver badges241 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 2

I wouldn't use private methods, or attributes in your case. As a rule, library authors mark them as non-public for a reason. Most often they are implementation details that may change in some next version, which may cause your code to stop working.

In your case, you can make things simpler, but still solve the N + 1 problem. For this you can use Prefetch object and this query:

from django.db import models  
  
  
ofs = ObjectFormSet(  
    queryset=TheObject.objects  
    .filter(objectset=os)  
    .prefetch_related(  
        models.Prefetch(  
            lookup='parts',  
            queryset=Part.objects.order_by('pk'),  
            to_attr='parts_ordered',  
        ),  
    )  
)

This will give similar results to yours and should improve overall performance a bit, since the database is doing the sorting and not python.


UPDATED

To answer your question from the comment - you just need to remove the parts_ordered method, I meant that the queryset in my answer is its replacement. You can also use any other attr name for the to_attr argument, for example:

ofs = ObjectFormSet(  
    queryset=TheObject.objects  
    .filter(objectset=os)  
    .prefetch_related(  
        models.Prefetch(  
            lookup='parts',  
            queryset=Part.objects.order_by('pk'),  
            to_attr='ordered_parts',  
        ),  
    )  
)

And then in your template you should use it this way:

{% for theobject in objs %}
  {% for part in theobject.ordered_parts %}
      <li>{{ part }}</li>
  {% endfor %}
{% endfor %}

For more flexible use, if this query can be used in different places. You can define this as a method for your manager. It would look something like this:

from django.db import models  
  
  
class TheObjQuerySet(models.QuerySet):  
    def attach_ordered_parts(self):  
        return self.prefetch_related(  
            models.Prefetch(  
                lookup='parts',  
                queryset=Part.objects.order_by('pk'),  
                to_attr='ordered_parts',  
            )  
        )  
  
  
class TheObject(models.Model):  
    # ...  
    objects = TheObjQuerySet.as_manager()


ofs = ObjectFormSet(  
    queryset=TheObject.objects  
    .filter(objectset=os)  
    .attach_ordered_parts()
)

And accordingly the code in your template:

{% for theobject in objs %}
  {% for part in theobject.ordered_parts %}
      <li>{{ part }}</li>
  {% endfor %}
{% endfor %}
发布评论

评论列表(0)

  1. 暂无评论