Pesquisas personalizadas

Django oferece uma grande variedade de: ref:lookups embutidas <field-lookups> para filtragem (por exemplo, exact e icontains). Esta documentação explica como escrever lookups personalizadas e como alterar o funcionamento de lookups existentes. Para as referências da API de lookups, consulte o: doc: /ref/models/lookups.

Um simples exemple de pesquisa

Vamos iniciar com uma simples pesquisa personalizada. Escreveremos uma simples pesquisa ne que funciona em frente ao exato. ``Author.objects.filter(name__ne=’Jack’) traduzindo para SQL.

"author"."name" <> 'Jack'

Este SQL é independente, por isso, não precisa se preocupar com os bancos de dados diferentes.

Há duas etapas para fazer este trabalho. Em primeiro lugar precisamos implementar a filtro, e depois precisamos dizer ao Django sobre ele. A implementação é bastante simples

from django.db.models import Lookup

class NotEqual(Lookup):
    lookup_name = 'ne'

    def as_sql(self, compiler, connection):
        lhs, lhs_params = self.process_lhs(compiler, connection)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        params = lhs_params + rhs_params
        return '%s <> %s' % (lhs, rhs), params

Para registrar o filtro NotEqual precisamos somente chamar register_lookup no campo da classe que queremos que o filtro esteja disponível. Neste caso, a lookup faz sentido em todos os Field da sub-classe, então registramos isso com `` Field`` diretamente

from django.db.models.fields import Field
Field.register_lookup(NotEqual)

O registro do filtro também pode ser feito usando um padrão de “decorator”:

from django.db.models.fields import Field

@Field.register_lookup
class NotEqualLookup(Lookup):
    # ...

Agora podemos usar `` foo__ne`` para qualquer campo `` foo``. Tenha certeza de registrar antes de tentar criar qualquer “querysets” que o use. É possível colocar a implementação no `` models.py``, ou registar a lookup no método ready() do `` AppConfig``.

Olhando mais perto da implementação, o primeiro atributo obrigatório é `` lookup_name``. Isso permite que o ORM entenda como interpretar `` name__ne`` e use `` NotEqual`` para gerar o SQL. Por convenção, estes nomes são sempre em strings caixa baixa contendo apenas letras, mas a única exigência é que não contenha a string `` __``.

Quando então é necessário definir o método as_sql. Este recebe um objeto SQL Compiler, chamado compiler, e a conexão de banco de dados ativa. Os objetos SQLCompiler não são documentados, mas a única coisa que precisamos saber sobre eles é que eles tem um método compile() o qual retorna uma tupla contendo uma string SQL, e os parâmetros a serem interpolados naquela string. Na maioria dos casos você não precisa usá-lo diretamente e pode ser passá-lo para o process_lhs() e process_rhs().

A Lookup trabalha em dois valores, e lhs e rhs, entendendo como left-hand side e right-hand side. O lado esquerdo é geralmente uma referência de campo, mas pode ser qualquer coisa que implemente query expression API. O lado direito é o valor dado pelo usuário. No exemplo Author.objects.filter (name__ne = 'Jack'), o lado esquerdo é uma referência para o campo name campo do modelo Author, e 'Jack ' é o lado direito.

Nós chamamos process_lhs e process_rhs para convertê-los para os valores que precisamos para SQL usando o objeto compiler descrito antes. Esses métodos retornam tuplas contendo alguns comandos SQL e os parâmetros a serem interpolados dentro destes comandos SQL, assim como nós precisamos para retornar do nosso método as_sql. No exemplo acima, process_lhs retorna ('"author"."name"', []) e process_rhs retorna ('"%s"', [ 'Jack' ]). Neste exemplo não havia parâmetros para o lado esquerdo, mas isso depende do objeto que temos, portanto ainda precisamos incluí-los nos parâmetros que retornamos.

FInalmente combinamos as partes em uma expressão SQL com <>, e fornecemos todos os parâmetros para a consulta. Então retornamos uma tupla contendo a string SQL gerada e os parâmetros.

Um exemplo simples transformador

O lookup personalizado acima é boa, mas em alguns casos você queira ser capaz de lookups encadeadas. Por exemplo, vamos supor que estamos construindo uma aplicação onde queremos fazer uso do operador abs(). Temos um modelo Experiment que registra um valor inicial, o valor final e a mudança (início - fim). Gostaríamos de encontrar todos os experimentos em que a alteração foi igual a uma certa quantidade ( Experiment.objects.filter (change__abs = 27)), ou onde não exceda um certo montante ( Experiment.objects.filter (change__abs__lt = 27)).

Nota

Este exemplo é um pouco artificial, mas demonstra muito bem a gama de funcionalidades que é possível de uma maneira independente em um backend de banco de dados., e sem duplicar funcionalidades já existentes no Django.

