Django ORM w akcji – wyrażenie F()

Czujesz się ograniczony tworząc zapytania do bazy danych przez Django ORM? Tęsknisz czasami za standardowym zapytaniem SQL? Zobacz, jakie funkcje okołobazodanowe oferuje Django. Zacznijmy od wyrażenia F(). 

Poznaj wyrażenie F()

Django Queryset API udostępnia wyrażenie F(), które umożliwia odwołanie się do wartości konkretnego pola modelu. 

Zacznijmy od przykładu i stwórzmy model pracownika – Employee.

from django.db import models


class Employee(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    salary = models.DecimalField(max_digits=9, decimal_places=2)
    bonus = models.DecimalField(max_digits=9, decimal_places=2, default=0)
    employment_date = models.DateField()
    position = models.CharField(max_length=50) 

Zwiększmy zarobki pracownika o 100zł. 

from employees.models import Employee

# Znalezienie wszystkich pracowników 
employees = Employee.objects.all()

for employee in employees:
    # Zwiększenie wypłaty danego pracownika
    employee.salary += 100
    # Zapis zmian danego pracownika 
    employee.save() 

Hmm… ale co jeśli mielibyśmy kilkuset pracowników (rekordów danej tabeli w bazie)?

Zmienianie wartości pola salary każdemu pracownikowi w pętli nie wydaje się optymalnym rozwiązaniem.

💡 Pamiętaj, że każde wywołanie save() wykonuje pojedyncze żądanie do bazy danych o zapis zmian.

Czy da się uprościć kod do jednego odwołania do bazy danych?
Tak, z wyrażeniem F().
from django.db.models import F
from employees.models import Employee

# Wypłata każdego pracownika zostanie powiększona o 100zł.
Employee.objects.update(salary = F("salary") + 100) 
Jedna linia kodu i jedno zapytanie do bazy danych, zdecydowanie prostszy i bardziej zoptymalizowany kod 👍. Wyrażenie F("salary") odwołuje się do pola salary obecnie przetwarzanego rekordu. Dzięki temu jesteśmy w stanie dostać się do konkretnej wartości z pola danego rekordu (salary) i następnie zmodyfikować ją według własnych potrzeb i przypisać do pola (salary).

Jak używać wyrażenia F()?

Wyrażenia F(), tak jak inne wyrażenia zapytań (query expressions), możesz użyć w funkcjach: update(), create(), filter(), order_by(), annotate() czy aggregate().

Zgodnie z dokumentacją Django pozwala na wykonywanie szeregu operacji arytmetycznych takich jak dodawanie, odejmowanie,  mnożenie, dzielenie, moduł czy potęgowanie, używając w tym celu stałych, zmiennych czy też innych wyrażeń.

Zobaczmy to w akcji.

Przykład 1 - Liczby

Korzystając z modelu Employee zwiększmy cenę o 15%.

Employee.objects.update(salary = F("salary") * 1.15) 

Równie dobrze możesz użyć zmiennej:

increase_value = 1.15
Employee.objects.update(salary=F("salary") * increase_value) 
Bez problemu wykonasz dodatkowe operacje arytmetyczne z użyciem innych pól z bazy. Przy pomocy funkcji annotate zwrócimy dla każdego pracownika pole earnings, które będzie odpowiadać sumie pól salary i bonus.
Employee.objects.annotate(earnings=F("salary") + F("bonus")) 

Sprawdźmy, jak zadziała takie użycie wyrażenia F().

employees = Employee.objects.annotate(earnings=F("salary") + F("bonus"))
employee = employees[0]
print(employee.salary) # Decimal('5000.00')
print(employee.bonus) # Decimal('100.00')
print(employee.earnings) # Decimal('5100') 

Przykład 2 - Teksty

Spróbujmy tak samo postąpić z tekstem. Model Employee posiada pole position, które zawiera informacje o jego stanowisku.

Hmm… a gdyby tak każde stanowisko miało nazwę: Senior <nazwa_stanowiska>.

Spróbujmy.

employees = Employee.objects.annotate(position_extra="Senior " + F("position")).all()

print(employees[0].position_extra) # 0 

z pewnością nie jest dobrym wynik działania funkcji. W takim razie czy jesteśmy w stanie wykonać nasze zadanie?

Tak, Django oferuje wiele funkcji bazodanowych, przedstawię Ci kilka z nich, po więcej odsyłam Cię do dokumentacji 🧐.

Concat() – przyjmuje co najmniej dwa ciągi znaków i zwraca złączony tekst.

Upper() – przyjmuje tekst i zwraca go wielkimi literami.

Coalesce() – przyjmuje listę co najmniej dwóch nazw pól/wyrażeń tego samego typu i zwraca pierwszą niezerową wartość.

Mając tą wiedzę wykorzystajmy funkcję Concat.
from django.db.models import F, Value
from django.db.models.functions import Concat
from employees.models import Employee

employees = Employee.objects.annotate(position_extra=Concat(Value("Senior "), F("position"))).all()

print(employees[0].position) # Developer
print(employees[0].position_extra) # Senior Developer 

Oprócz funkcji Concat() pojawiło się kolejne wyrażenie (query expression) – Value().

Value() reprezentuje prostą wartość: integer, bool lub string. W zapytaniach powyżej używaliśmy m.in. F("salary") + 100. Django domyślnie opakuje 100 w Value(100), dlatego możemy to pominąć w zapisie. Jednak musimy pamiętać o użyciu Value przy stringach.

Sprawdźmy inną funkcję bazodanową. A gdyby tak nazwisko przedstawić wielkimi literami?

employees = Employee.objects.annotate(last_name_upper=Upper(F("last_name"))).all()

print(employees[0].last_name) # Kowalski
print(employees[0].last_name_upper) # KOWALSKI 

Concat() z różnymi typami CharField i Text Field

Jeśli chcesz połączyć ze sobą teksty znajdujące się w polach innego typu (CharField i TextField), pamiętaj o dodaniu dodatkowego parametru output_field do funkcji Concat().

from django.db.models import CharField, Value
from django.db.models.functions import Concat

# Załóżmy, że first_name jest CharField, a last_name TextField

employees = Employee.objects.annotate(full_name=Concat(F('first_name'), Value(' '), F('last_name'), output_field=CharField())).all()

print(employees[0].first_name) # Jan
print(employees[0].last_name) # Kowalski
print(employees[0].full_name) # Jan Kowalski 

Podsumowując: wyrażenie F(nazwa_pola) odwołuje się do pola nazwa_pola obecnie przetwarzanego rekordu.

Używanie wyrażenia F() pozwala zwiększyć wydajność zapytań poprzez stosowanie operacji bazodanowych zamiast Pythona, co powoduje redukowanie liczby żądań do bazy danych.

BONUS - zrefactoruj kod z nami 👷

Przed Tobą kod, który ma duży potencjał do refactoringu. Zastanów się, jak jesteś w stanie uprościć działanie tego kodu. Nie zaglądaj od razu do odpowiedzi 😉.

Przykład jest inspirowany kodem z projektu legacy. 

Poniższy kod przedstawia modele – Employee i Event. Za Event będzie odpowiedzialna jedna osoba – creator.

# models.py
from django.db import models

class Employee(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.TextField()

    def get_short_fullname(self):
        return self.first_name + " " + self.last_name[0] + "."


class Event(models.Model):
    creator = models.ForeignKey(Employee, on_delete=models.CASCADE)
    title = models.CharField(max_length=50)
    active = models.BooleanField(default=False)
 

Poniżej znajduje się zdecydowanie mało optymalny kod, mający na celu zwrócenie listy wszystkich twórców wydarzeń w odpowiednim formacie. Zastanów się, ile zapytań do bazy danych zostanie wywołanych przez ten kod i jak można go uprościć (Rozwiązanie poniżej).

events = Event.objects.all()

creators_list = []

for event in events:
    try:
        if event.creator:
            creators_list.append(event.creator.get_short_fullname())
    except Employee.DoesNotExist:
        continue

creators_list = list(set(creators_list))

print(creators_list) # ['Adam M.', 'Jan K.']
 

Jak myślisz, ile zapytań wywoła się w powyższym przykładzie?

1 (pobranie wszystkich rekordów modelu Event) + liczba eventów (Pobranie creatora dla każdego rekordu Event).

Co jeśli mamy 10000 rekordów modelu Event? Zostanie wykonanych 10001 zapytań do bazy danych!!! Nawet, jeśli ostatecznie okaże się, że potrzebowaliśmy wyciągnąć informacje o 2 creatorach!

Propozycja rozwiązania:

from django.db.models import F, Value
from django.db.models.functions import Concat, Substr


employees = Employee.objects.filter(events__active=False).annotate(
    full_name=Concat(F('first_name'), Value(' '), Substr(F('last_name'), 1, 1), Value('.'))).distinct().values_list("full_name", flat=True)

print(list(employees)) # ['Adam M.', 'Jan K.'] 

Wykorzystując funkcje takie jak F(), Concat(), Value(), Substr() zdecydowanie uprościliśmy kod, a dodatkowo odwołujemy się do bazy danych tylko raz!

Masz inny pomysł? Znałeś/używałeś wcześniej wyrażenie F()? Podziel się z nami w komentarzu. Każda Twoja reakcja to dodatkowa motywacja do działania, dziękujemy 🙏.

5 1 vote
Article Rating
guest
2 komentarzy
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Maciej
Maciej
8 miesięcy temu

Cześć,

chciałbym nieco się odnieść do Twojego wpisu. 🙂 Myślę że zacznę od przykładu bonus. Rzeczywiście kod początkowo wykonuje dużą liczbę zapytań. Miej proszę świadomość, że odpowiedzialność za optymalne wczytanie danych do aplikacji ciąży na Tobie.

Refaktor, który zaproponowałaś moim zdaniem wyszedł strasznie: jednolinijkowy potwór w stylu python-way. Ciężko się to czyta, nie wiadomo o co chodzi. Chciałbym tutaj wystąpić w obronie pierwszego rozwiązania. Sytuację bardzo poprawia metoda select_related. Leci jedno query i buduje od razu całą strukturę danych wykorzystując joina. Dla tego konkretnego przykładu: tworzenie skróconej sygnatury osoby. Jak duża musiałaby być skala danych, aby przenosić logikę z aplikacji na bazę? Tysiąc, milion rekordów? Tutaj kolejna kwestia.

Dobrze, że postrzegasz swoje rozwiązania przez pryzmat potencjału obsługi dużych zbiorów danych. Popatrzmy proszę na przykład pierwszy. Czy zwiększenie wartości pola salary dla całej tabeli to operacja do wykonania w ciągu cyklu request-response? Niewątpliwie jedno query jest wydajniejsze od zapisywania każdego obiektu osobno, jednak nic nam to nie da, kiedy mamy 20 milionów rekordów. Tutaj wypadałoby przemyśleć czy operacja nie powinna zostać wykonana asynchronicznie.

Podsumowując: rozgraniczmy proszę trzy istotne elementy.

  • Świadomą pracę z ORM’em i wiedzę jakie query wywołuje: w razie potrzeby można skorzystać z normalnego SQL’a.
  • Czysty kod pisany obiektowo nawet kosztem kilku milisekund – po to mamy ORM żeby na warstwie biznesowej patrzeć przez pryzmat obiektów jako realnych bytów.
  • Potencjał do asynchronicznego przetwarzania dużych zbiorów danych: tam mamy wolną rękę.