Pesquisas personalizadas

Django oferece uma grande variedade de: ref: pesquisas embutidos <field-lookups> `Para filtragem (por exemplo, e exact` 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 “lookup”, então 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 a lookup NotEqual precisamos somente chamar register_lookup no campo da classe que queremos que a lookup 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 da “lookup” também pode ser feito usando um pattern decorador

from django.db.models.fields import Field

@Field.register_lookup
class NotEqualLookup(Lookup):
    # ...
Changed in Django 1.8:

foi adicionado a capacidade de usar o pattern decorator.

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 `` __``.

We then need to define the as_sql method. This takes a SQLCompiler object, called compiler, and the active database connection. SQLCompiler objects are not documented, but the only thing we need to know about them is that they have a compile() method which returns a tuple containing an SQL string, and the parameters to be interpolated into that string. In most cases, you don’t need to use it directly and can pass it on to process_lhs() and 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 :ref: query expression API <query-expression>. 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 e 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 SQL e os parâmetros a serem interpolados dentro deste SQL, assim como nós precisamos para retornar do nosso método as_sql. No exemplo acima, process_lhs retorna ( "autor". "Nome" ', []) `` 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.

Finally we combine the parts into an SQL expression with <>, and supply all the parameters for the query. We then return a tuple containing the generated SQL string and the parameters.

Um exemplo simples transformador

A 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.

Vamos começar por escrever um tradutor AbsoluteValue. Isto irá 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 `` Transform`` ao invés de Lookup isso significa que somos capazes de encadear lookups umas as outras. Então Experiment.objects.filter (change__abs__lt = 27) irá gerar o seguinte SQL

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

Note-se que no caso não há nenhuma outra lookup especificada, o Django interpreta change__abs = 27 como change__abs__exact = 27.

Quando olhamos par quais lookups são permitidas depois de ter aplicado o Trnasform, Django usa o atibuto òutput_field`. Não é necessário especificar isso aqui, já que não muda, más supondo que fosse aplicado AbsoluteValue``para algum campo que represente um tipo mais complexo (por exemplo um ponto reativo a uma origem ou um número complexo) então talvez quereríamos especificar  que retornasse um tipo ``FloatField para próximas lookups. Isso pode ser feito adicionando um atributo `òutput_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 novas pesquisas como abs__lte se comportem como fariam para um FloatField.

Escrevendo uma lookup abs__lt eficiente

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

Então nós gostaríamos Experiment.objects.filter (change__abs__lt=27) para gerar 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 a ``self.lhs.lhs é tão seguro quanto AbsoluteValueLessThan pode ser acessado diretamente da lookup AbsoluteValue, que é a 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() ) que não podemos fazer as transformações em Python.

Nota

Na verdade, a maioria das lookups com __abs poderias ser implementadas 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 em na abs(change) o que permitiria que essas consultas serem muito eficientes.

Um exemplo bilateral transformador

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, como Django já vem com um monte de pesquisas de maiúsculas e minúsculas embutidos, mas será uma boa demonstração de transformações bilaterais de uma forma agnóstica ao banco de dados.

Definimos um transformador UpperCase que usa a função SQL UPPER() para transformar os valores antes de comparação. Definimos :attr: bilateral = True <django.db.models.Transform.bilateral> para indicar que essa transformação deverá aplicar-se a 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 pesquisas 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 MySQL para o operador NotEqual. 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 apoiados por Django).

Nós podemos mudar o comportamento em um backend específico, criando uma 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, Django primeiro procura pelos métodos as_%s % connection.vendor, e depois para as_sql. Os nomes de fornecedores para os back-ends suportador são sqlite, postgresql, oracle e mysql.

Como o Django determina as lookups e transformações que são usados

Em alguns casos, talvez queira mudar dinamicamente qual Transform ou Lookup é devolvido com base no nome passado, em vez de corrigi-lo. Como exemplo, é possível ter um campo que armazena coordenadas ou uma dimensão arbitrária, e deseja permitir que 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(CoordinatesField, self).get_lookup(lookup_name)

Defina então o `` get_coordinate_lookup`` adequadamente para retornar uma subclasse Lookup que 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 podem.

Ao filtrar, se houver apenas um nome lookup 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 lookup 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