Nos vamos começar escrevendo um transformador AbsoluteValue. Isso vai usar a função SQL ABS() para transformar o valor antes da comparação

from django.db.models import Transform

class AbsoluteValue(Transform):
    lookup_name = 'abs'
    function = 'ABS'

Em seguida, vamos registrá-lo para `` IntegerField``

from django.db.models import IntegerField
IntegerField.register_lookup(AbsoluteValue)

agora podemos executar as queries que tínhamos antes. ``Experiment.objects.filter (change__abs = 27) `` irá gerar o seguinte SQL

SELECT ... WHERE ABS("experiments"."change") = 27

Ao usar o Transform ao invés de Lookup significa que somos capazes de encadear lookups umas às outras. Então Experiment.objects.filter (change__abs__lt = 27) irá gerar o seguinte SQL

SELECT ... WHERE ABS("experiments"."change") < 27

Note que no caso de não haver nenhuma outra lookup especificada, o Django interpreta change__abs = 27 como change__abs__exact = 27.

Isso também permite que os resultados sejam utilizados nas cláusulas ORDER BY e DISTINCT ON. Por exemplo Experiment.objects.order_by('change__abs') gerando:

SELECT ... ORDER BY ABS("experiments"."change") ASC

E em bancos de dados que suportam campos distintos (tais como PostgreSQL). Experiment.objects.distinct('change__abs') gerando:

SELECT ... DISTINCT ON ABS("experiments"."change")
Changed in Django 2.1:

Ordenação e suporte distinto foram adicionados tal como descrito nos dois últimos parágrafos.

Ao procurar quais lookups são permitidas depois de aplicar o Transform, o Django usa o atributo output_field. Não precisamos especificar isso aqui, já que não mudou, mas supondo que estivéssemos aplicando AbsoluteValue para algum campo que represente um tipo mais complexo (por exemplo um ponto relativo a uma origem ou um número complexo) então talvez querer especificar que o transform retorne um tipo FloatField para os próximos lookups. Isso pode ser feito adicionando um atributo output_field ao “transform”:

from django.db.models import FloatField, Transform

class AbsoluteValue(Transform):
    lookup_name = 'abs'
    function = 'ABS'

    @property
    def output_field(self):
        return FloatField()

Isso garante que novos filtros como o abs__lte se comportem como deveriam para um FloatField.

Escrevendo um filtro abs__lt eficiente

Quando usar o filtro abs escrito acima, em alguns casos o SQL gerado não irá usar os índices de maneira eficiente. Em particular, quando usa change__abs__lt=27, isso é equivalente a change__gt=-27 e change__lt=27. (Para o caso lte podemos usar o BETWEEN do SQL).

Então nós gostaríamos que o Experiment.objects.filter (change__abs__lt=27) gerasse o seguinte SQL:

SELECT .. WHERE "experiments"."change" < 27 AND "experiments"."change" > -27

A implementação é:

from django.db.models import Lookup

class AbsoluteValueLessThan(Lookup):
    lookup_name = 'lt'

    def as_sql(self, compiler, connection):
        lhs, lhs_params = compiler.compile(self.lhs.lhs)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        params = lhs_params + rhs_params + lhs_params + rhs_params
        return '%s < %s AND %s > -%s' % (lhs, rhs, lhs, rhs), params

AbsoluteValue.register_lookup(AbsoluteValueLessThan)

Há algumas coisas notáveis ​​acontecendo. Em primeiro lugar, AbsoluteValueLessThan não está chamando process_lhs (). Em vez disso ele ignora a transformação do lhs feito por AbsoluteValue e usa o lhs original. Ou seja, queremos pegar `` “experiments”.”change” `` não ABS( "experiments". "change") ``. Referindo-se diretamente ao ``self.lhs.lhs é tão seguro quanto o AbsoluteValueLessThan pode ser acessado diretamente do filtro AbsoluteValue, que é lhs é sempre uma instância de AbsoluteValue.

Note também que como ambos os lados são usados ​​várias vezes na consulta os parâmetros precisam conter lhs_params e rhs_params várias vezes.

A consulta final faz a inversão ( 27 para `` -27``) diretamente no banco de dados. A razão para isso é que, se o `` self.rhs`` é algo mais do que um valor inteiro simples (por exemplo, uma referência F() ) não podemos fazer as transformações em Python.

Nota

Na verdade, a maioria das filtros com __abs poderiam ser implementados como consultas de intervalos como esta, e na maioria dos “backends” de banco de dados, é provável que seja mais sensato fazê-lo já que podería fazer uso dos índices. No entanto, com PostgreSQL você pode querer adicionar um índice na abs(change) o qual permitiria que essas consultas fossem muito eficientes.

Um exemplo bilateral de “transforme”

