Witaj w kolejnym wpisie z serii: "Zrozumieć Django".
Poprzednio opisałem podstawowe informacje o modelach Zrozumieć Django: wprowadzenie do modeli oraz Zrozumieć Django: Relacje pomiędzy modelami z przykładem relacji jeden do jednego.
W tym wpisie na warsztat wchodzi relacja jeden do wielu. Podam przykłady zastosowań po czym przejdę do ćwiczeń. Pokażę:
- niezbędny kod
- przykłady z panelu administracyjnego
- przykłady prostych zapytań
- przykłady zapytań z wykorzystaniem filtrowania
- przykłady zapytań z wykorzystaniem relacji wstecz (i wykorzystaniem atrybutu
related_name
) - przykłady zaawansowanych zapytań wykorzystujących relacje wstecz
Przykłady zastosowań relacji jeden do wielu
Relacja jeden-do-wielu (OneToMany) jest używana, gdy jeden rekord jednego modelu jest powiązany z wieloma rekordami innego modelu. Na przykład modele Department i Employee mogą mieć relację jedno-do-wielu, ponieważ jeden dział może mieć wielu pracowników, a każdy pracownik jest przypisany do dokładnie jednego działu. Tę relację można właściwie znaleźć w każdej firmie.

Innymi przykładami relacji jeden do wielu mogą być:
- uczniowie danej klasy w szkole — klasa ma wielu uczniów, ale uczeń jest przypisany do jednej klasy
- szkoła ma wiele klas, ale klasa należy do jednej szkoły
- miasto ma wielu mieszkańców, ale mieszkaniec jest zameldowany tylko w jednym mieście
- jedna osoba może posiadać wiele telefonów, ale telefon ma jednego właściciela
- jedna marka może produkować wiele różnych modeli produktów, ale produkt ma tylko jedną markę
- jedno przedsiębiorstwo może mieć wiele oddziałów w różnych miejscach, ale oddział należy do jednego przedsiębiorstwa
- i o wiele więcej
Tworzenie relacji jeden do wielu.
Relację jeden do wielu tworzy się za pomocą pola ForeignKey
w modelu. Deklaracja wymaga dwóch atrybutów:
- modelu źródłowego. Jeśli nie został jeszcze zadeklarowany, to jego nazwę należy zapisać w cudzysłowie
- atrybutu
on_delete
, którego działanie opisałem w poprzednim wpisie
owner = models.ForeignKey("User", on_delete=models.CASCADE)
Relacja jeden do wielu na przykładzie pracowników (Employee) i działów (Department).
from django.db import models
# Create your models here.
class Employee(models.Model):
first_name = models.CharField(max_length=255)
# ponieważ Department jeszcze nie jest zadeklarowany, jego nazwa jest w cudzysłowiu
department = models.ForeignKey("Department", on_delete=models.DO_NOTHING)
# dla uproszczenia pomijam pozostałe pola.
def __str__(self):
return f"{self.first_name}"
class Department(models.Model):
name = models.CharField(max_length=255)
# dla uproszczenia pomijam pozostałe pola.
def __str__(self):
return f"{self.name}"
Standardowo, po przygotowaniu modelu należy przygotować i wykonać migracje
./manage.py makemigrations
./manage.py migrate
i dodać modele do panelu administracyjnego
from django.contrib import admin
from .models import Employee, Department
admin.site.register(Employee)
admin.site.register(Department)
Dodam kilka działów, IT, Księgowość, HR.

Teraz, w momencie dodawania pracownika (Employee) możemy wybrać lub dodać dział.

