conditional-expressions.txt 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. =======================
  2. Conditional Expressions
  3. =======================
  4. .. currentmodule:: django.db.models.expressions
  5. Conditional expressions let you use :keyword:`if` ... :keyword:`elif` ...
  6. :keyword:`else` logic within filters, annotations, aggregations, and updates. A
  7. conditional expression evaluates a series of conditions for each row of a
  8. table and returns the matching result expression. Conditional expressions can
  9. also be combined and nested like other :doc:`expressions <expressions>`.
  10. The conditional expression classes
  11. ==================================
  12. We'll be using the following model in the subsequent examples::
  13. from django.db import models
  14. class Client(models.Model):
  15. REGULAR = "R"
  16. GOLD = "G"
  17. PLATINUM = "P"
  18. ACCOUNT_TYPE_CHOICES = {
  19. REGULAR: "Regular",
  20. GOLD: "Gold",
  21. PLATINUM: "Platinum",
  22. }
  23. name = models.CharField(max_length=50)
  24. registered_on = models.DateField()
  25. account_type = models.CharField(
  26. max_length=1,
  27. choices=ACCOUNT_TYPE_CHOICES,
  28. default=REGULAR,
  29. )
  30. ``When``
  31. --------
  32. .. class:: When(condition=None, then=None, **lookups)
  33. A ``When()`` object is used to encapsulate a condition and its result for use
  34. in the conditional expression. Using a ``When()`` object is similar to using
  35. the :meth:`~django.db.models.query.QuerySet.filter` method. The condition can
  36. be specified using :ref:`field lookups <field-lookups>`,
  37. :class:`~django.db.models.Q` objects, or :class:`~django.db.models.Expression`
  38. objects that have an ``output_field`` that is a
  39. :class:`~django.db.models.BooleanField`. The result is provided using the
  40. ``then`` keyword.
  41. Some examples:
  42. .. code-block:: pycon
  43. >>> from django.db.models import F, Q, When
  44. >>> # String arguments refer to fields; the following two examples are equivalent:
  45. >>> When(account_type=Client.GOLD, then="name")
  46. >>> When(account_type=Client.GOLD, then=F("name"))
  47. >>> # You can use field lookups in the condition
  48. >>> from datetime import date
  49. >>> When(
  50. ... registered_on__gt=date(2014, 1, 1),
  51. ... registered_on__lt=date(2015, 1, 1),
  52. ... then="account_type",
  53. ... )
  54. >>> # Complex conditions can be created using Q objects
  55. >>> When(Q(name__startswith="John") | Q(name__startswith="Paul"), then="name")
  56. >>> # Condition can be created using boolean expressions.
  57. >>> from django.db.models import Exists, OuterRef
  58. >>> non_unique_account_type = (
  59. ... Client.objects.filter(
  60. ... account_type=OuterRef("account_type"),
  61. ... )
  62. ... .exclude(pk=OuterRef("pk"))
  63. ... .values("pk")
  64. ... )
  65. >>> When(Exists(non_unique_account_type), then=Value("non unique"))
  66. >>> # Condition can be created using lookup expressions.
  67. >>> from django.db.models.lookups import GreaterThan, LessThan
  68. >>> When(
  69. ... GreaterThan(F("registered_on"), date(2014, 1, 1))
  70. ... & LessThan(F("registered_on"), date(2015, 1, 1)),
  71. ... then="account_type",
  72. ... )
  73. Keep in mind that each of these values can be an expression.
  74. .. note::
  75. Since the ``then`` keyword argument is reserved for the result of the
  76. ``When()``, there is a potential conflict if a
  77. :class:`~django.db.models.Model` has a field named ``then``. This can be
  78. resolved in two ways:
  79. .. code-block:: pycon
  80. >>> When(then__exact=0, then=1)
  81. >>> When(Q(then=0), then=1)
  82. ``Case``
  83. --------
  84. .. class:: Case(*cases, **extra)
  85. A ``Case()`` expression is like the :keyword:`if` ... :keyword:`elif` ...
  86. :keyword:`else` statement in ``Python``. Each ``condition`` in the provided
  87. ``When()`` objects is evaluated in order, until one evaluates to a
  88. truthful value. The ``result`` expression from the matching ``When()`` object
  89. is returned.
  90. An example:
  91. .. code-block:: pycon
  92. >>>
  93. >>> from datetime import date, timedelta
  94. >>> from django.db.models import Case, Value, When
  95. >>> Client.objects.create(
  96. ... name="Jane Doe",
  97. ... account_type=Client.REGULAR,
  98. ... registered_on=date.today() - timedelta(days=36),
  99. ... )
  100. >>> Client.objects.create(
  101. ... name="James Smith",
  102. ... account_type=Client.GOLD,
  103. ... registered_on=date.today() - timedelta(days=5),
  104. ... )
  105. >>> Client.objects.create(
  106. ... name="Jack Black",
  107. ... account_type=Client.PLATINUM,
  108. ... registered_on=date.today() - timedelta(days=10 * 365),
  109. ... )
  110. >>> # Get the discount for each Client based on the account type
  111. >>> Client.objects.annotate(
  112. ... discount=Case(
  113. ... When(account_type=Client.GOLD, then=Value("5%")),
  114. ... When(account_type=Client.PLATINUM, then=Value("10%")),
  115. ... default=Value("0%"),
  116. ... ),
  117. ... ).values_list("name", "discount")
  118. <QuerySet [('Jane Doe', '0%'), ('James Smith', '5%'), ('Jack Black', '10%')]>
  119. ``Case()`` accepts any number of ``When()`` objects as individual arguments.
  120. Other options are provided using keyword arguments. If none of the conditions
  121. evaluate to ``TRUE``, then the expression given with the ``default`` keyword
  122. argument is returned. If a ``default`` argument isn't provided, ``None`` is
  123. used.
  124. If we wanted to change our previous query to get the discount based on how long
  125. the ``Client`` has been with us, we could do so using lookups:
  126. .. code-block:: pycon
  127. >>> a_month_ago = date.today() - timedelta(days=30)
  128. >>> a_year_ago = date.today() - timedelta(days=365)
  129. >>> # Get the discount for each Client based on the registration date
  130. >>> Client.objects.annotate(
  131. ... discount=Case(
  132. ... When(registered_on__lte=a_year_ago, then=Value("10%")),
  133. ... When(registered_on__lte=a_month_ago, then=Value("5%")),
  134. ... default=Value("0%"),
  135. ... )
  136. ... ).values_list("name", "discount")
  137. <QuerySet [('Jane Doe', '5%'), ('James Smith', '0%'), ('Jack Black', '10%')]>
  138. .. note::
  139. Remember that the conditions are evaluated in order, so in the above
  140. example we get the correct result even though the second condition matches
  141. both Jane Doe and Jack Black. This works just like an :keyword:`if` ...
  142. :keyword:`elif` ... :keyword:`else` statement in ``Python``.
  143. ``Case()`` also works in a ``filter()`` clause. For example, to find gold
  144. clients that registered more than a month ago and platinum clients that
  145. registered more than a year ago:
  146. .. code-block:: pycon
  147. >>> a_month_ago = date.today() - timedelta(days=30)
  148. >>> a_year_ago = date.today() - timedelta(days=365)
  149. >>> Client.objects.filter(
  150. ... registered_on__lte=Case(
  151. ... When(account_type=Client.GOLD, then=a_month_ago),
  152. ... When(account_type=Client.PLATINUM, then=a_year_ago),
  153. ... ),
  154. ... ).values_list("name", "account_type")
  155. <QuerySet [('Jack Black', 'P')]>
  156. Advanced queries
  157. ================
  158. Conditional expressions can be used in annotations, aggregations, filters,
  159. lookups, and updates. They can also be combined and nested with other
  160. expressions. This allows you to make powerful conditional queries.
  161. Conditional update
  162. ------------------
  163. Let's say we want to change the ``account_type`` for our clients to match
  164. their registration dates. We can do this using a conditional expression and the
  165. :meth:`~django.db.models.query.QuerySet.update` method:
  166. .. code-block:: pycon
  167. >>> a_month_ago = date.today() - timedelta(days=30)
  168. >>> a_year_ago = date.today() - timedelta(days=365)
  169. >>> # Update the account_type for each Client from the registration date
  170. >>> Client.objects.update(
  171. ... account_type=Case(
  172. ... When(registered_on__lte=a_year_ago, then=Value(Client.PLATINUM)),
  173. ... When(registered_on__lte=a_month_ago, then=Value(Client.GOLD)),
  174. ... default=Value(Client.REGULAR),
  175. ... ),
  176. ... )
  177. >>> Client.objects.values_list("name", "account_type")
  178. <QuerySet [('Jane Doe', 'G'), ('James Smith', 'R'), ('Jack Black', 'P')]>
  179. .. _conditional-aggregation:
  180. Conditional aggregation
  181. -----------------------
  182. What if we want to find out how many clients there are for each
  183. ``account_type``? We can use the ``filter`` argument of :ref:`aggregate
  184. functions <aggregation-functions>` to achieve this:
  185. .. code-block:: pycon
  186. >>> # Create some more Clients first so we can have something to count
  187. >>> Client.objects.create(
  188. ... name="Jean Grey", account_type=Client.REGULAR, registered_on=date.today()
  189. ... )
  190. >>> Client.objects.create(
  191. ... name="James Bond", account_type=Client.PLATINUM, registered_on=date.today()
  192. ... )
  193. >>> Client.objects.create(
  194. ... name="Jane Porter", account_type=Client.PLATINUM, registered_on=date.today()
  195. ... )
  196. >>> # Get counts for each value of account_type
  197. >>> from django.db.models import Count
  198. >>> Client.objects.aggregate(
  199. ... regular=Count("pk", filter=Q(account_type=Client.REGULAR)),
  200. ... gold=Count("pk", filter=Q(account_type=Client.GOLD)),
  201. ... platinum=Count("pk", filter=Q(account_type=Client.PLATINUM)),
  202. ... )
  203. {'regular': 2, 'gold': 1, 'platinum': 3}
  204. This aggregate produces a query with the SQL 2003 ``FILTER WHERE`` syntax
  205. on databases that support it:
  206. .. code-block:: sql
  207. SELECT count('id') FILTER (WHERE account_type=1) as regular,
  208. count('id') FILTER (WHERE account_type=2) as gold,
  209. count('id') FILTER (WHERE account_type=3) as platinum
  210. FROM clients;
  211. On other databases, this is emulated using a ``CASE`` statement:
  212. .. code-block:: sql
  213. SELECT count(CASE WHEN account_type=1 THEN id ELSE null) as regular,
  214. count(CASE WHEN account_type=2 THEN id ELSE null) as gold,
  215. count(CASE WHEN account_type=3 THEN id ELSE null) as platinum
  216. FROM clients;
  217. The two SQL statements are functionally equivalent but the more explicit
  218. ``FILTER`` may perform better.
  219. Conditional filter
  220. ------------------
  221. When a conditional expression returns a boolean value, it is possible to use it
  222. directly in filters. This means that it will not be added to the ``SELECT``
  223. columns, but you can still use it to filter results:
  224. .. code-block:: pycon
  225. >>> non_unique_account_type = (
  226. ... Client.objects.filter(
  227. ... account_type=OuterRef("account_type"),
  228. ... )
  229. ... .exclude(pk=OuterRef("pk"))
  230. ... .values("pk")
  231. ... )
  232. >>> Client.objects.filter(~Exists(non_unique_account_type))
  233. In SQL terms, that evaluates to:
  234. .. code-block:: sql
  235. SELECT ...
  236. FROM client c0
  237. WHERE NOT EXISTS (
  238. SELECT c1.id
  239. FROM client c1
  240. WHERE c1.account_type = c0.account_type AND NOT c1.id = c0.id
  241. )