Search code examples
pythondjango

Can I define an Alias to a foreign field in Django?


I'm looking to define an alias to a foreign key related set so that it can then be used in a generic filtering function. To explain with a little more detail here is a simplified example of my use case demonstrating what I'm trying to achieve:

models.py

from django.db import models

class Family(models.Model):
    family_name = models.CharField(max_length=100)

    pets: models.Manager['Pet'] = ...
    members: models.Manager['Person'] = ...

    def __str__(self):
        return f'{self.id} - {self.family_name}'

class Pet(models.Model):
    class PetType(models.TextChoices):
        CAT = 'cat'
        DOG = 'dog'
        LIZARD = 'lizard'

    name = models.CharField(max_length=100)
    type = models.CharField(max_length=100, choices=PetType)
    family = models.ForeignKey(Family, on_delete=models.CASCADE, related_name='pets')

    def __str__(self):
        return f'{self.id} - {self.name} {self.family.family_name} [{self.type}]'

class Person(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField()
    family = models.ForeignKey(Family, on_delete=models.CASCADE, related_name='members')

    @property
    def pets(self) -> models.Manager[Pet]:
        return self.family.pets

    def __str__(self):
        return f'{self.id} - {self.name} {self.family.family_name}'

If I have a Person entity, I can use it's pets property to get the pets from the family, what I'm looking for is a way to add an alias/annotation/whatever to a queryset of Person so that I can define:

def has_a_cat(query_set):
    return query_set.filter(pets__type='cat')

and then be able to use that for both Family and Person query sets. I know I can filter a Person queryset by the pet type because Person.objects.filter(family__pets__type='cat') works perfectly fine, but I'd like a way to alias family__pets to pets so I can use the shared filter.

I've tried using .annotate(pets=F('family__pets')) and .alias(pets=F('family__pets)) but then when filtering on pets__type I get the following error:

django.core.exceptions.FieldError: Unsupported lookup 'type' for BigAutoField or join on the field not permitted.

Solution

  • The solution that worked was provided by willem-van-ossem in the comments under my question.

    To solve this we can annotate the query set with a FilteredRelation which allows applying the queries I was looking to apply. Further a custom Manager implementation like so:

    class PersonManager(models.Manager):
        def get_queryset(self):
            return super().get_queryset().annotate(pets=FilteredRelation('family__pets'))
    
    class Person(models.Model):
        name = models.CharField(max_length=100)
        age = models.IntegerField()
        family = models.ForeignKey(Family, on_delete=models.CASCADE, related_name='members')
    
        objects = PersonManager()
    
        @property
        def pets(self) -> models.Manager[Pet]:
            return self.family.pets
    
        def __str__(self):
            return f'{self.id} - {self.name} {self.family.family_name}'
    

    With the above in place we can filter based on pets on both a Family and Person models and querysets (see question for remaining model classes).