Witaj w kolejnym poście z serii: Zrozumieć Django.
W poprzednich postach omówiłem podstawy pracy z modelami, relacje jeden do jednego (one to one) i wiele do jednego (OneToMany, ForeignKey).
W tym poście opiszę relacje wiele do wielu. Dowiesz się:
- Czym jest relacja wiele do wielu. Poznasz przykłady spotykane w rzeczywistym świecie.
- Jak stworzyć prostą relację wiele do wielu.
- Jak stworzyć zaawansowaną relację wiele do wielu z dodatkowymi danymi.
- Jak pobierać dane w relacjach wiele do wielu.
- Jak usuwać relacje wiele do wielu.
Dodatkowo, do każdego zagadnienia będą podane obszerne listingi kodu z opisami. Pozwoli Ci to na powtórzenie ćwiczeń na własnym środowisku. Jeśli po lekturze posta będziesz miał pytania lub potrzebował porady - śmiało pisz do mnie. Czytam wszystkie maile.
Czym jest relacja wiele do wielu? Przykłady.
Relacja wiele do wielu jest chyba najczęściej spotykaną w "prawdziwym życiu". W systemach informatycznych jest natomiast stosowana dość rzadko, by nie powiedzieć, unikana. Powodem może być to, że system informatyczny często operuje na wybranym fragmencie rzeczywistości i dąży do jego uproszczenia. Gdzie więc spotkać można relację wiele do wielu?
- Relacja między pacjentami a lekarzami w szpitalu. Każdy pacjent może mieć wielu lekarzy, a każdy lekarz może mieć wielu pacjentów.
- Relacja między książkami a autorami w bibliotece. Każda książka może mieć wielu autorów, a każdy autor może mieć wiele książek.
- Relacja między nauczycielami a przedmiotami w szkole. Każdy nauczyciel może uczyć wielu przedmiotów, a każdy przedmiot może być nauczany przez wielu nauczycieli.
- Relacja między zawodnikami a drużynami w sporcie. Każdy zawodnik może grać w wielu drużynach, a każda drużyna ma wielu zawodników.
- Relacja między filmami a aktorami w kinie. Każdy film może mieć wielu aktorów, a każdy aktor może wystąpić w wielu filmach.
- Relacja między produktami a zamówieniami w sklepie internetowym. Produkt możeb być w wielu zamówieniach, a zamówienie ma wiele produktów.
Ponieważ w poprzednich postach omawiałem przykład aplikacji do zarządzania szkołą, tutaj też podążę tym tropem.
Omówię relację pomiędzy studentem i kursem. Student może uczęszczać na wiele kursów np, matematyka, fizyka, chemia itp. Kurs z kolei jest przypisany do wielu studentów. Dodatkowo fajnie byłoby wiedzieć kiedy dany student rozpoczął i skończył kurs oraz jaką ocenę na nim uzyskał.
Jak stworzyć prostą relację wiele do wielu?
Relację wiele do wielu w Django uzyskuje się poprzez zastosowanie pola ManyToManyField. Pole to posiada następujące argumenty:
to
- wymagany argument, wskazujący na model do którego ma być stworzona relacja. Podobnie jak przy ForeignKey, jeśli model jeszcze nie został stworzony, nazwę trzeba podać w "cudzysłowiu".related_name
- używane do relacji wstecznych (https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.ForeignKey.related_name).related_query_name
- używane do filtrowania relacji wstecznych (https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.ForeignKey.related_query_name).limit_choices_to
- używane do ograniczania opcji wyboru podczas edycji modelu w panelu administracyjnym lub poprzez ModelForm (https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.ForeignKey.limit_choices_to). Uwaga: dalej można zapisać dowolną wartość dla pola w bazie poprzez bezpośrednią edycje modelu.symmetrical
- używane w przypadku relacji modelu do samego siebie, na przykład jeśli ja jestem Twoim przyjacielem, to Ty jesteś moim przyjacielem. (https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.ManyToManyField.symmetrical).through
- pole generowane automatycznie przez Django do obsługi relacji. Można je stworzyć samemu i wykorzystać do dodatkowych danych (https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.ManyToManyField.through). Omawiam to w sekcji "Jak stworzyć zaawansowaną relację wiele do wielu z dodatkowymi danymi?"
Poniżej przedstawiam kod potrzebny do stworzenia prostej relacji wiele do wielu. Korzystam z uproszczonych modeli, nie dodaję pola "through". Na diagramie relacje można przedstawić:
W models.py
wygląda to tak:
# models.py
from django.db import models
# Create your models here.
class Student(models.Model):
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
def __str__(self):
return f"{self.first_name} {self.last_name}"
class Course(models.Model):
name = models.CharField(max_length=255)
students = models.ManyToManyField(Student)
def __str__(self):
return self.name
Żeby zmiany były widoczne w aplikacji, należy przygotować i wykonać migrację.
./manage.py makemigrations
./manage.py migrate
Po wykonaniu migracji warto dodać model Course i Student do panelu administracyjnego, i tak podejrzeć relacje.
# admin.py
from django.contrib import admin
# Register your models here.
from .models import Student, Course
admin.site.register(Student)
admin.site.register(Course)
Teraz podczas dodawania i edycji kursu można dodać do niego studentów.
A co się wydarzyło "za kulisami"? Django automatycznie stworzyło sobie tabelę pomocniczą w bazie danych, która zawiera klucze obce do modeli Course i Student.
Niestety, na stronie studenta w panelu administracyjnym nie widać kursów, na jakie student jest zapisany. Naprawić to można przez TabularInline. Mając do czynienia z polem typu ManyToMany, nie można użyć bezpośrednio modelu "Course" jako TabularInline. Zgodnie z dokumentacją, należy użyć modelu Course.students.through
. Jest to model utworzony automatycznie przez Django do obsługi relacji ManyToMany(M2M).
# admin.py
from django.contrib import admin
# Register your models here.
from .models import ContactMessage, StudentCard, Student, Course
admin.site.register(Course)
class CourseTabularInline(admin.TabularInline):
model = Course.students.through
verbose_name_plural = "Courses"
# Extra pozwala określić ile "pustych" wierszy będzie widoczne w panelu.
# Domyślnie są to 3.
extra = 1
class StudentAdmin(admin.ModelAdmin):
# dodaje inlines do modelu
inlines = [CourseTabularInline]
admin.site.register(Student, StudentAdmin)
Teraz, korzystając zarówno ze stron edycji studenta, jak i kursu możemy kontrolować, który student jest zapisany na jaki kurs.
Tworzenie relacji wiele do wielu z poziomu kodu.
Połączenie instancji obiektów za pomocą relacji wiele do wielu możemy stworzyć za pomocą następujących metod:
- add
- create
- set
Dodatkowo, można tworzyć relacje za pomocą bezpośredniej edycji modelu Course.students.through
. To zaawansowana technika, wykraczające poza zakres tego artykułu. W sumie nie przypominam sobie nawet, czy kiedykolwiek sam z tego korzystałem poza wyświetlaniem relacji w panelu administracyjnym.
Tworzenie relacji wiele do wielu przy wykorzystaniu metody add
.
Metoda add
pozwala na dodanie relacji pomiędzy dwoma już istniejącymi instancjami obiektów, które są powiązane relacją ManyToMany albo OneToMany, tj. jeden do wielu (dokumentacja). Dla przykładu dodam kurs do studenta, korzystając z Djangowej konsoli uruchamianej poleceniem ./manage.py shell
.
» ./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 Course, Student
>>> # sprawdzenie czy mam jakieś dane w bazie
>>> Course.objects.all()
<QuerySet [<Course: Algebra>, <Course: Mathematics>, <Course: test>]>
>>> Student.objects.all()
<QuerySet [<Student: John Doe>, <Student: Clark Kent>, <Student: Johny Mnemonic>]>
>>> # sprawdzenie, czy mam jakieś relacje pomiędzy studentami a kursami
>>> for course in Course.objects.all():
... course.students.all()
...
<QuerySet []>
<QuerySet []>
<QuerySet []>
>>> # biorę pierwszego studenta i pierwszy kurs
>>> student = Student.objects.first()
>>> course = Course.objects.first()
>>> # tworzę relacje
>>> course.students.add(student)
>>> course.students.all()
<QuerySet [<Student: John Doe>]>
>>> # dodaje kolejnego studenta - ostatniego
>>> student2 = Student.objects.last()
>>> student2
<Student: Clark Kent>
>>> course.students.add(student2)
>>> course.students.all()
<QuerySet [<Student: John Doe>, <Student: Clark Kent>]>
>>> # teraz dodam kurs do studenta od strony studenta
>>> # sprawdzam kursy studenta
>>> student.course_set.all()
<QuerySet [<Course: Algebra>]>
>>> # pobieram kurs który za chwilę dodam
>>> course2 = Course.objects.last()
>>> course2
<Course: test>
>>> student.course_set.add(course2)
>>> # sprawdzam
>>> student.course_set.all()
<QuerySet [<Course: Algebra>, <Course: test>]>
>>>
Tworzenie relacji wiele do wielu przy wykorzystaniu metody create
.
Metoda create pozwala w jednym kroku stworzyć instancje oraz dodać relacje. Na przykład stworzyć studenta i dodać do kursu. Pokażę to w konsoli.
» ./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 Student, Course
>>> # sprawdzam pierwszy kurs - zapisanych studentów
>>> course = Course.objects.first()
>>> course.students.all()
<QuerySet []>
>>> # sprawdzam listę wszystkich studentów, zeby nie stworzyć duplikatu
>>> Student.objects.all()
<QuerySet [<Student: John Doe>, <Student: Clark Kent>, <Student: Johny Mnemonic>]>
>>> # tworzę studenta i automatycznie zapisuje na kurs
>>> course.students.create(first_name="Lois", last_name="Lane")
<Student: Lois Lane>
>>> # sprawdzam dane
>>> Student.objects.all()
<QuerySet [<Student: John Doe>, <Student: Clark Kent>, <Student: Johny Mnemonic>, <Student: Lois Lane>]>
>>> course.students.all()
<QuerySet [<Student: Lois Lane>]>
>>>
Jak widać studentka "Lois Lane" została stworzona oraz automatycznie dodana do osób zapisanych na kurs. Można również zrobić to w drugą stronę, czyli stworzyć kurs z poziomu studenta, za pomocą course_set
. Pozostawiam to jako ćwiczenie.
Tworzenie relacji wiele do wielu przy wykorzystaniu metody set
.
Metoda set jednocześnie usuwa wszystkie relacje i dodaje nowe. U nas zastąpię Lois Lane Clarkiem Kentem (Lois została dodana do kursu w poprzednim kroku).
» ./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 Course, Student
>>>
>>> # biorę kurs
>>> course = Course.objects.first()
>>> course
<Course: Algebra>
>>> course.students.all()
<QuerySet [<Student: Lois Lane>]>
>>> # szukam Clarka
>>> clark = Student.objects.filter(first_name="Clark").first()
>>> clark
<Student: Clark Kent>
>>> # dodatkowo, dodam Johnego Mnemonic
>>> johny = Student.objects.filter(last_name="Mnemonic").first() # dlaczego first? Filter zwraca listę
>>> johny
<Student: Johny Mnemonic>
>>> # zapisuję Clarka i Johnego na Algebrę i usuwam Lois w jednym kroku
>>> course.students.set([clark, johny])
>>> course.students.all()
<QuerySet [<Student: Clark Kent>, <Student: Johny Mnemonic>]>
>>>
Jak stworzyć zaawansowaną relację wiele do wielu z dodatkowymi danymi?
W poprzedniej sekcji pokazałem jak tworzyć proste relacje wiele do wielu.
W rzeczywistym świecie taka relacja to trochę mało. Brakuje informacji o tym, kiedy student zapisał się na kurs, jaką ocenę uzyskał, czy kurs jest zaliczony. Dodatkowe dane można by mnożyć, np. kto prowadzi dany kurs (to kolejna relacja wiele do wielu z modelem Employee), daty rozpoczęcia i zakończenia kursu, i zapewne wiele więcej. Dla uproszczenia skupię się tutaj tylko na studencie. Potrzebuję następujących informacji:
- data zapisania się studenta na kurs
- ocena końcowa (początkowo null)
- czy kurs jest zaliczony? Tutaj wprowadzę trochę automatyzacji: kurs będzie zaliczony jeśli ocena końcowa jest wyższa niż 2 i oczywiście początkowo jest oznaczony jako niezaliczony. Student może przecież odpuścić i nie pojawić się na zaliczeniu.
Spełnienie tych założeń uzyskamy przez stworzenie własnego modelu pośredniczącego (through) w relacji Student -> Course.
Uwaga!
Zmiana pliku models.py
wg poniższego kodu i wykonanie migracji spowoduje błąd! Dlaczego? Następuje tutaj zmiana pola ManyToMany - dodajemy pole through. Django nie dopuszcza do takiej sytuacji. Żeby migracja została wykonana poprawnie należy:
- usunąc linię
students = models.ManyToManyField(Student, through="StudentCourseMembership")
w plikumodels.py
- zakomentować wszystko w pliku
admin.py
- inaczej Django wykryje brakującą relacje i nie pozwoli zrobić migracji - przygotować i wykonać migracje
./manage.py makemigrations
./manage.py migrate
Dane dla relacji student <> kurs utworzone wcześniej, zostały usunięte. Dopiero teraz można zaktualizować plik models.py
do poniższej wersji oraz przygotować i wykonać migrację.
# models.py
from django.db import models
# Create your models here.
from django.core.validators import MaxValueValidator
class Student(models.Model):
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
def __str__(self):
return f"{self.first_name} {self.last_name}"
class Course(models.Model):
name = models.CharField(max_length=255)
# nowy sposób tworzenia relacji, z polem through
students = models.ManyToManyField(Student, through="StudentCourseMembership")
def __str__(self):
return self.name
# nowy model dla relacji ManyToMany
class StudentCourseMembership(models.Model):
student = models.ForeignKey(Student, on_delete=models.CASCADE)
course = models.ForeignKey(Course, on_delete=models.CASCADE)
enrollment_date = models.DateField("Data zapisania studenta", auto_now_add=True)
final_grade = models.PositiveIntegerField("Ocena końcowa", validators=[MaxValueValidator(5)], null=True, blank=True)
def __str__(self):
return f"{self.student}: {self.course}"
@property
def passed(self):
# property jest wyliczane automatycznie dla każdej instacji obiektu
# chcesz wiedzieć więcej? Napisz do mnie przez formularz kontaktowy.
return self.final_grade is not None and self.final_grade > 2
Standardowo, należy przygotować i wykonać migrację.
./manage.py makemigrations
./manage.py migrate
Omówienie zmian w pliku models.py
- Model
Student
pozostaje bez zmian. - W modelu
Course
do polastudents
został dodany atrybutthrough
. Wskazuje on model, stworzony przez nas, do obsługi relacji studenta i kursu. - Powstał nowy model
StudentCourseMembership
.
Jak działa model StudentCourseMembership, czyli obsługa zaawansowanej relacji ManyToMany w praktyce.
Pole through
przy relacji ManyToMany pozwala na dodanie dodatkowych danych do relacji (https://docs.djangoproject.com/en/4.1/topics/db/models/#intermediary-manytomany). W tym przypadku są to:
- data zapisania studenta na kurs. Zapisywana automatycznie w momencie utworzenia obiektu.
- ocena końcowa
- automatycznie wyliczana wartość
passed
określająca czy student zaliczył dany kurs.
Model, który wykorzystujemy w polu through
musi mieć relację typu ForeignKey
do modelu źródłowego oraz docelowego. Generalnie można dowolnie ustalać co jest modelem źródłowym, a co docelowym - nie ma to żadnego znaczenia, poza formą robienia zapytań do bazy. Tutaj modelem źródłowym jest Course
, a docelowym Student
. Jak to określam? To model źródłowy, Course
, ma zadeklarowaną relacje do Students
porzez StudentCourseMembership
.
W modelu Course
StudentCourseMembership
jest zapisany w cudzysłowie, gdyż po prostu nie jest jeszcze zdefiniowanmy i Django musi na niego "poczekać". Próba usunięcia apostrofów zakończy się błędem: NameError: name 'StudentCourseMembership' is not defined
.
Na początku tej sekcji poprosiłem o zakomentowanie całej zawartości pliku admin.py, żeby uniknąć błędów. Zaktualizowany plik admin.py
# admin.py
from django.contrib import admin
# Register your models here.
from .models import Student, Course, StudentCourseMembership
admin.site.register(Course)
class CourseTabularInline(admin.TabularInline):
model = StudentCourseMembership
verbose_name_plural = "Courses"
# Extra pozwala określić ile "pustych" wierszy będzie widoczne w panelu.
# Domyślnie są to 3.
extra = 1
readonly_fields = ["enrollment_date", "passed"]
class StudentAdmin(admin.ModelAdmin):
# dodaje inlines do modelu
inlines = [CourseTabularInline]
admin.site.register(Student, StudentAdmin)
Tutaj za dużo zmian nie ma - import nowego modelu StudentCourseMembership oraz wykorzystanie tego modelu w CourseTabularInline. Dodałem też pola do readonly_fields
- to pozwoli wyświetlić te wartości, które są generowane automatycznie, czyli passed
i enrollment_date
.
Teraz w panelu administracyjnym studenta wygląda to tak
A z poziomu kursu:
Jak widać zniknęła opcja dodania studentów do kursu. Tak się dzieje przy wprowadzeniu własnego modelu through
. Żeby przywrócić tę możliwość, trzeba dodać TabularInline
, podobnie jak przy StudentAdmin
. Pozostawiam to jako ćwiczenie dla zainteresowanych.
Żeby w pełni zapanować nad relacją ManyToMany z modelem pośredniczącym, możemy dodać sobie ten model do panelu administracyjnego. W tym celu wystarczy tylko dodać admin.site.register(StudentCourseMembership)
na końcu pliku admin.py
. Teraz dodawanie studentów do kursu realizujemy w adminie przez edycje tego modelu.
Dodawanie i usuwanie relacji wiele do wielu - ManyToMany
z modelem pośredniczącym through
.
Dodawanie i usuwanie relacji wiele do wielu z modelem pośredniczącym w panelu admninistracyjnym realizowane jest przez standardową edycję modelu pośredniczącego. Opisałem to na końcu poprzedniej sekcji.
Dodawanie i usuwanie tej relacji z poziomu kodu niczym nie różni się od zwykłej pracy z modelem. Żeby dodać studenta do kursu trzeba stworzyć i zapisać obiekt modelu StudentCourseMembership
. Poniżej przykład, z wykorzystaniem konsoli Django, uruchamianej komendą ./manage.py shell
.
./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 Student, Course, StudentCourseMembership
>>> StudentCourseMembership.objects.count() # sprawdzam ilu istnieje zapisanych studentów
8
>>> # zapisuję studenta na kurs - tworzę relację
>>> student = Student.objects.first() # pobieram pierwszego studenta
>>> student # sprawdzam kto to dla pewności :-)
<Student: John Doe>
>>> course = Course.objects.first() # pobieram pierwszy kurs
>>> course # sprawdzam co to za kurs
<Course: Algebra>
>>> # zapis właściwy - stworzenie relacji
>>> # czyli zapisanie Joego na algebre
>>> course_membership = StudentCourseMembership(student=student, course=course)
>>> course_membership # sprawdzenie
<StudentCourseMembership: John Doe: Algebra>
>>> course_membership.enrollment_date # timestamp zapisany automatycznie przez auto_now_add. Puste, bo jeszcze nie było save na modelu.
>>> course_membership.final_grade # wartość domyślna - pusty string
>>> course_membership.passed # wartość domyślna
False
>>> course_membership.save() # zapis w bazie
>>> course_membership.enrollment_date # dopiero save powoduje zapisanie daty
datetime.date(2023, 2, 9)
>>> StudentCourseMembership.objects.count() # ilość zapisanych studentów podskoczyła o 1
9
>>>
Dodatkowo, relacje można tworzyć korzystając ze standardowych metod modeli jak add
, create
, set
, używając pola through_defaults
. Użycie tych metod świetnie opisuje dokumentacja Django i są też opisane w sekcji: Jak stworzyć prostą relację wiele do wielu. Ja osobiście preferuję tworzenie instancji modelu through bezpośrednio.
Filtrowanie, wyszukiwanie i praca z relacją wiele do wielu
Filtrowanie, wyszukiwanie i po prostu praca z relacją wiele do wielu z modelem pośredniczącym nie różni się od pracy przy relacji jeden do wielu. Właściwie relacja wiele do wielu z modelem pośredniczącym to jest relacja jeden do wielu. Po prostu "po środku" jest model pomocniczy. Relację jeden do wielu opisałem bliżej w poście Zrozumieć Django: Relacje pomiędzy modelami, jeden do wielu, porady i triki.
Poniżej kilka przykładów, również z wykorzystaniem Djangowej konsoli, uruchamianej przez ./manage.py shell
» ./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 Student, Course, StudentCourseMembership
>>> StudentCourseMembership.objects.all() # lista wszystkich zapisanych studentów
<QuerySet [<StudentCourseMembership: Johny Mnemonic: Algebra>, <StudentCourseMembership: Johny Mnemonic: Mathematics>, <StudentCourseMembership: John Doe: Algebra>, <StudentCourseMembership: John Doe: Algebra>, <StudentCourseMembership: John Doe: Algebra>, <StudentCourseMembership: John Doe: Algebra>, <StudentCourseMembership: John Doe: Algebra>, <StudentCourseMembership: John Doe: Algebra>, <StudentCourseMembership: John Doe: Algebra>]>
# z poziomu studenta, sprawdzenie informacji o kursach
>>> student = Student.objects.first() # pobranie pierwszego studenta
>>> student.studentcoursemembership_set.all() # sprawdzenie jego zapisów
<QuerySet [<StudentCourseMembership: John Doe: Algebra>, <StudentCourseMembership: John Doe: Algebra>, <StudentCourseMembership: John Doe: Algebra>, <StudentCourseMembership: John Doe: Algebra>, <StudentCourseMembership: John Doe: Algebra>, <StudentCourseMembership: John Doe: Algebra>, <StudentCourseMembership: John Doe: Algebra>]>
>>> student.studentcoursemembership_set.first() # wyciągnięcie pierwszego kursu i informacji o nim
<StudentCourseMembership: John Doe: Algebra>
>>> student.studentcoursemembership_set.first().course
<Course: Algebra>
>>> student.studentcoursemembership_set.first().course.name
'Algebra'
>>>
# z poziomu kursu, sprawdzenie informacji o studentach
>>> course = Course.objects.first()
>>> course.students.all()
<QuerySet [<Student: John Doe>, <Student: John Doe>, <Student: John Doe>, <Student: John Doe>, <Student: John Doe>, <Student: John Doe>, <Student: John Doe>, <Student: Johny Mnemonic>]>
>>> # wyświetlenie informacji o każdym ze studentów
>>> for student in course.students.all():
... student
...
<Student: John Doe>
<Student: John Doe>
<Student: John Doe>
<Student: John Doe>
<Student: John Doe>
<Student: John Doe>
<Student: John Doe>
<Student: Johny Mnemonic>
>>> # żeby dostać się do informacji o ocenach studentów z kursu,
>>> # trzeba skorzystać ze studentcoursemembership_set,
>>> # czyli relacji jeden do wielu do modelu StudentCourseMembership
>>> for course_membership in course.studentcoursemembership_set.all():
... course_membership.student
... course_membership.enrollment_date
...
<Student: Johny Mnemonic>
datetime.date(2023, 2, 5)
<Student: John Doe>
datetime.date(2023, 2, 9)
<Student: John Doe>
datetime.date(2023, 2, 9)
<Student: John Doe>
datetime.date(2023, 2, 9)
<Student: John Doe>
datetime.date(2023, 2, 9)
<Student: John Doe>
datetime.date(2023, 2, 9)
<Student: John Doe>
datetime.date(2023, 2, 9)
<Student: John Doe>
datetime.date(2023, 2, 9)
Usuwanie relacji wiele do wielu
Usuwanie relacji można zrobić na kilka sposobów. Metody te generalnie działają tak samo z modelem pośredniczącym through
jak i bez niego. Dodatkowo, można je zastosować przy relacji jeden do wielu (to zadanie domowe dla ciekawych).
- Bezpośrednie usunięcie instancji modelu pośredniczącego.
- Usunięcie obiektu z jednego końca relacji.
- Wykorzystanie metody
remove
- Wykorzystanie metody
clear
Bezpośrednie usunięcie instacji obiektu pośredniczącego
StudentCourseMembership.objects.first().delete()
- pobierze pierwszą (first()
) instancję StudentCourseMembership
i ją usunie. To podejście jest polecam tylko wtedy, kiedy sami zadeklarowaliśmy model through
- inaczej po prostu ciężko się do niego dostać.
Usunięcie obiektu z jednego końca relacji.
Na przykład usunięcie studenta z uczelni, po prostu usunie relację między tym studentem i kursem. Prześledźmy to na przykładzie.
» ./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 Student, Course
>>> # pobieram kurs
>>> course = Course.objects.first()
>>> course
<Course: Algebra>
>>> course.students.all()
<QuerySet [<Student: Clark Kent>, <Student: Johny Mnemonic>]>
>>> # pobiorę i usunę Clarka
>>> clark = Student.objects.filter(first_name="Clark").first()
>>> clark.course_set.all()
<QuerySet [<Course: Algebra>]>
>>> clark.delete() # usuwam studenta
(2, {'accounts.Course_students': 1, 'accounts.Student': 1})
>>> # sprawdzam ponownie kurs
>>> course.students.all()
<QuerySet [<Student: Johny Mnemonic>]>
>>> # sprawdzam jeszcze dla pewności studentów
>>> Student.objects.all()
<QuerySet [<Student: John Doe>, <Student: Johny Mnemonic>, <Student: Lois Lane>]>
>>>
Użycie metody remove
.
Remove
po prostu usuwa relacje między instancjami obiektów. Nie usuwa samych obiektów, poza ewentualnymi instancjami modelu through
,
» ./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 Student, Course, StudentCourseMembership
>>> course = Course.objects.first()
>>> course
<Course: Algebra>
>>> student = course.students.first()
>>> student
<Student: John Doe>
>>> course.students.all()
<QuerySet [<Student: John Doe>, <Student: John Doe>, <Student: John Doe>, <Student: John Doe>, <Student: John Doe>, <Student: John Doe>, <Student: John Doe>]>
>>> course.students.remove(student)
>>> course.students.all()
<QuerySet []>
Ciekawostka - zostały usunięte wszystkie relacje John Doe do kursu! Dlaczego? Metoda remove
usuwa wszystkie relacje pomiędzy dwoma instancjami. Tutaj student był wielokrotnie zapisany na ten sam kurs, stąd remove
wyczyściło wszystkie relacje między nimi. Dlatego osobiście preferuję metodę 1 - jawne usuwanie. Jeśli na kurs byłoby zapisanych wielu różnych studentów, zostałby usunięty tylko John Doe.
Użycie metody clear
Działanie metody clear
jest zbliżone do remove
, tylko nie przyjmuje parametrów. Metoda clear
po prostu usuwa wszystkie relacje z instancji:
» ./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 Student, Course, StudentCourseMembership
>>> course = Course.objects.last()
>>> course.students.all()
<QuerySet [<Student: Johny Mnemonic>]>
>>> course.students.clear() # usuwa wszystkie relacje pomiędzy tą instancją course a obiektami students
>>> course.students.all()
<QuerySet []>
>>>
Podsumowanie
W tym dość długim wpisie omówiłem relację wiele do wielu z wykorzystaniem modelu pośredniczącego. Django może model pośredniczący wygenerować dla nas automatycznie, lub możemy stworzyć go sami. W tym drugim przypadku możemy do relacji dodać dodatkowe informacje. U nas to były: data utworzenia relacji, ocena końcowa, czy kurs został zaliczony.
Ten post zamyka temat relacji pomiędzy modelami. Dziękuję Ci za uwagę :-). Czy jest coś, co obecnie w Django sprawia Ci trudność? Napisz do mnie a może powstanie z tego wpis na blogu? Czytam wszystkie maile.
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!