Opened 3 weeks ago
Last modified 11 days ago
#37201 assigned Bug
Prefetch with to_attr silently ignored if matching property exists
| Reported by: | dc-strahlkraft | Owned by: | zky |
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | 6.0 |
| Severity: | Normal | Keywords: | |
| Cc: | Triage Stage: | Accepted | |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | yes |
| Easy pickings: | no | UI/UX: | no |
Description
Minimal reproduction https://gitlab.com/Dan_Strahlkraft/django-prefetch
Given these models:
class Product(models.Model):
@property
def enabled_images(self):
return self.images.filter(enabled=True)
class ProductImage(models.Model):
product = models.ForeignKey(Product, models.PROTECT, related_name="images")
enabled = models.BooleanField()
I want to eagerly load the enabled_images property so that it doesn't hit the database on access. So I write: Product.objects.prefetch_related(Prefetch("images", ProductImage.objects.filter(enabled=True), to_attr="enabled_images")). But that doesn't prefetch anything. The Prefetch is silently ignored.
The simplest workaround I can think of is:
class Product(models.Model):
@property
def enabled_images(self):
try:
return self.prefetched_enabled_images
except AttributeError:
return self.images.filter(enabled=True)
Then I can write Product.objects.prefetch_related(Prefetch("images", ProductImage.objects.filter(enabled=True), to_attr="prefetched_enabled_images")) and now accessing product.enabled_images won't hit the database, but I think it's uglier.
Change History (9)
comment:1 by , 3 weeks ago
| Triage Stage: | Unreviewed → Accepted |
|---|
comment:2 by , 3 weeks ago
| Owner: | set to |
|---|---|
| Status: | new → assigned |
follow-up: 6 comment:3 by , 3 weeks ago
Re-hashing this issue I wonder if it will even be possible to implement support for property when callable(prop.fset) as the prefetching algorithm requires a form of bookeeping for previously fetched attributes.
In the case where to_attr is not used the already-prefetched sentinel is the presence of the relationship name in _prefetched_objects_cache and if to_attr is used to target a cached_property it's the presence of the to_attr in instance.__dict__. The latter works because cached_property is known to use __dict__ to store a value assigned to it but property implementations are opaque to Django so it can't assume the same.
The various approaches I can think of are
- Adjust the prefetching logic to stop doing already-prefetched bookkeeping using attribute presence as alluded to in ticket:26916#comment:1
- Special case
propertyby setting an extra attribute (e.g._prefetched_to_attr_{name} = True) to denote already-fetched - Fail loudly when detecting a
property(and potentially any descriptor not recognized by Django)
comment:5 by , 3 weeks ago
| Has patch: | unset |
|---|
comment:6 by , 3 weeks ago
Replying to Simon Charette:
Re-hashing this issue I wonder if it will even be possible to implement support for
propertywhencallable(prop.fset)as the prefetching algorithm requires a form of bookeeping for previously fetched attributes.
In the case where
to_attris not used the already-prefetched sentinel is the presence of the relationship name in_prefetched_objects_cacheand ifto_attris used to target acached_propertyit's the presence of theto_attrininstance.__dict__. The latter works becausecached_propertyis known to use__dict__to store a value assigned to it butpropertyimplementations are opaque to Django so it can't assume the same.
The various approaches I can think of are
- Adjust the prefetching logic to stop doing already-prefetched bookkeeping using attribute presence as alluded to in ticket:26916#comment:1
- Special case
propertyby setting an extra attribute (e.g._prefetched_to_attr_{name} = True) to denote already-fetched- Fail loudly when detecting a
property(and potentially any descriptor not recognized by Django)
Hi Simon,
Structurally, I think adjusting the prefetching logic to stop relying on attribute presence for bookkeeping is the best way forward. Moving the prefetch state to an internal state (like obj._state.prefetched_to_attrs) cleanly decouples data loading from class-level descriptors. This fixes the root cause rather than just patching specific descriptors.
However, should we avoid raising a hard exception directly? Since some users might currently have this incorrect usage in their code, would it be better to emit a warning instead?
comment:7 by , 12 days ago
| Has patch: | set |
|---|
follow-up: 9 comment:8 by , 12 days ago
| Patch needs improvement: | set |
|---|
Hey zky,
Structurally, I think adjusting the prefetching logic to stop relying on attribute presence for bookkeeping is the best way forward. Moving the prefetch state to an internal state (like obj._state.prefetched_to_attrs) cleanly decouples data loading from class-level descriptors.
Agreed.
However, should we avoid raising a hard exception directly? Since some users might currently have this incorrect usage in their code, would it be better to emit a warning instead?
I think we should consider going the deprecation route to eventually turns into an exception. In practice today any property usage is silently broken because the bookeeping logic triggers the getter so even if we don't add proper support I think we should consider deprecating towards an exception that suggests using cached_property instead.
Thirumalesh,
The proposed patch ignores the second part of the conversation regarding assignment bookeeping; we can't use to_attr in obj.__dict__ as an heuristic for reasons explained in comment:3. Please avoid submitting patches for tickets already claimed by other contributors, zky in this case.
comment:9 by , 11 days ago
Replying to Simon Charette:
Hey zky,
Structurally, I think adjusting the prefetching logic to stop relying on attribute presence for bookkeeping is the best way forward. Moving the prefetch state to an internal state (like obj._state.prefetched_to_attrs) cleanly decouples data loading from class-level descriptors.
Agreed.
However, should we avoid raising a hard exception directly? Since some users might currently have this incorrect usage in their code, would it be better to emit a warning instead?
I think we should consider going the deprecation route to eventually turns into an exception. In practice today any
propertyusage is silently broken because the bookeeping logic triggers the getter so even if we don't add proper support I think we should consider deprecating towards an exception that suggests usingcached_propertyinstead.
Thirumalesh,
The proposed patch ignores the second part of the conversation regarding assignment bookeeping; we can't use
to_attr in obj.__dict__as an heuristic for reasons explained in comment:3. Please avoid submitting patches for tickets already claimed by other contributors, zky in this case.
Awesome, I'll start working on the implementation.
It's likely something we should document explicitly but the mere presence of an attribute conflicting with a specified
to_attris assumed to denote that the relationship is already fetched and this it is ignore by design.I guess we could adapt the logic added in #26916 for
cached_propertyto allow anypropertywith a setter (not onlycached_property) and fail loudly if pointed at one that doesn't have one.It the mean time your best bet is likely to use
cached_propertyinstead ofproperty.