django query based on dynamic property()

2019-02-16 11:50发布

问题:

I was wondering if there was a way to use Django's filter() on query sets using a dynamically generated python property using property(). I have first_name and last_name of every user, and I want to filter based on their concatenated name first_name last_name. (The reason behind this is that when I do autocomplete I search to see if the query matches first name, last name, or part of the concatenation. I want John S to match John Smith, for example.

I created a property of name:

def _get_name(self):
    return self.first_name + " " + self.last_name
    name = property(_get_name)

This way I can call user.name to get the concatenated name.

However, if I try to do User.objects.filter(name__istartswith=query) I get the error Cannot resolve keyword 'name' into field.

Any ideas on how to do this? Do I have to create another field in the database to store the full name?

回答1:

filter() operates on the database level (it actually writes SQL), so it won't be possible to use it for any queries based on your python code (dynamic property in your question).

This is an answer put together from many other answers in this department : )



回答2:

The accepted answer is not entirely true.

For many cases, you can override get() in the model manager to pop dynamic properties from the keyword arguments, then add the actual attributes you want to query against into the kwargs keyword arguments dictionary. Be sure to return a super so any regular get() calls return the expected result.

I'm only pasting my own solution, but for the __startswith and other conditional queries you could add some logic to split the double-underscore and handle appropriately.

Here was my work-around to allow querying by a dynamic property:

class BorrowerManager(models.Manager):
    def get(self, *args, **kwargs):
        full_name = kwargs.pop('full_name', None)
        # Override #1) Query by dynamic property 'full_name'
        if full_name:
            names = full_name_to_dict(full_name)
            kwargs = dict(kwargs.items() + names.items())
        return super(BorrowerManager, self).get(*args, **kwargs)

In models.py:

class Borrower(models.Model):
    objects = BorrowerManager()

    first_name = models.CharField(null=False, max_length=30)
    middle_name = models.CharField(null=True, max_length=30)
    last_name = models.CharField(null=False, max_length=30)
    created = models.DateField(auto_now_add=True)

In utils.py (for the sake of context):

def full_name_to_dict(full_name):
    ret = dict()
    values = full_name.split(' ')
    if len(values) == 1:
        raise NotImplementedError("Not enough names to unpack from full_name")
    elif len(values) == 2:
        ret['first_name'] = values[0]
        ret['middle_name'] = None
        ret['last_name'] = values[1]
        return ret
    elif len(values) >= 3:
        ret['first_name'] = values[0]
        ret['middle_name'] = values[1:len(values)-1]
        ret['last_name'] = values[len(values)-1]
        return ret
    raise NotImplementedError("Error unpacking full_name to first, middle, last names")


回答3:

I had a similar problem and was looking for solution. Taking for granted that a search engine would be the best option (e.g. django-haystack with Elasticsearch), that's how I would implement some code for your needs using only the Django ORM (you can replace icontains with istartswith):

from django.db.models import Value
from django.db.models.functions import Concat

queryset = User.objects.annotate(full_name=Concat('first_name', Value(' '), 'last_name')
return queryset.filter(full_name__icontains=value)

In my case I didn't know whether the user would insert 'first_name last_name' or viceversa, so I used the following code.

from django.db.models import Q, Value
from django.db.models.functions import Concat

queryset = User.objects.annotate(first_last=Concat('first_name', Value(' '), 'last_name'), last_first=Concat('last_name', Value(' '), 'first_name'))
return queryset.filter(Q(first_last__icontains=value) | Q(last_first__icontains=value))

With Django <1.8, you would probably need to resort to extra with the SQL CONCAT function, something like the following:

queryset.extra(where=['UPPER(CONCAT("auth_user"."last_name", \' \', "auth_user"."first_name")) LIKE UPPER(%s) OR UPPER(CONCAT("auth_user"."first_name", \' \', "auth_user"."last_name")) LIKE UPPER(%s)'], params=['%'+value+'%', '%'+value+'%'])


回答4:

Think it's not possible in django to filter on properties that does not present as a database filed, but what you can do to make cool autocomplete search is something like this:

if ' ' in query:
    query = query.split()
    search_results = list(chain(User.objects.filter(first_name__icontains=query[0],last_name__icontains=query[1]),
                                    User.objects.filter(first_name__icontains=query[1],last_name__icontains=query[0])))
else:
    search_results =  User.objects.filter(Q(first_name__icontains=query)| Q(last_name__icontains=query))

This code gives the user of your system a flexibility to start typing either first name or last name and the user will be thankful to you for allowing this.