Można trochę "podkręcić" panel administracyjny, żeby na stronie działu wyświetlać jego pracowników. W tym celu wykorzystam pola Inline
. Są dwa rodzaje: TabularInline
i StackedInline
(ref: https://docs.djangoproject.com/en/4.1/ref/contrib/admin/#inlinemodeladmin-objects). TabularInline
wyświetli dane w postaci tabelki, StackedInline
z kolei wyświetli w formie "stosu".
Zmodyfikowany kod admin.py
, żeby wyświetlić oba przypadki, wygląda tak:
from django.contrib import admin
from .models import Employee, Department
class EmployeeTabularInline(admin.TabularInline):
model = Employee
# ustawiam nazwe wyświetlaną w adminie
verbose_name_plural = "Employees TabularInline"
# Extra pozwala określić ile "pustych" wierszy będzie widoczne w na stronie.
# Domyślnie są to 3.
extra = 1
class EmployeeStackedInline(admin.StackedInline):
model = Employee
verbose_name_plural = "Employees StackedInline"
extra = 1
class DepartmentAdmin(admin.ModelAdmin):
# dodaje inlines do modelu
inlines = [EmployeeTabularInline, EmployeeStackedInline]
admin.site.register(Employee)
# rejestruje DepartmentAdmin jako klasę wyświetlającją Department
admin.site.register(Department, DepartmentAdmin)

Jak widać, zbytniej różnicy pomiędzy StackedInline
i TabularInline
nie ma — poza sposobem wyświetlania. Oba pola zdecydowanie ułatwiają zarządzanie danymi. Trzeba natomiast uważać na limity — może się zdarzyć sytuacja, w której rekordów na stronie będzie zbyt dużo (bodaj ponad 100) i zmiany po prostu nie będą zapisywane przez panel administracyjny. Jedynym znanym mi w tym momencie obejściem problemu jest edycja modelu Employee
bezpośrednio na stronie Employee
w panelu administracyjnym, nie za pomocą Inlines
.
Relacje jeden do wielu w zapytaniach ORM
Django i jego model obsługi bazy danych (ORM) znacząco ułatwiają wyszukiwanie połączonych modeli. Poniżej przykłady z wykorzystaniem Djangowej powłoki, uruchamianej poleceniem ./manage.py shell
$ ./manage.py shell
Python 3.10.7 (main, Sep 15 2022, 01:51:29) [Clang 14.0.0 (clang-1400.0.29.102)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from accounts.models import Employee, Department
>>> it = Department.objects.get(name="IT")
>>> it
<Department: IT>
# Wyszukanie wszystkich pracowników działu IT
>>> it.employee_set.all()
<QuerySet [<Employee: Joe>, <Employee: John>, <Employee: Judy>, <Employee: Billy>]>
Widoczne w zapytaniu sformułowanie employee_set
to relacja wsteczna. Umożliwia ona wszystkie standardowe operacje na queryset
, czyli all()
, filter()
, get
, itd.
>>> it.employee_set.get(first_name="Joe")
<Employee: Joe>
>>> it.employee_set.filter(first_name__startswith="J")
<QuerySet [<Employee: Joe>, <Employee: John>, <Employee: Judy>]>
Nazwa relacji wstecznej jest tworzona automatycznie przez Django z połączenia nazwy modelu i słowa set
. Można stworzyć własną nazwę, poprzez wykorzystanie klucza related_name
w modelu definiującym relacje, tutaj Employee
.
class Employee(models.Model):
first_name = models.CharField(max_length=255)
# ponieważ Department jeszcze nie jest zadeklarowany, jego nazwa jest w cudzysłowiu
# Dodane related name
department = models.ForeignKey("Department", related_name="employees", on_delete=models.DO_NOTHING)
# dla uproszczenia pomijam pozostałe pola.
def __str__(self):
return f"{self.first_name}"
Zmiana modelu wymusza restart konsoli Djangowej. Po tej operacji zapytania wyglądają następująco:
» ./manage.py shell
Python 3.10.7 (main, Sep 15 2022, 01:51:29) [Clang 14.0.0 (clang-1400.0.29.102)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from accounts.models import Employee, Department
>>> it = Department.objects.get(name="IT")
>>> it.employees.all()
<QuerySet [<Employee: Joe>, <Employee: John>, <Employee: Judy>, <Employee: Billy>]>
>>> it.employees.filter(first_name__startswith="J")
<QuerySet [<Employee: Joe>, <Employee: John>, <Employee: Judy>]>
Jest czytelniej. Po nabraniu pewnego doświadczenia related_name
przestaje mieć znaczenie.
Relacje jeden do wielu, podobnie jak jeden do jednego, można również odpytywać wprost, czyli z poziomu modelu Employee
(deklarującego relacje) pytać o Department
.
» ./manage.py shell
Python 3.10.7 (main, Sep 15 2022, 01:51:29) [Clang 14.0.0 (clang-1400.0.29.102)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from accounts.models import Employee, Department
>>> employee = Employee.objects.first()
>>> employee
<Employee: Joe>
>>> employee.department
<Department: IT>
>>>
Ciekawe może być odpytanie o współpracowników naszego Joego.
>>> employee.department.employees.all()
<QuerySet [<Employee: Joe>, <Employee: John>, <Employee: Judy>, <Employee: Billy>]>
Mając rozbudowany model można wyobrazić sobie zapytanie o współpracowników Joego ktorzy są na zwolnieniu chorobowym.
Dla osób, które są bardziej zaznajomione z bazami danych i SQL, użyteczne może być podejrzenie zapytania, jakie tworzy Django. Uzyskuje się to poprzez metodę .query
na queryset
.
>>> print(employee.department.employees.all().query)
SELECT "accounts_employee"."id", "accounts_employee"."first_name", "accounts_employee"."department_id" FROM "accounts_employee" WHERE "accounts_employee"."department_id" = 1
Tutaj ciekawostka. Django nie próbuje tutaj szukać, jaki Deparment
ma Joe. Po prostu wykorzystuje wartość department_id
z wiersza tabeli opisującego Joe, robi proste zapytanie do tabeli accounts_employee
, szukając innych pracowników z department_id = 1
. Optymalne podejście.
Przypomnę tutaj jeszcze, że queryset
w Django są leniwe. Oznacza to, że możemy konstruować dowolnie skomplikowane queryset
, a zapytanie do bazy zostanie wykonane dopiero w momencie, kiedy poprosimy o dane. Oto przykład:
» ./manage.py shell
Python 3.10.8 (main, Nov 15 2022, 05:25:54) [Clang 14.0.0 (clang-1400.0.29.202)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from accounts.models import Employee, Department
>>> employee = Employee.objects.first()
>>> employee
<Employee: Joe>
>>> # zapytanie o wszystkich współpracowników Joe
>>> # nie zostanie wykonane - tylko konstruujemy Queryset
>>> deparment_employees = employee.department.employees.all()
>>> # szukam tych, których imiona zaczynają się na "Bi"
>>> # też nie wykonane zapytanie - tylko dodany warunek
>>> filtered_employees = deparment_employees.filter(first_name__startswith="Bi")
>>> # dopiero tutaj wykonuje zapytanie
>>> filtered_employees
<QuerySet [<Employee: Billy>]>
>>> print (employee.department.employees.all().filter(first_name__startswith="Bi").query)
SELECT "accounts_employee"."id", "accounts_employee"."first_name", "accounts_employee"."department_id" FROM "accounts_employee" WHERE ("accounts_employee"."department_id" = 1 AND "accounts_employee"."first_name"::text LIKE Bi%)
>>>
Przeanalizujmy to zapytanie:
SELECT "accounts_employee"."id", "accounts_employee"."first_name", "accounts_employee"."department_id" FROM "accounts_employee"
- pobierz wartości kolumn dla pól id
, first_name
, department_id
z tabeli accounts_employee
- to standardowe działanie.WHERE ("accounts_employee"."department_id" = 1 AND "accounts_employee"."first_name"::text LIKE Bi%)
- tu dzieje się magia — nie widać żadnego zapytania o wszystkich pracowników, Django od razu filtruje AND "accounts_employee"."first_name"::text LIKE Bi%"
. Szuka tylko tych, których imię zaczyna się na "Bi". Warto zwrócić uwagę na operator AND
- Django dodało warunek WHERE ("accounts_employee"."department_id" = 1
na podstawie employee.department
- po prostu przefiltrowało tabele.
W następnym wpisie omówię, z przykładami, relację wiele do wielu. To będzie długi post i zdecydowanie najbardziej rozbudowany w serii. Niedługo!
Zapisz się na newsletter by otrzymać informacje, kiedy pojawi się nowy post w tej serii. Zapraszam!
Ps. Spodobał Ci się post? Udostępnij go na swoich kanałach.
PS2. Masz uwagi do posta, chcesz porozmawiać, szukasz pomocy z Pythonem i Django? Napisz do mnie!