Villkorliga uttryck¶
Med villkorliga uttryck kan du använda logiken if
… elif
… else
i filter, annoteringar, aggregeringar och uppdateringar. Ett villkorligt uttryck utvärderar en serie villkor för varje rad i en tabell och returnerar det matchande resultatuttrycket. Villkorliga uttryck kan också kombineras och nästlas på samma sätt som andra uttryck.
Klasserna för villkorliga uttryck¶
Vi kommer att använda följande modell i de följande exemplen:
from django.db import models
class Client(models.Model):
REGULAR = "R"
GOLD = "G"
PLATINUM = "P"
ACCOUNT_TYPE_CHOICES = {
REGULAR: "Regular",
GOLD: "Gold",
PLATINUM: "Platinum",
}
name = models.CharField(max_length=50)
registered_on = models.DateField()
account_type = models.CharField(
max_length=1,
choices=ACCOUNT_TYPE_CHOICES,
default=REGULAR,
)
När
¶
Ett When()
-objekt används för att kapsla in ett villkor och dess resultat för användning i det villkorliga uttrycket. Att använda ett When()
-objekt liknar att använda metoden filter()
. Villkoret kan anges med :ref:field lookups <field-lookups>
, Q
-objekt eller Expression
-objekt som har ett output_field
som är ett BooleanField
. Resultatet tillhandahålls med hjälp av nyckelordet then
.
Några exempel:
>>> from django.db.models import F, Q, When
>>> # String arguments refer to fields; the following two examples are equivalent:
>>> When(account_type=Client.GOLD, then="name")
>>> When(account_type=Client.GOLD, then=F("name"))
>>> # You can use field lookups in the condition
>>> from datetime import date
>>> When(
... registered_on__gt=date(2014, 1, 1),
... registered_on__lt=date(2015, 1, 1),
... then="account_type",
... )
>>> # Complex conditions can be created using Q objects
>>> When(Q(name__startswith="John") | Q(name__startswith="Paul"), then="name")
>>> # Condition can be created using boolean expressions.
>>> from django.db.models import Exists, OuterRef
>>> non_unique_account_type = (
... Client.objects.filter(
... account_type=OuterRef("account_type"),
... )
... .exclude(pk=OuterRef("pk"))
... .values("pk")
... )
>>> When(Exists(non_unique_account_type), then=Value("non unique"))
>>> # Condition can be created using lookup expressions.
>>> from django.db.models.lookups import GreaterThan, LessThan
>>> When(
... GreaterThan(F("registered_on"), date(2014, 1, 1))
... & LessThan(F("registered_on"), date(2015, 1, 1)),
... then="account_type",
... )
Tänk på att vart och ett av dessa värden kan vara ett uttryck.
Observera
Eftersom nyckelordsargumentet then
är reserverat för resultatet av When()
, finns det en potentiell konflikt om en Model
har ett fält med namnet then
. Detta kan lösas på två sätt:
>>> When(then__exact=0, then=1)
>>> When(Q(then=0), then=1)
Fall
¶
Ett Case()
-uttryck är som if
… elif
… else
-satsen i Python
. Varje villkor
i de medföljande When()
-objekten utvärderas i ordning, tills ett utvärderas till ett sanningsenligt värde. result
-uttrycket från det matchande When()
-objektet returneras.
Ett exempel:
>>>
>>> from datetime import date, timedelta
>>> from django.db.models import Case, Value, When
>>> Client.objects.create(
... name="Jane Doe",
... account_type=Client.REGULAR,
... registered_on=date.today() - timedelta(days=36),
... )
>>> Client.objects.create(
... name="James Smith",
... account_type=Client.GOLD,
... registered_on=date.today() - timedelta(days=5),
... )
>>> Client.objects.create(
... name="Jack Black",
... account_type=Client.PLATINUM,
... registered_on=date.today() - timedelta(days=10 * 365),
... )
>>> # Get the discount for each Client based on the account type
>>> Client.objects.annotate(
... discount=Case(
... When(account_type=Client.GOLD, then=Value("5%")),
... When(account_type=Client.PLATINUM, then=Value("10%")),
... default=Value("0%"),
... ),
... ).values_list("name", "discount")
<QuerySet [('Jane Doe', '0%'), ('James Smith', '5%'), ('Jack Black', '10%')]>
Case()
accepterar ett valfritt antal When()
objekt som individuella argument. Andra alternativ ges med hjälp av nyckelordsargument. Om inget av villkoren utvärderas till TRUE
, returneras det uttryck som anges med default
nyckelordsargumentet. Om ett default
argument inte anges, används None
.
Om vi ville ändra vår tidigare fråga för att få rabatten baserat på hur länge Client
har varit hos oss, kan vi göra det med hjälp av uppslagningar:
>>> a_month_ago = date.today() - timedelta(days=30)
>>> a_year_ago = date.today() - timedelta(days=365)
>>> # Get the discount for each Client based on the registration date
>>> Client.objects.annotate(
... discount=Case(
... When(registered_on__lte=a_year_ago, then=Value("10%")),
... When(registered_on__lte=a_month_ago, then=Value("5%")),
... default=Value("0%"),
... )
... ).values_list("name", "discount")
<QuerySet [('Jane Doe', '5%'), ('James Smith', '0%'), ('Jack Black', '10%')]>
Observera
Kom ihåg att villkoren utvärderas i ordning, så i exemplet ovan får vi rätt resultat även om det andra villkoret matchar både Jane Doe och Jack Black. Det här fungerar precis som en if
… elif
… else
-sats i Python
.
Case()
fungerar också i en filter()
-sats. Till exempel:, för att hitta guldkunder som registrerade sig för mer än en månad sedan och platinakunder som registrerade sig för mer än ett år sedan:
>>> a_month_ago = date.today() - timedelta(days=30)
>>> a_year_ago = date.today() - timedelta(days=365)
>>> Client.objects.filter(
... registered_on__lte=Case(
... When(account_type=Client.GOLD, then=a_month_ago),
... When(account_type=Client.PLATINUM, then=a_year_ago),
... ),
... ).values_list("name", "account_type")
<QuerySet [('Jack Black', 'P')]>
Avancerade frågor¶
Villkorliga uttryck kan användas i annoteringar, aggregeringar, filter, uppslagningar och uppdateringar. De kan också kombineras och nästlas med andra uttryck. Detta gör att du kan skapa kraftfulla villkorliga frågor.
Villkorlig uppdatering¶
Låt oss säga att vi vill ändra account_type
för våra kunder för att matcha deras registreringsdatum. Vi kan göra detta med hjälp av ett villkorligt uttryck och metoden update()
:
>>> a_month_ago = date.today() - timedelta(days=30)
>>> a_year_ago = date.today() - timedelta(days=365)
>>> # Update the account_type for each Client from the registration date
>>> Client.objects.update(
... account_type=Case(
... When(registered_on__lte=a_year_ago, then=Value(Client.PLATINUM)),
... When(registered_on__lte=a_month_ago, then=Value(Client.GOLD)),
... default=Value(Client.REGULAR),
... ),
... )
>>> Client.objects.values_list("name", "account_type")
<QuerySet [('Jane Doe', 'G'), ('James Smith', 'R'), ('Jack Black', 'P')]>
Villkorlig aggregering¶
Vad händer om vi vill ta reda på hur många kunder det finns för varje account_type
? Vi kan använda argumentet filter
i :ref:aggregate functions <aggregation-functions>
för att uppnå detta:
>>> # Create some more Clients first so we can have something to count
>>> Client.objects.create(
... name="Jean Grey", account_type=Client.REGULAR, registered_on=date.today()
... )
>>> Client.objects.create(
... name="James Bond", account_type=Client.PLATINUM, registered_on=date.today()
... )
>>> Client.objects.create(
... name="Jane Porter", account_type=Client.PLATINUM, registered_on=date.today()
... )
>>> # Get counts for each value of account_type
>>> from django.db.models import Count
>>> Client.objects.aggregate(
... regular=Count("pk", filter=Q(account_type=Client.REGULAR)),
... gold=Count("pk", filter=Q(account_type=Client.GOLD)),
... platinum=Count("pk", filter=Q(account_type=Client.PLATINUM)),
... )
{'regular': 2, 'gold': 1, 'platinum': 3}
Detta aggregat producerar en fråga med SQL 2003-syntaxen FILTER WHERE
i databaser som stöder den:
SELECT count('id') FILTER (WHERE account_type=1) as regular,
count('id') FILTER (WHERE account_type=2) as gold,
count('id') FILTER (WHERE account_type=3) as platinum
FROM clients;
I andra databaser emuleras detta med hjälp av en CASE
-sats:
SELECT count(CASE WHEN account_type=1 THEN id ELSE null) as regular,
count(CASE WHEN account_type=2 THEN id ELSE null) as gold,
count(CASE WHEN account_type=3 THEN id ELSE null) as platinum
FROM clients;
De två SQL-satserna är funktionellt likvärdiga, men den mer explicita FILTER
kan ge bättre resultat.
Villkorligt filter¶
När ett villkorligt uttryck returnerar ett booleskt värde är det möjligt att använda det direkt i filter. Det innebär att det inte läggs till i SELECT
-kolumnerna, men du kan ändå använda det för att filtrera resultat:
>>> non_unique_account_type = (
... Client.objects.filter(
... account_type=OuterRef("account_type"),
... )
... .exclude(pk=OuterRef("pk"))
... .values("pk")
... )
>>> Client.objects.filter(~Exists(non_unique_account_type))
I SQL-termer utvärderas detta till:
SELECT ...
FROM client c0
WHERE NOT EXISTS (
SELECT c1.id
FROM client c1
WHERE c1.account_type = c0.account_type AND NOT c1.id = c0.id
)