Milano
Milano

Reputation: 18745

Django - prefetch_related for specific situation doesn't work

I have a three models: Trip, City and GoogleApiCityCache.

If I want to know name of the city, I use @property on City object which tries to get GoogleApiCityCache for current language.

I want to reduce a number of queries in my template:

{% for trip in trips %}
    {{ trip.city.formatted_address }}
{% endfor %} 

Now, it performs new db query to find GoogleApiCityCache in each iteration. I want to prefetch these caches.

So property formatted_address has to find a GoogleApiCityCache and get formatted_address from this object.

Tried to use prefetch_related but it doesn't help.

def profile(request, slug):
    owner = User.objects.filter(userprofile__slug=slug).prefetch_related('trips__city__city_caches').first()
    trips = Trip.objects.filter(user=owner).select_related('city').prefetch_related('city__city_caches')
    return render(request, 'profiles/profile/profile.html', {'owner': owner,'trips':trips})

My models:

class Trip(models.Model):
    user = models.ForeignKey('auth.User', related_name='trips')
    city = models.ForeignKey('locations.City', related_name='trips')
    date_from = models.DateField()
    date_to = models.DateField()
    detail = models.TextField(null=True, blank=True)
    # participants = models.ManyToManyField('auth.User', related_name='participating_on_trips', blank=True)
    objects = TripManager()

class City(models.Model):
    ...
    def get_cache_by_lang(self, lang=None, force_refresh=False):
        if not lang:
            lang = get_language() or 'en'
        cache, requested = GoogleApiCityCache.objects.get_or_request(city=self, lang=lang)
        return cache

    @property
    def formatted_address(self):
        return self.get_cache_by_lang().formatted_address

class GoogleApiCityCacheManager(models.Manager):
    def _request_create(self, city, lang):
        """ Create cache (wasn't found) """
        ....
        return cache

    def get_or_request(self, city, lang):
        created = False
        try:
            cache = GoogleApiCityCache.objects.get(city=city, language_code=lang)
        except GoogleApiCityCache.DoesNotExist:
            cache = self._request_create(city, lang)
            created = True
        return cache,created

class GoogleApiCityCache(models.Model):
    language_code = models.CharField(max_length=5)
    api_json_response = JSONField(null=True, blank=True)
    city = models.ForeignKey('locations.City', on_delete=models.CASCADE, related_name='city_caches')
    objects = GoogleApiCityCacheManager()

    class Meta:
        unique_together = ('city', 'language_code')


    @property
    def formatted_address(self):
        try:
            return self.api_json_response['result']['formatted_address']
        except:
            return None

As you can see, GoogleApiCityCache for city and language_code can be none. In this case, the Google Api is requested, created a new GoogleApiCityCache object and returned.

Upvotes: 0

Views: 1770

Answers (1)

Alasdair
Alasdair

Reputation: 309099

When you prefetch_related('trips__city__city_caches'), you are prefetching the city_caches. That means that the following loop will not cause any extra queries.

for cache in city.city_caches.all()
    print(cache)

However, in your code formatted_address calls get_cache_by_lang which calls get_or_request which calls GoogleApiCityCache.objects.get(), which will cause extra queries.

In order to take advantage of prefetch_related, you need to rewrite get_cache_by_lang so that it loops through self.city_caches.all() to find the correct object.

Upvotes: 2

Related Questions