Exists a way to check if related objects has been prefetched?

For example whit these models:

class Course(models.Model):
    name = models.CharField(max_length=50)

class Module(models.Model):
    course = models.ForeignKey(Course, related_name='modules')
    name = models.CharField(max_length=50)

    class Meta:
        order_with_respect_to = 'course'

I want to make a function that return the course last module id but the related course.modules maybe prefetched or no, so this function would be like.

@cached_property
def latest_module_id(self) -> int:
    try:
        if modules_are_loaded:
            return self.modules.all().latest().pk

        return Module.objects.filter(course=self)\
            .values_list('id', flat=True)\
            .latest()
    except Module.DoesNotExist:
        return None

Exists a way to check if some related objects has already prefetched?

I’m not clear on what you’re trying to accomplish here.

Django manages its own cache for data retrieved from the database. (See multiple references at QuerySet API reference | Django documentation | Django)

If Django has loaded data for a view, all future references for that data will pull what has already been retrieved.

On top of that PostgreSQL (among others) maintains their own cache, such that multiple identical queries don’t necessary need to retrieve data from disk.

Unless you’ve got a specific, identified issue where this is a problem, this generally isn’t worth worrying about. (And, if you are in a situation where this is a problem, there are generally other issues you want to address first.)

1 Like

As you are saying

If Django has loaded data for a view, all future references for that data will pull what has already been retrieved.

If a do these queries

course = Course.objects.get(pk=1)
# and
course = Course.objects.pretetch_related('modules').get(pk=1)

The 2nd has already their related modules in cache, so if i need to know the last module id, i must be use this modules on cache instead of making another query.
In the 1st i don’t have the related modules, so i make another query that only retrieve the data that i need, instead to bring all objects to memory.

I’m trying to write a model method that handle both cases and i think a i need to know if the course has already their modules on cache or not.

You don’t write models methods for this. The retrieval of data is handled within either your views or a Model Manager. A model method should simply refer to the function necessary to retrieve the latest module it.

If you’re writing a view that doesn’t need the last module id, you write the first query you posted.

If you’re writing a view that does, then your view uses the second query.

Keep in mind that all view invocations are independent of each other. When view “X” refers to model “A”, then view “Y” also refers to model “A”, they are most likely going to be different instances of model “A”. These model instances do not have a “life of their own” outside the invocation of a view.

If you’re writing a view that doesn’t need the last module id, you write the first query you posted.
If you’re writing a view that does, then your view uses the second query.

Yeah, that’s exactly how it works currently, but I’m trying refactor that behavior. Maybe I change that method to a utility or just leave it as it is.

The python zen says

There should be one-- and preferably only one --obvious way to do it.

So I’m trying to make a only one way to retrieve the last course module id, regardless if the instance has or not those related objects in memory.

The question in the first place was if how can I check if related objects has been prefetched or no. I searched in the documentation and seems that does not exist a way to accomplish that. I appreciate the code recommendations but that’s not what the topic is about.

I’m changing the topic title.

Yes. Work with Django and not against it. You’re trying to refactor into a pattern contrary to what roughly 15 years of Django architecture has built.

Your model method should only do something like:

def latest_module_id(self):
    return self.module_set.all().latest().pk

If Django has that set prefetched, it’ll use it. If it doesn’t, it’ll retrieve it.

You could prefetch to an attribute:

.prefetch_related(Prefetch('modules', to_attr='custom_attr'))...

Then you can check on the model:

if hasattr(self, 'custom_attr'):
    # its been prefetched

Thanks @djangoat

.prefetch_related(Prefetch('modules', to_attr='custom_attr'))

That’s a nice option, maybe I change to that form or, instead to declare a @property, I can use a model method or utility function with a bool arg to say if the related objects are in cache or not.

I read the prefetch_related documentation again and see that use .latest() on a prefetched queryset produce another hit to the database, so the above method for the example can be

self.modules.all()[len(self.modules.all())-1].pk

That doesn’t hit the database again.

Now, if a do self.modules.all().latest().pk, brings to memory all model values, and that’s not what a want, as it django optimization says, I should only retrieve the data that I need. For the example, I only need the last module pk, if the modules are in memory, simple do it a loop until the last, else, do it a queryset that only retrieve de last module pk.

What I wanted to know is if exists a option like self.related_set.loaded() that return a bool and seems that not exists.

Thanks for the recommendations.

That is not an accurate statement. You can verify a couple different ways that it does not necessarily invalidate the prefetched queryset and does not result in an extra query, beyond the original second query required by the prefetch_related.

I make this query

image

It seems that the .latest() hit the database again.

But, here
image

It don’t do an extra query.

Everytime I do a .latest() on a queryset (prefetched or not) execute another query. Maybe I’m doing something wrong?

Nope - I stand corrected here - I was the one misreading the results of my test. You are correct, that does result in an extra query.