jatinderjit
jatinderjit

Reputation: 1399

Chain lookup through queryset

I have two models: City, and its alias CityAlias. The CityAlias model contains all the names in the City, plus the aliases. What I want is that whenever City is searched by name, the CityAlias model should be queried. This is what I've come up with:

class CityQuerySet(models.QuerySet):
    """ If City is searched by name, search it in CityAlias """
    def _search_name_in_alias(self, args, kwargs):
        for q in args:
            if not isinstance(q, models.Q): continue
            for i, child in enumerate(q.children):
                # q.children is a list of tuples of queries:
                # [('name__iexact', 'calcutta'), ('state__icontains', 'bengal')]
                if child[0].startswith('name'):
                    q.children[i] = ('aliases__%s' % child[0], child[1])

        for filter_name in kwargs:
            if filter_name.startswith('name'):
                kwargs['aliases__%s' % filter_name] = kwargs.pop(filter_name)

    def _filter_or_exclude(self, negate, *args, **kwargs):
        # handles 'get', 'filter' and 'exclude' methods
        self._search_name_in_alias(args=args, kwargs=kwargs)
        return super(CityQuerySet, self)._filter_or_exclude(negate, *args, **kwargs)


class City(models.Model):
    name = models.CharField(max_length=255, db_index=True)
    state = models.ForeignKey(State, related_name='cities')
    objects = CityQuerySet.as_manager()

class CityAlias(models.Model):
    name = models.CharField(max_length=255, db_index=True)
    city = models.ForeignKey(City, related_name='aliases')

Example: Kolkata will have an entry in City model, and it will have two entries in the CityAlias model: Kolkata and Calcutta. The above QuerySet allows to use lookups on the name field. So the following two queries will return the same entry:

City.objects.get(name='Kolkata')     # <City: Kolkata>
City.objects.get(name__iexact='calcutta')    # <City: Kolkata>

So far so good. But the problem arises when City is a ForeignKey in some other model:

class Trip(models.Model):
    destination = models.ForeignKey(City)
    # some other fields....

Trip.objects.filter(destination__name='Kolkata').count()   # some non-zero number
Trip.objects.filter(destination__name='Calcutta').count()  # always returns zero

Django internally handles these joins differently, and doesn't call the get_queryset method of City's manager. The alternative is to call the above query as following:

Trip.objects.filter(destination=City.objects.get(name='Calcutta'))

My question is that can I do something, so that however the City model is searched by name, it always searches in the CityAlias table instead? Or is there another better way to implement the functionality I require?

Upvotes: 9

Views: 542

Answers (3)

Xeberdee
Xeberdee

Reputation: 2057

Speaking of keeping it simple. Why not just give the City model a char field 'CityAlias' that contains the string? If I understand your question correctly, this is the most simple solution if you only need one alias per city. It just looks to me as though you are complicating a simple problem.

class City(models.Model):
    name = models.CharField(max_length=255, db_index=True)
    state = models.ForeignKey(State, related_name='cities')
    alias = models.CharField(max_length=255)

c = City.objects.get(alias='Kolkata')

>>>c.name
Calcutta
>>>c.alias
Kolkata

Upvotes: 1

Rafel Bennassar
Rafel Bennassar

Reputation: 348

I was trying to use Custom Lookups but apparently you cannot add a table to the join list. (Well, you could add an extra({"table": ...}) in the model's manager but it's not an elegant solution).

So I'd propose you:

1) Keep always your 'main/preferred' name city also as a CityAlias. So the metadata of the city will be in City... but all the naming information will be in CityAlias. (and maybe change the names)

In this way all look-ups will happen in that table. You could have a boolean to mark which instance is the original/preferred.

class City(models.Model):
    state = models.ForeignKey(State, related_name='cities')
    [...]

class CityAlias(models.Model):
    city = models.ForeignKey(City, related_name='aliases')
    name = models.CharField(max_length=255, db_index=True)

2) If you are thinking about translations... Have you thought about django-modeltranslation app?

In this case, it would create a field for each language and it would be always better than having a join.

3) Or, if you are using PostgreSQL, and you are thinking about "different translations for the same city-name" (and I'm thinking with transliterations from Greek or Russian language), maybe you could use PostgreSQL dictionaries, trigrams with similarities, etc. Or even in this case, the 1st approach.

Upvotes: 2

Tomas Walch
Tomas Walch

Reputation: 2305

I think it is better (and more pythonic) to be explicit in what you ask for throughout instead of trying to do magic in the Manager and thus:

City.objects.get(aliases__name__iexact='calcutta') # side note: this can return many (same in original) so you need to catch that

And:

Trip.objects.filter(destination__aliases__name='Calcutta').count()

Upvotes: 4

Related Questions