Opened 13 years ago
Closed 12 years ago
#17271 closed New feature (fixed)
QuerySet.none() doesn't work well with subclasses of QuerySet
Reported by: | Andrey Popp | Owned by: | nobody |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | dev |
Severity: | Normal | Keywords: | |
Cc: | 8mayday@…, fizista@… | Triage Stage: | Accepted |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | yes | UI/UX: | no |
Description
I use QuerySet
subclassing to allow building complex composable queries out of smaller ones:
class MyQuerySet(QuerySet): def allowed(self, user): return self.filter(permissions__in=user.permissions) def have_feature(feature_name): return self.filter(feature__name=feature_name) ... objs = Obj.objects.allowed(user).have_feature("somefeature")
Sometimes I want to return EmptyQuerySet
by calling none()
to provide "fast-path" for some conditions, but unfortunately EmptyQuerySet
doesn't have my custom methods, so call chain breaks after returning it:
class MyQuerySet(QuerySet): def allowed(self, user): if user.is_anonymous: return self.none() return self.filter(permissions__in=user.permissions) def have_feature(feature_name): return self.filter(feature__name=feature_name) ... objs = Obj.objects.allowed(user).have_feature("some feature") # AttributeError: EmptyQuerySet doesn't have have_feature attribute ...
I've attached a patch which instruments EmptyQuerySet
subclass for each particular QuerySet
implementation and returns its instance as a result of none()
method call. I've also refactored code in Manager.none()
to reuse existing code in QuerySet.none()
and to parametrize Manager()
constructor over QuerySet
implementation to use.
Attachments (1)
Change History (14)
comment:1 by , 13 years ago
Type: | Bug → New feature |
---|
comment:2 by , 13 years ago
Cc: | added |
---|
by , 13 years ago
Attachment: | none_custom_qs.patch added |
---|
follow-up: 4 comment:3 by , 13 years ago
The same does my patch (17270), but the filter is added to the manager and not in queries.
follow-up: 5 comment:4 by , 13 years ago
Cc: | added |
---|
Replying to Wojciech Banaś <fizista@…>:
The same does my patch (17270), but the filter is added to the manager and not in queries.
I tested also a problem with the method of none () in my solution(17270), and this problem does NOT exist there.
So I think the filters should be located just in manager and not in the QuerySet.
follow-up: 6 comment:5 by , 13 years ago
Replying to Wojciech Banaś <fizista@…>:
I tested also a problem with the method of none () in my solution(17270), and this problem does NOT exist there.
So I think the filters should be located just in manager and not in the QuerySet.
I like to define methods on QuerySet
because it allows chain several filters at once:
users = User.objects.have_permission("some permission").is_staff().have_new_messages()
and so on...
follow-up: 7 comment:6 by , 13 years ago
Replying to andreypopp:
Replying to Wojciech Banaś <fizista@…>:
I tested also a problem with the method of none () in my solution(17270), and this problem does NOT exist there.
So I think the filters should be located just in manager and not in the QuerySet.
I like to define methods on
QuerySet
because it allows chain several filters at once:
users = User.objects.have_permission("some permission").is_staff().have_new_messages()and so on...
The same you can get by using my patch, and filters are where they should, in the manager.
See my examples in this ticket 17270
follow-up: 8 comment:7 by , 13 years ago
Replying to Wojciech Banaś <fizista@…>:
The same you can get by using my patch, and filters are where they should, in the manager.
See my examples in this ticket 17270
Yep, indeed, I actually wondered if I could provide all methods on Manager
and QuerySet
simultaneously and haven't found any solutions which look good. Your patch implements this correctly but I think it:
- suffers from performance degradation(very much code are executed during
get_query_set()
call which isn't rare call, I think) - doesn't work if
Manager
implements__getattr__
There's also a strange spot if QuerySet
already has some methods — you decided not to override such methods and I think it's good decision while it also makes some implicit assumptions which can confuse users and create new "special case" in documentation.
For me the better and the bigger spot to touch is to completely merge Manager
and QuerySet
classes into one unit or just expose only on them in API.
follow-up: 10 comment:8 by , 13 years ago
Replying to andreypopp:
Yep, indeed, I actually wondered if I could provide all methods on
Manager
andQuerySet
simultaneously and haven't found any solutions which look good. Your patch implements this correctly but I think it:
- suffers from performance degradation(very much code are executed during
get_query_set()
call which isn't rare call, I think)- doesn't work if
Manager
implements__getattr__
There's also a strange spot if
QuerySet
already has some methods — you decided not to override such methods and I think it's good decision while it also makes some implicit assumptions which can confuse users and create new "special case" in documentation.
For me the better and the bigger spot to touch is to completely merge
Manager
andQuerySet
classes into one unit or just expose only on them in API.
Your comments were very useful. Have combined your ideas with me, and I received a very short and working code (17270, see attachment manager.3.py).
Do you think that this solution would be good now?
follow-up: 11 comment:9 by , 13 years ago
Resolution: | → duplicate |
---|---|
Status: | new → closed |
comment:10 by , 13 years ago
Replying to Wojciech Banaś <fizista@…>:
Your comments were very useful. Have combined your ideas with me, and I received a very short and working code (17270, see attachment manager.3.py).
Do you think that this solution would be good now?
It still generates new class at runtime and performs method mangling which can be quite expensive in terms of performance. As I've already said, I suggest to merge Manager
and QuerySet
functionality into single class or eliminate one of them from public API, probably Manager
is the best candidate for this.
comment:11 by , 13 years ago
Resolution: | duplicate |
---|---|
Status: | closed → reopened |
Triage Stage: | Unreviewed → Accepted |
Replying to lukeplant:
Special casing the
none()
method doesn't seem very compelling compared to #17270 which addresses the larger problem, therefore closing this bug as a duplicate of #17270.
I don't agree with this resolution. I am not at all convinced that the technique in #17270 is a candidate for core (although it can be nice as a third-party module), as it adds a bunch of extra runtime code execution and implicit magical behavior to every single ORM call. And it's not really addressing the same problem at all: it's addressing the difference between a Manager
and a QuerySet
, not the difference between a QuerySet
and an EmptyQuerySet
. If you look at the patch there, it only fixes this problem too because it special-cases .none()
and EmptyQuerySet
every bit as much as a patch for this would. In other words, these are two separate issues, it just happens that #17270 conflates fixes for both of them into a single patch.
In any case, .none()
is already special-cased by the existence of EmptyQuerySet
- this bug is just looking for a remedy to the consequences of that existing special-case. I think it's a legitimate, and separate, bug.
comment:13 by , 12 years ago
Resolution: | → fixed |
---|---|
Status: | reopened → closed |
Patch for ticket #17271