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")
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á chamarmyfield.get_transform('mytransform')
, e em seguidamytransform.get_lookup('mylookup')
..filter(myfield__mytransform)
irá chamar primeiromyfield.get_lookup( 'mytransform')
, o qual irá falhar, por isso vai retornar e chamarmyfield.get_transform('mytransform')
e em seguida,mytransform.get_lookup( "exact")
.