Hur man skriver anpassade uppslagsord¶
Django erbjuder ett brett utbud av inbyggda uppslagsord för filtrering (till exempel exakt
och innehåller
). Denna dokumentation förklarar hur man skriver anpassade uppslagsord och hur man ändrar funktionen hos befintliga uppslagsord. För API-referenser för uppslagsord, se API-referens för uppslagning.
Ett exempel på uppslagning¶
Låt oss börja med en liten anpassad uppslagning. Vi kommer att skriva en anpassad lookup ne
som fungerar motsatt till exact
. Author.objects.filter(name__ne='Jack')
kommer att översättas till SQL:
"author"."name" <> 'Jack'
Denna SQL är oberoende av backend, så vi behöver inte oroa oss för olika databaser.
Det finns två steg för att få detta att fungera. Först måste vi implementera uppslagningen, sedan måste vi berätta för Django om den:
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
För att registrera NotEqual
-uppslaget måste vi anropa register_lookup
på den fältklass som vi vill att uppslaget ska vara tillgängligt för. I det här fallet är uppslaget meningsfullt för alla underklasser till Field
, så vi registrerar det med Field
direkt:
from django.db.models import Field
Field.register_lookup(NotEqual)
Registrering av uppslag kan också göras med hjälp av ett dekoratormönster:
from django.db.models import Field
@Field.register_lookup
class NotEqualLookup(Lookup): ...
Vi kan nu använda foo__ne
för alla fält foo
. Du måste se till att denna registrering sker innan du försöker skapa några frågeuppsättningar med hjälp av den. Du kan placera implementationen i en models.py
-fil, eller registrera uppslagningen i ready()
-metoden i en AppConfig
.
Om man tittar närmare på implementationen är det första obligatoriska attributet lookup_name
. Detta gör det möjligt för ORM att förstå hur man tolkar name__ne
och använda NotEqual
för att generera SQL. Enligt konvention är dessa namn alltid gemena strängar som bara innehåller bokstäver, men det enda hårda kravet är att det inte får innehålla strängen __
.
Vi måste sedan definiera metoden as_sql
. Detta tar ett SQLCompiler
-objekt, kallat compiler
, och den aktiva databasanslutningen. SQLCompiler
-objekt är inte dokumenterade, men det enda vi behöver veta om dem är att de har en compile() `` -metod som returnerar en tuple som innehåller en SQL-sträng och parametrarna som ska interpoleras i den strängen. I de flesta fall behöver du inte använda den direkt utan kan skicka den vidare till ``process_lhs()
och process_rhs()
.
En Lookup
arbetar mot två värden, lhs
och rhs
, som står för vänster sida och höger sida. Den vänstra sidan är vanligtvis en fältreferens, men det kan vara vad som helst som implementerar :ref:query expression API <query-expression>
. Den högra sidan är det värde som användaren anger. I exemplet Author.objects.filter(name__ne='Jack')
är vänstersidan en referens till fältet name
i modellen Author
och 'Jack'
är högersidan.
Vi kallar process_lhs
och process_rhs
för att konvertera dem till de värden vi behöver för SQL med hjälp av compiler
-objektet som beskrivs tidigare. Dessa metoder returnerar tuples som innehåller SQL och de parametrar som ska interpoleras till SQL, precis som vi behöver returnera från vår as_sql
-metod. I exemplet ovan returnerar process_lhs
('"author"."name"', [])
och process_rhs
returnerar ('"%s"', ['Jack'])
. I det här exemplet fanns det inga parametrar för den vänstra sidan, men det beror på vilket objekt vi har, så vi måste fortfarande inkludera dem i de parametrar vi returnerar.
Slutligen kombinerar vi delarna till ett SQL-uttryck med <>
och anger alla parametrar för frågan. Vi returnerar sedan en tupel som innehåller den genererade SQL-strängen och parametrarna.
Ett exempel på en transformator¶
Den anpassade lookupen ovan är bra, men i vissa fall kanske du vill kunna kedja ihop lookups. Låt oss till exempel anta att vi bygger en applikation där vi vill använda operatören abs()
. Vi har en Experiment
-modell som registrerar ett startvärde, slutvärde och förändringen (start - slut). Vi skulle vilja hitta alla experiment där förändringen var lika med ett visst belopp (Experiment.objects.filter(change__abs=27)
), eller där den inte översteg ett visst belopp (Experiment.objects.filter(change__abs__lt=27)
).
Observera
Detta exempel är något konstruerat, men det visar på ett bra sätt hur många funktioner som är möjliga på ett sätt som är oberoende av databasens backend, och utan att duplicera funktioner som redan finns i Django.
Vi börjar med att skriva en transformator för AbsoluteValue
. Detta kommer att använda SQL-funktionen ABS()
för att omvandla värdet före jämförelse:
from django.db.models import Transform
class AbsoluteValue(Transform):
lookup_name = "abs"
function = "ABS"
Låt oss sedan registrera den för IntegerField
:
from django.db.models import IntegerField
IntegerField.register_lookup(AbsoluteValue)
Vi kan nu köra de frågor vi hade tidigare. Experiment.objects.filter(change__abs=27)
kommer att generera följande SQL:
SELECT ... WHERE ABS("experiments"."change") = 27
Genom att använda Transform
istället för Lookup
innebär det att vi kan kedja ytterligare uppslagningar efteråt. Så Experiment.objects.filter(change__abs__lt=27)
kommer att generera följande SQL:
SELECT ... WHERE ABS("experiments"."change") < 27
Observera att om det inte finns någon annan uppslagning angiven, tolkar Django change__abs=27
som change__abs__exact=27
.
Detta gör också att resultatet kan användas i klausulerna ORDER BY
och DISTINCT ON
. Till exempel genererar Experiment.objects.order_by('change__abs')
:
SELECT ... ORDER BY ABS("experiments"."change") ASC
Och på databaser som stöder distinkta på fält (som PostgreSQL) genererar Experiment.objects.distinct('change__abs')
:
SELECT ... DISTINCT ON ABS("experiments"."change")
När Django letar efter vilka uppslagningar som är tillåtna efter att Transform
har tillämpats använder Django attributet output_field
. Vi behöver inte ange detta här eftersom det inte ändras, men om vi antar att vi tillämpar AbsoluteValue
på ett fält som representerar en mer komplex typ (till exempel en punkt i förhållande till ett ursprung eller ett komplext tal), kanske vi vill ange att transformationen returnerar en FloatField
-typ för ytterligare uppslagningar. Detta kan göras genom att lägga till ett output_field
-attribut till transform:
from django.db.models import FloatField, Transform
class AbsoluteValue(Transform):
lookup_name = "abs"
function = "ABS"
@property
def output_field(self):
return FloatField()
Detta säkerställer att ytterligare uppslagningar som abs__lte
beter sig som de skulle göra för en FloatField
.
Skriva en effektiv abs__lt
-uppslagning¶
När man använder den ovan skrivna abs
-uppslagningen kommer den SQL som produceras inte att använda index effektivt i vissa fall. I synnerhet när vi använder change__abs__lt=27
, är detta likvärdigt med change__gt=-27
AND change__lt=27
. (För lte
-fallet kan vi använda SQL BETWEEN
).
Så vi vill att Experiment.objects.filter(change__abs__lt=27)
ska generera följande SQL:
SELECT .. WHERE "experiments"."change" < 27 AND "experiments"."change" > -27
Genomförandet är:
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)
Det finns ett par anmärkningsvärda saker som händer. För det första anropar inte AbsoluteValueLessThan
process_lhs()
. Istället hoppar den över omvandlingen av lhs
som görs av AbsoluteValue
och använder den ursprungliga lhs
. Det vill säga, vi vill få fram "experiments"."change"
inte ABS("experiments"."change")
. Att referera direkt till self.lhs.lhs
är säkert eftersom AbsoluteValueLessThan
endast kan nås från AbsoluteValue
lookup, det vill säga lhs
är alltid en instans av AbsoluteValue
.
Observera också att eftersom båda sidorna används flera gånger i frågan måste params innehålla lhs_params
och rhs_params
flera gånger.
Den sista frågan gör inversionen (27
till -27
) direkt i databasen. Anledningen till att vi gör detta är att om self.rhs
är något annat än ett vanligt heltalsvärde (till exempel en F()
-referens) kan vi inte göra omvandlingarna i Python.
Observera
Faktum är att de flesta lookups med __abs
kan implementeras som intervallfrågor som detta, och på de flesta databasbackends är det troligtvis mer förnuftigt att göra det eftersom du kan använda indexen. Men med PostgreSQL kanske du vill lägga till ett index på ``abs (change) `` vilket gör att dessa frågor kan vara mycket effektiva.
Ett exempel på en bilateral transformator¶
Exemplet AbsoluteValue
som vi diskuterade tidigare är en transformation som gäller för den vänstra sidan av uppslagningen. Det kan finnas vissa fall där du vill att transformationen ska tillämpas på både vänster och höger sida. Till exempel om du vill filtrera en frågeuppsättning baserat på likheten mellan vänster och höger sida utan hänsyn till någon SQL-funktion.
Låt oss undersöka skiftlägesokänsliga transformationer här. Denna omvandling är inte särskilt användbar i praktiken eftersom Django redan har ett gäng inbyggda skiftlägesokänsliga uppslagningar, men det kommer att vara en trevlig demonstration av bilaterala omvandlingar på ett databasagnostiskt sätt.
Vi definierar en UpperCase
-transformator som använder SQL-funktionen UPPER()
för att omvandla värdena före jämförelse. Vi definierar bilateral = True
för att ange att denna omvandling ska gälla för både lhs
och rhs
:
from django.db.models import Transform
class UpperCase(Transform):
lookup_name = "upper"
function = "UPPER"
bilateral = True
Nu ska vi registrera den:
from django.db.models import CharField, TextField
CharField.register_lookup(UpperCase)
TextField.register_lookup(UpperCase)
Nu kommer frågeuppsättningen Author.objects.filter(name__upper="doe")
att generera en skiftlägeskänslig fråga så här:
SELECT ... WHERE UPPER("author"."name") = UPPER('doe')
Skriva alternativa implementationer för befintliga uppslagningar¶
Ibland kräver olika databasleverantörer olika SQL för samma operation. I det här exemplet kommer vi att skriva om en anpassad implementering för MySQL för operatorn NotEqual. Istället för <>
kommer vi att använda operatorn !=
. (Observera att i verkligheten stöder nästan alla databaser båda, inklusive alla officiella databaser som stöds av Django).
Vi kan ändra beteendet på en specifik backend genom att skapa en subklass av NotEqual
med en as_mysql
-metod:
class MySQLNotEqual(NotEqual):
def as_mysql(self, compiler, connection, **extra_context):
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)
Vi kan sedan registrera den med Field
. Den ersätter den ursprungliga klassen NotEqual
eftersom den har samma lookup_name
.
När en fråga sammanställs letar Django först efter as_%s % connection.vendor
-metoder och faller sedan tillbaka till as_sql
. Leverantörsnamnen för de inbyggda backends är qlite
, postgresql
, oracle
och mysql
.
Hur Django bestämmer vilka lookups och transformationer som ska användas¶
I vissa fall kanske du dynamiskt vill ändra vilken Transform
eller Lookup
som returneras baserat på det namn som skickas in, snarare än att fixa det. Som ett exempel kan du ha ett fält som lagrar koordinater eller en godtycklig dimension, och vill tillåta en syntax som .filter(coords__x7=4)
för att returnera de objekt där den 7:e koordinaten har värdet 4. För att göra detta skulle du åsidosätta get_lookup
med något liknande:
class CoordinatesField(Field):
def get_lookup(self, lookup_name):
if lookup_name.startswith("x"):
try:
dimension = int(lookup_name.removeprefix("x"))
except ValueError:
pass
else:
return get_coordinate_lookup(dimension)
return super().get_lookup(lookup_name)
Du skulle då definiera get_coordinate_lookup
på lämpligt sätt för att returnera en Lookup
subklass som hanterar det relevanta värdet av dimension
.
Det finns en metod med liknande namn som heter get_transform()
. get_lookup()
skall alltid returnera en Lookup
subklass, och get_transform()
en Transform
subklass. Det är viktigt att komma ihåg att Transform
-objekt kan filtreras ytterligare, medan Lookup
-objekt inte kan det.
Vid filtrering, om det bara finns ett uppslagsnamn kvar att lösa, letar vi efter en Lookup
. Om det finns flera namn letar vi efter en Transform
. Om det bara finns ett namn och ingen Lookup
hittas, letar vi efter en Transform
och sedan den exakta
lookupen på den Transform
. Alla anropssekvenser slutar alltid med en Lookup
. För att förtydliga:
.filter(myfield__mylookup)
kommer att anropamyfield.get_lookup('mylookup')
..filter(myfield__mytransform__mylookup)
kommer att anropamyfield.get_transform('mytransform')
och sedanmytransform.get_lookup('mylookup')
..filter(myfield__mytransform)
kommer först att anropamyfield.get_lookup('mytransform')
, vilket kommer att misslyckas, så det kommer att falla tillbaka till att anropamyfield.get_transform('mytransform')
och sedanmytransform.get_lookup('exact')
.