O exemplo AbsoluteValue discutido anteriormente é uma transformação que se aplica para o lado esquerdo da lookup. Pode haver alguns casos onde você quer que a transformação seja aplicada tanto para o lado esquerdo quanto para o lado direito. Por exemplo, se você quiser filtrar um queryset com base em uma igualdade de lado esquerdo e direito inperseptível para alguma função SQL.

Vamos examinar o exemplo simples de transformação “case-insensitive” aqui. Essa transformação não é muito útil na prática, já que o Django já vem com um monte de pesquisas de maiúsculas e minúsculas embutidas, mas será uma boa demonstração de transformações bilaterais de uma forma agnóstica ao banco de dados.

Definimos um “transformer” de UpperCase que usa a função UPPER() do SQL para transformar os valores antes da comparação. Definimos :attr: bilateral = True <django.db.models.Transform.bilateral> para indicar que essa transformação deve ser aplicada a ambos lhs e rhs:

from django.db.models import Transform

class UpperCase(Transform):
    lookup_name = 'upper'
    function = 'UPPER'
    bilateral = True

Em seguida, registre

from django.db.models import CharField, TextField
CharField.register_lookup(UpperCase)
TextField.register_lookup(UpperCase)

Agora, o queryset Author.objects.filter(name__upper = "doe") irá gerar uma query case-insensitive como esta:

SELECT ... WHERE UPPER("author"."name") = UPPER('doe')

Escrever implementações alternativas para filtros existentes

Às vezes, diferentes fornecedores de banco de dados requerem diferentes SQL para a mesma operação. Para este exemplo, vamos reescrever uma implementação personalizada para o do operador NotEqual para o MySQL. Em vez de <> usaremos o operador !=. (Note que, na realidade, quase todos os bancos de dados suportam ambos, incluindo todas as bases de dados oficiais apoiadas pelo Django).

Nós podemos mudar o comportamento em um backend específico criado pela subclasse de NotEqual com um método as_mysql:

class MySQLNotEqual(NotEqual):
    def as_mysql(self, compiler, connection):
        lhs, lhs_params = self.process_lhs(compiler, connection)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        params = lhs_params + rhs_params
        return '%s != %s' % (lhs, rhs), params

Field.register_lookup(MySQLNotEqual)

Podemos então registrá-lo com Field. Ele toma o lugar da classe NotEqual original já que tem o mesmo lookup_name.

Ao compilar uma consulta, o Django primeiro procura pelos métodos as_%s % connection.vendor, e depois para as_sql. Os nomes de fornecedores para os back-ends suportados são sqlite, postgresql, oracle e mysql.

Como o Django determina quais filtros e “transforms” são usados

Em alguns casos, talvez queira mudar dinamicamente qual Transform ou filtro é devolvido com base no nome passado, em vez de corrigí-lo. Como exemplo, você pode ter um campo o qual armazena coordenadas ou uma dimensão arbitrária, e deseja permitir uma sintaxe como .filter (coords__x7=4) para retornar os objetos onde a 7ª coordenada tem valor 4. Para fazer isso, substitua get_lookup com algo como:

class CoordinatesField(Field):
    def get_lookup(self, lookup_name):
        if lookup_name.startswith('x'):
            try:
                dimension = int(lookup_name[1:])
            except ValueError:
                pass
            else:
                return get_coordinate_lookup(dimension)
        return super().get_lookup(lookup_name)

Você deve então definir `` get_coordinate_lookup`` adequadamente para retornar uma subclasse de Lookup a qual lida com o valor relevante da dimension.

Existe um método de nome semelhante chamado get_transform() ``. `` Get_lookup () `` deve sempre retornar uma subclasse de ``Lookup, e get_transform() uma subclasse de Transform. É importante lembrar que objetos do tipo Transform podem ainda ser filtrados, e objetos do tipo Lookup não.

Ao filtrar, se houver apenas um nome de filtro remanescente a ser resolvido, olhamos para um Lookup. Se houver vários nomes, ele irá procurar por um Transform. Em uma situação onde há apenas um nome e um Lookup não é encontrado, procuramos por um Transform e então o filtro exact naquele Transform. Todas as sequências de chamadas sempre terminam com um Lookup. Para esclarecer:

  • .filter(myfield__mylookup) irá chamar myfield.get_lookup(‘mylookup’)`.
  • .filter(myfield__mytransform__mylookup) irá chamar myfield.get_transform('mytransform'), e em seguida mytransform.get_lookup('mylookup').
  • .filter(myfield__mytransform) irá chamar primeiro myfield.get_lookup( 'mytransform'), o qual irá falhar, por isso vai retornar e chamar myfield.get_transform('mytransform') e em seguida, mytransform.get_lookup( "exact").
Back to Top