Filtrar por propriedade

95

É possível filtrar um queryset Django por propriedade de modelo?

eu tenho um método em meu modelo:

@property
def myproperty(self):
    [..]

e agora quero filtrar por esta propriedade como:

MyModel.objects.filter(myproperty=[..])

isso é de alguma forma possível?

schneck
fonte
Está em SQLAlchemy: docs.sqlalchemy.org/en/latest/orm/extensions/hybrid.html e você pode conectar django com SQLAlchemy via pypi.python.org/pypi/aldjemy, mas tenho dúvidas de que os dois possam ser conectados do jeito que você quer que eles sejam.
rattray

Respostas:

78

Não. Os filtros Django operam no nível do banco de dados, gerando SQL. Para filtrar com base nas propriedades do Python, você precisa carregar o objeto no Python para avaliar a propriedade - e nesse ponto, você já fez todo o trabalho para carregá-lo.

Glenn Maynard
fonte
4
azar que esse recurso não seja implementado, seria uma extensão interessante para pelo menos filtrar objetos correspondentes após o conjunto de resultados ter sido construído.
Schneck
1
como lidar com isso no admin? Existe alguma solução alternativa?
andilabs
39

Posso não ter entendido sua pergunta original, mas há um filtro embutido no python.

filtered = filter(myproperty, MyModel.objects)

Mas é melhor usar uma compreensão de lista :

filtered = [x for x in MyModel.objects if x.myproperty()]

ou melhor ainda, uma expressão geradora :

filtered = (x for x in MyModel.objects if x.myproperty())
Clint
fonte
14
Isso funciona para filtrar uma vez que você tenha um objeto Python, mas ele está perguntando sobre Django QuerySet.filter, que constrói consultas SQL.
Glenn Maynard
1
certo, mas como explicado acima, gostaria de adicionar a propriedade ao meu filtro de banco de dados. filtrar depois que a consulta foi feita é exatamente o que eu quero evitar.
Schneck
18

Revelando a solução alternativa sugerida por @ TheGrimmScientist, você pode fazer essas "propriedades sql" definindo-as no gerenciador ou no QuerySet e reutilizando / encadeando / compondo-as:

Com um gerente:

class CompanyManager(models.Manager):
    def with_chairs_needed(self):
        return self.annotate(chairs_needed=F('num_employees') - F('num_chairs'))

class Company(models.Model):
    # ...
    objects = CompanyManager()

Company.objects.with_chairs_needed().filter(chairs_needed__lt=4)

Com um QuerySet:

class CompanyQuerySet(models.QuerySet):
    def many_employees(self, n=50):
        return self.filter(num_employees__gte=n)

    def needs_fewer_chairs_than(self, n=5):
        return self.with_chairs_needed().filter(chairs_needed__lt=n)

    def with_chairs_needed(self):
        return self.annotate(chairs_needed=F('num_employees') - F('num_chairs'))

class Company(models.Model):
    # ...
    objects = CompanyQuerySet.as_manager()

Company.objects.needs_fewer_chairs_than(4).many_employees()

Veja https://docs.djangoproject.com/en/1.9/topics/db/managers/ para mais. Observe que estou retirando a documentação e não testei os itens acima.

ratoeira
fonte
14

Parece que usar F () com anotações será minha solução para isso.

Não vai filtrar @property, já que Ffala com o banco de dados antes que os objetos sejam trazidos para o python. Mas ainda estou colocando aqui como uma resposta, já que minha razão para querer filtrar por propriedade era realmente querer filtrar objetos pelo resultado de aritmética simples em dois campos diferentes.

então, algo como:

companies = Company.objects\
    .annotate(chairs_needed=F('num_employees') - F('num_chairs'))\
    .filter(chairs_needed__lt=4)

em vez de definir a propriedade como:

@property
def chairs_needed(self):
    return self.num_employees - self.num_chairs

em seguida, fazer uma lista de compreensão de todos os objetos.

TheGrimmScientist
fonte
4

Eu tive o mesmo problema e desenvolvi esta solução simples:

objects_id = [x.id for x in MyModel.objects.all() if x.myProperty == [...]]
MyModel.objects.filter(id__in=objects_id)

Eu sei que não é a solução mais performática, mas pode ajudar em casos simples como o meu

Revitalizar
fonte
3

POR FAVOR, alguém me corrija, mas acho que encontrei uma solução, pelo menos para o meu caso.

Eu quero trabalhar em todos aqueles elementos cujas propriedades são exatamente iguais a ... qualquer coisa.

Mas eu tenho vários modelos, e essa rotina deve funcionar para todos os modelos. E faz:

def selectByProperties(modelType, specify):
    clause = "SELECT * from %s" % modelType._meta.db_table

    if len(specify) > 0:
        clause += " WHERE "
        for field, eqvalue in specify.items():
            clause += "%s = '%s' AND " % (field, eqvalue)
        clause = clause [:-5]  # remove last AND

    print clause
    return modelType.objects.raw(clause)

Com esta sub-rotina universal, posso selecionar todos os elementos que são exatamente iguais ao meu dicionário de combinações de 'especificar' (nome da propriedade, valor da propriedade).

O primeiro parâmetro leva um (models.Model),

o segundo, um dicionário como: {"propriedade1": "77", "propriedade2": "12"}

E cria uma instrução SQL como

SELECT * from appname_modelname WHERE property1 = '77' AND property2 = '12'

e retorna um QuerySet sobre esses elementos.

Esta é uma função de teste:

from myApp.models import myModel

def testSelectByProperties ():

    specify = {"property1" : "77" , "property2" : "12"}
    subset = selectByProperties(myModel, specify)

    nameField = "property0"
    ## checking if that is what I expected:
    for i in subset:
        print i.__dict__[nameField], 
        for j in specify.keys():
             print i.__dict__[j], 
        print 

E? O que você acha?

Akrueger
fonte
Geralmente, parece um trabalho decente. Eu não diria que é ideal, mas é melhor do que ter que bifurcar um repositório para modificar o modelo dos pacotes que você instalou do PyPI sempre que precisar de algo assim.
hlongmore
E agora que tive tempo de brincar um pouco com isso: a desvantagem real dessa abordagem é que os querysets retornados por .raw () não são querysets completos, com isso quero dizer que há métodos queryset ausentes:AttributeError: 'RawQuerySet' object has no attribute 'values'
hlongmore,
1

Eu sei que é uma pergunta antiga, mas para o bem de quem pula aqui, acho que é útil ler a pergunta abaixo e a resposta relativa:

Como personalizar o filtro administrativo no Django 1.4

FSp
fonte
1
Para aqueles que estão esquecendo esta resposta - este link é para informações sobre a implementação de Filtros de lista no Django Admin usando "SimpleListFilter". Útil, mas não é uma resposta à pergunta, exceto em um caso muito específico.
jenniwren
0

Também pode ser possível usar anotações queryset que duplicam a propriedade get / set-logic, como sugerido, por exemplo, por @rattray e @thegrimmscientist , em conjunto com o property. Isso poderia produzir algo que funciona tanto no nível Python e no nível de banco de dados.

Não tenho certeza sobre as desvantagens, entretanto: veja esta pergunta do SO para um exemplo.

djvg
fonte
O link da sua pergunta de revisão de código avisa que foi voluntariamente removido pelo autor. Você se importaria de atualizar sua resposta aqui, com um link para o código ou com uma explicação, ou apenas remover sua resposta?
hlongmore,
@hlongmore: Desculpe por isso. Essa pergunta foi movida para o SO. Consertei o link acima.
djvg