Is there a way to check whether a related object is already fetched?
Asked Answered
D

3

16

I would like to be able to check if a related object has already been fetched by using either select_related or prefetch_related, so that I can serialize the data accordingly. Here is an example:

class Address(models.Model):
    street = models.CharField(max_length=100)
    zip = models.CharField(max_length=10)

class Person(models.Model):
    name = models.CharField(max_length=20)
    address = models.ForeignKey(Address)

def serialize_address(address):
    return {
        "id": address.id,
        "street": address.street,
        "zip": address.zip
    }

def serialize_person(person):
    result = {
        "id": person.id,
        "name": person.name
    }
    if is_fetched(person.address):
        result["address"] = serialize_address(person.address)
    else:
        result["address"] = None

######

person_a = Person.objects.select_related("address").get(id=1)
person_b = Person.objects.get(id=2)

serialize_person(person_a) #should be object with id, name and address
serialize_person(person_b) #should be object with only id and name

In this example, the function is_fetched is what I am looking for. I would like to determine if the person object already has a resolves address and only if it has, it should be serialized as well. But if it doesn't, no further database query should be executed.

So is there a way to achieve this in Django?

Debar answered 4/4, 2016 at 12:3 Comment(0)
K
11

If the address relation has been fetched, then the Person object will have a populated attribute called _address_cache; you can check this.

def is_fetched(obj, relation_name):
    cache_name = '_{}_cache'.format(relation_name)
    return getattr(obj, cache_name, False)

Note you'd need to call this with the object and the name of the relation:

is_fetched(person, 'address')

since doing person.address would trigger the fetch immediately.

Edit reverse or many-to-many relations can only be fetched by prefetch_related; that populates a single attribute, _prefetched_objects_cache, which is a dict of lists where the key is the name of the related model. Eg if you do:

addresses = Address.objects.prefetch_related('person_set')

then each item in addresses will have a _prefetched_objects_cache dict containing a "person' key.

Note, both of these are single-underscore attributes which means they are part of the private API; you're free to use them, but Django is also free to change them in future releases.

Knobloch answered 4/4, 2016 at 12:9 Comment(3)
Thanks, that sounds promising. Will it also work for ManyToMany relations or reverse ForeignKey relations? For instance: '_topping_set_cache'?Debar
While this is the accepted answer, note that it no longer works in a more recent version of Django. obj._state.fields_cache seems like the way to go now.Knut
This do not works for reverse m2m cache, if fetched using Person.objects.prefetch_related('address').all(). Pre-fetched values available in obj._state.fields_cache for .select_related(''), but not for .prefetch_related. How django returns pre-fetched related objects from cache? Where it stores and do we have access to read it?Gomorrah
A
23

Since Django 2.0 you can easily check for all fetched relation by:

obj._state.fields_cache

ModelStateFieldsCacheDescriptor is responsible for storing your cached relations.

>>> Person.objects.first()._state.fields_cache
{}
>>> Person.objects.select_related('address').first()._state.fields_cache
{'address': <Address: Your Address>}
Airlie answered 14/5, 2019 at 9:25 Comment(1)
Thank you for this answer, I created a Django ticket to see if this attribute should be considered public or private (code.djangoproject.com/ticket/31803)Knut
K
11

If the address relation has been fetched, then the Person object will have a populated attribute called _address_cache; you can check this.

def is_fetched(obj, relation_name):
    cache_name = '_{}_cache'.format(relation_name)
    return getattr(obj, cache_name, False)

Note you'd need to call this with the object and the name of the relation:

is_fetched(person, 'address')

since doing person.address would trigger the fetch immediately.

Edit reverse or many-to-many relations can only be fetched by prefetch_related; that populates a single attribute, _prefetched_objects_cache, which is a dict of lists where the key is the name of the related model. Eg if you do:

addresses = Address.objects.prefetch_related('person_set')

then each item in addresses will have a _prefetched_objects_cache dict containing a "person' key.

Note, both of these are single-underscore attributes which means they are part of the private API; you're free to use them, but Django is also free to change them in future releases.

Knobloch answered 4/4, 2016 at 12:9 Comment(3)
Thanks, that sounds promising. Will it also work for ManyToMany relations or reverse ForeignKey relations? For instance: '_topping_set_cache'?Debar
While this is the accepted answer, note that it no longer works in a more recent version of Django. obj._state.fields_cache seems like the way to go now.Knut
This do not works for reverse m2m cache, if fetched using Person.objects.prefetch_related('address').all(). Pre-fetched values available in obj._state.fields_cache for .select_related(''), but not for .prefetch_related. How django returns pre-fetched related objects from cache? Where it stores and do we have access to read it?Gomorrah
K
6

Per this comment on the ticket linked in the comment by @jaap3 above, the recommended way to do this for Django 3+ (perhaps 2+?) is to use the undocumented is_cached method on the model's field, which comes from this internal mixin:

>>> person1 = Person.objects.first()
>>> Person.address.is_cached(person1)
False

>>> person2 = Person.objects.select_related('address').last()
>>> Person.address.is_cached(person2)
True
Krp answered 30/6, 2021 at 18:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.