Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django filter related field using related model's custom manager

How can I apply annotations and filters from a custom manager queryset when filtering via a related field? Here's some code to demonstrate what I mean.

Manager and models

from django.db.models import Value, BooleanField

class OtherModelManager(Manager):
    def get_queryset(self):
        return super(OtherModelManager, self).get_queryset().annotate(
            some_flag=Value(True, output_field=BooleanField())
        ).filter(
            disabled=False
        )

class MyModel(Model):
    other_model = ForeignKey(OtherModel)

class OtherModel(Model):
    disabled = BooleanField()

    objects = OtherModelManager()

Attempting to filter the related field using the manager

# This should only give me MyModel objects with related 
# OtherModel objects that have the some_flag annotation 
# set to True and disabled=False
my_model = MyModel.objects.filter(some_flag=True)

If you try the above code you will get the following error:

TypeError: Related Field got invalid lookup: some_flag

To further clarify, essentially the same question was reported as a bug with no response on how to actually achieve this: https://code.djangoproject.com/ticket/26393.

I'm aware that this can be achieved by simply using the filter and annotation from the manager directly in the MyModel filter, however the point is to keep this DRY and ensure this behaviour is repeated everywhere this model is accessed (unless explicitly instructed not to).

like image 520
Ben Avatar asked Jun 15 '17 03:06

Ben


2 Answers

How about running nested queries (or two queries, in case your backend is MySQL; performance).

The first to fetch the pk of the related OtherModel objects.

The second to filter the Model objects on the fetched pks.

other_model_pks = OtherModel.objects.filter(some_flag=...).values_list('pk', flat=True)
my_model = MyModel.objects.filter(other_model__in=other_model_pks)
# use (...__in=list(other_model_pks)) for MySQL to avoid a nested query.
like image 61
Moses Koledoye Avatar answered Sep 25 '22 09:09

Moses Koledoye


I don't think what you want is possible.

1) I think you are miss-understanding what annotations do.

Generating aggregates for each item in a QuerySet

The second way to generate summary values is to generate an independent summary for each object in a QuerySet. For example, if you are retrieving a list of books, you may want to know how many authors contributed to each book. Each Book has a many-to-many relationship with the Author; we want to summarize this relationship for each book in the QuerySet.

Per-object summaries can be generated using the annotate() clause. When an annotate() clause is specified, each object in the QuerySet will be annotated with the specified values.

The syntax for these annotations is identical to that used for the aggregate() clause. Each argument to annotate() describes an aggregate that is to be calculated.

So when you say:

MyModel.objects.annotate(other_model__some_flag=Value(True, output_field=BooleanField()))

You are not annotation some_flag over other_model.
i.e. you won't have: mymodel.other_model.some_flag

You are annotating other_model__some_flag over mymodel.
i.e. you will have: mymodel.other_model__some_flag

2) I'm not sure how familiar SQL is for you, but in order to preserve MyModel.objects.filter(other_model__some_flag=True) possible, i.e. to keep the annotation when doing JOINS, the ORM would have to do a JOIN over subquery, something like:

INNER JOIN 
(
    SELECT other_model.id, /* more fields,*/ 1 as some_flag
    FROM other_model
) as sub on mymodel.other_model_id = sub.id

which would be super slow and I'm not surprised they are not doing it.

Possible solution

don't annotate your field, but add it as a regular field in your model.

like image 42
Todor Avatar answered Sep 23 '22 09:09

Todor