Django

Model

Modely

Spíš pár úvah a postřehů...

Obsah stránky (hide)

  1.   1.  ManyToMany
    1.   1.1  Bez zprostředkující tabulky
    2.   1.2  Se zprostředkující tabulkou
    3.   1.3  V jedné tabulce

1.  ManyToMany

Tady jsou jisté rozdíly, (ne)použijeme-li explicitní zprostředkující tabulku (through='TblName').

Nemáme-li jí, Django ji vytvoří automaticky na pozadí a bude obsahovaj jen tři pole: id, tbl1_id a tbl2_id. Tj. id záznamu v tabulce vztahu a odkazy na id z propojených tabulek. Záznamy se přidávají pomocí metody add(). Django navíc hlídá unikátnost záznamů ve zprostředkující tabulce - každá kombinace tbl1_id a tbl2_id musí být jedinečná. Při pokusu přidat znovu totéž to ale neskončí výjimkou - Django to tiše přejde.

Pokud si zprostředkující tabulku vytvoříme sami, pak nelze používat metodu add() pro přidání záznamu, protože by Django nevědělo, co napsat do ostatních položek ve zprostředkující tabulce. Musíme tedy vytvářet záznamy ve zprostředkující tabulce přímo. Dalším rozdílem, že Django nám tu nehlídá jedinečnost záznamů, takže můžeme vložit i více záznamů se stejnou kombinací tbl1_id, tbl2_id. Kontrolu jedinečnosti můžeme vynutit použitím vlastnosti unique_together třídy Meta.

1.1  Bez zprostředkující tabulky

Vytvoříme modely a data

Budeme sledovat členství hudebníků ve skupinách:

 # models.py

 class Musician(models.Model):
     name = models.CharField(max_length=50)

     def __unicode__(self):
         return self.name


 class Band(models.Model):
     name = models.CharField(max_length=50)
     musician = models.ManyToManyField('Musician')

     def __unicode__(self):
         return self.name

Nejprve vytvoříme nějaké hudebníky (v konzoli).

 # python manage.py shell
 # tohle: >>> tam nedělám, aby se to dobře kopírovalo.
 h1 = Musician.objects.create(name='Carl Gustafson')
 h2 = Musician.objects.create(name='Ela Miami')
 h3 = Musician.objects.create(name='Henry Ayaho')
 h4 = Musician.objects.create(name='Milan McMilan')
 h5 = Musician.objects.create(name='Irene Loud')

A skupiny:

 b1 = Band.objects.create(name='Walkers')
 b2 = Band.objects.create(name='Dry Trees')

Členy přidáme metodou add():

 b1.musician.add(h1, h2, h4)
 b2.musician.add(h1, h3, h5)

Takže h1 hraje v obou kapelách - jedna kapela může mít více členů, jeden hudebník může být ve více kapelách, to je vztah ManyToMany.

Co se stane, pokusíme-li se přidat h2 do b1 znovu? Čekáte-li, že to skončí chybou (už tam je, takže by se narušila jedinečnost záznamů o vztazích), tak se pletete. Django tento lapsus tiše přejde a chybu nenehlásí. Zároveň ale duplikátní záznam nepřidá, takže vše dopadne O.K. Trocha rámusu by tady asi neškodila...

Přidávali jsme hudebníky do kapely. Můžeme to dělat i obráceně? Přidávat kapelu k hudebníkovi? Ano:

 >>> h3.band_set.add(b1)

Všimněme si, že v prvním případě jsme se na druhou tabulku odkazovali jejím jménem (musician), v druhém případě jako na sadu (band_set) - to je pro mě trochu matoucí. Z tabulky kde je ManyToMany definováno se do té druhé odkazujeme jejím jménem, opačně pomocí jméno_set.

K dispozici máme i poněkud obskurní styl:

 >>> b1.musician.create(name='Frank Houska')

To vytvoří hudebníka a rovnou ho šoupne do kapely. Někdy je Django fakt gumové.

Čtení dat

Chceme-li znát členy kapely b1:

 >>> b1.musician.all()

Chceme-li vědět, kde všude hraje hudebník h1:

 >>> h1.band_set.all()

Samozřejmě můžeme používat i .filter(), .exclude() a .get() a spočítat si je pomocí .count(). Metoda .exists() zjistí, zda má kapela nějaké členy. Metody .values() a .values_list() vrátí data jako slovník, nebo seznam.

Odstraňování dat ze vztahu

K tomu neslouží metoda delete(), ale remove(): Metoda k tomu použitá nemá očekávané jméno delete, ale remove():

 >>> b1.musician.remove(h3)

Odstranili jsme hudebníka h3 ze skupiny b1. A jde to i opačně:

 >>> h3.band_set.remove(b1)

Pokusíme-li se odstranit hudebníka ze skupiny, v níž není, Django to tiše přejde - chybu nehlásí.

Pokud se skupina rozpadne, použijeme metodu clear():

 >>> b1.musicians.clear()

Hudebníci i kapla zůstanou v databázi. Kapela ale nemá žádné členy.

Odstranit můžeme samozřejmě hudebníky i kapely - tady klasickým delete():

 >>> h1.delete()

Po vymazání odstraní Django i odpovídající záznamy ze zprostředkující tabulky; automaticky. To je logické - jak by mohl záznam ve zprostředkující tabulce odkazovat na neexistujícího hudebníka?

Je to výchozí chování v relační databázi - akce se dějí v kaskádě. Lze to změnit v definici modelu použitím parametru on_delete. Pokud místo kaskády dáme PROTECT, skončí pokus o smazání hudebníka, který je v nějaké kapele (tedy má záznam ve zprostředkující tabulce) výjimkou ProtectedError.

 class Band ...
     ...
     musician = models.ManyToManyField(..., on_delete=models.PROTECTED)

1.2  Se zprostředkující tabulkou

 class Man(models.Model):
     name = models.CharField(max_length=50)
     woman = models.ManyToManyField('Woman', through='Relat')

     def __unicode__(self):
         return self.name


 class Woman(models.Model):
     name = models.CharField(max_length=50)

     def __unicode__(self):
         return self.name


 class Relat(models.Model):
     REL_CHOICES = (
         (1, 'friend'),
         (2, 'lover'),
         (3, 'married'),
         (4, 'ex')
     )
     man = models.ForeignKey(Man)
     woman = models.ForeignKey(Woman)
     relation = models.SmallIntegerField(choices=REL_CHOICES)

     def __unicode__(self):
         return '{0} + {1} = {2}'.format(self.man, self.woman, self.get_relation_display())

Tabulka Relat spojuje tabulky Man a Woman do ManyToMany vztahu. To bychom zvládli i bez této tabulky, ale my jí potřebujeme proto, že chceme sledovat i další aspekty vztahu. Zde třeba druh vztahu (kamarád, milenec,...). Mohli bychom sledovat i datum vzniku vztahu apod. Ale nekomplikujme si příklad.

V modelu Man je spojení do modelu Woman a je tam nově parametr through=, který odkazuje na zprostředkující tabulku.

Propojovací tabulka musí obsahovat cizí klíče do obou propojovaných tabulek. Dále pak může obsahovat i další data (kvůli nim se s ní zřizujeme).

Přidávání dat

Pro začátek pár příkazů do Django shellu, ať máme nějaká data:

 v = Man.objects.create(name='Viktor')
 p = Man.objects.create(name='Pavel')
 j = Man.objects.create(name='Jirka')
 a = Woman.objects.create(name='Alice')
 b = Woman.objects.create(name='Beta')
 c = Woman.objects.create(name='Cilka')
 d = Woman.objects.create(name='Dana')

Pokud bychom chtěli Jirkovi zadat nějaké vztahy, není možné použít metodu .add(). Tedy není možné napsat:

 # Tohle nejde!:
 j.woman.add(a, c)

Je to proto, že zprostředkující tabulka obsahuje i údaj "relation" a Django neví, co by tam mělo zapsat. Nejen, že to nejde, Django tuto metodu v tomto případě ani nenabízí. Takže představa, že by se to vyřešilo nějakým default, nebo blank=True jde mimo.

Máme-li zprostředkující tabulku, musíme zadávat údaje přímo do ní:

 rel_ja = Relat.objects.create(man=j, woman=a, relation=2)

A Jirka má milenku Alici. A teď ještě kamarádka Cecilka:

 rel_jc = Relat.objects.create(man=j, woman=c, relation=1)

Čtení dat

Můžeme se teď na ty vztahy kouknout:

 j.woman.all()
 [<Woman: Alice>, <Woman: Cilka>]

 c.man_set.all()
 [<Man: Jirka>]

Takže tohle už známe. Přibyla nám ale i tabulka Relat, takže vidíme i do ní:

 Relat.objects.all()

Z instance "j", tj. "Jirka" vidíme i do tabulky "Relat":

 j.relat_set.all()

Vypíše všechny záznamy z tabulky Relat, které se týkají Jirky. Každý jednotlivý záznam samozřejmě vidí všechny údaje z této tabulky z tabulek provázaných:

 j0 = j.relat_set.All()[0]
 j0.man
 j0.man.name

Vybrali jsme jeden záznam ze sady Jirkových vztahů. Vidíme tu pochopitelně údaj "man" - protože ten tu fyzicky je, ale můžeme se přes tečkovou notaci dostat i do nadřízené tabulky a vypsat si údaj "name". Totéž jde samozřejmě i pro "woman".

 rel_jc
 <Relat: Jirka + Cilka = friend>

 rel_jc.man
 rel_jc.woman
 rel_jc.relation
 rel_jc.get_relation_display()

 rel_jc.woman.name
 rel_jc.woman.man_set.all()

Poslední řádek je opět o gumovosti Djanga a ukazuje, s kým vším má Jirkova kamarádka Cilka ještě vztahy.

Chceme-li si vypsat informace o tom, s kým mají vztah Jirkovy ženy:

 for jirkovo in j.woman.all():
     print(jirkovo.name)
     for chlap in jirkovo.man_set.all():
         print(' - {0}'.format(chlap.name))

Unikátnost záznamů

Zkusíte-li zadat znovu příkaz pro vytvoření již existujícího vztahu, Django ochotně zapíše do databáze podruhé to samé. Narozdíl od ManyToMany bez explictní tabulky tady Django unikátnost nehlídá. A je to vlastně dobře. Protože neví, co má a nemá být unikátní, když je tu tolik dalších možností.

Přijde-li nám nelogické, aby dva lidi spolu mohli mít víc než jeden vztah, pomůže nám, když do modelu Relat přidáme:

 class Relat...
     ...
     class Meta:
         unique_together = ('man', 'woman')

Nyní, když bychom se pokusili zapsat další vztah mezi těmito dvěm lidmi, vyhodí Django výjimku IntegrityError.

Ale co když je to dobře?

Mějme situaci, kdy vztahy nesledujeme jen teď, ale dlouhodobě. Pak bychom do zprostředkující tabulky mohli přidat údaja o datu počátku vztahu (např. relat_from) a řekněme že kamarádi od 1.9.2008, kdy se seznámili na škole, spolu začali 31.12.2009 chodit. Pak tito dva lidé musí mít možnost mít v naší tabulce další záznam. Vynucená unikátnost by tu byla na překážku.

Nicméně i tady se nabízí unikátnost ke sledování. Neměli by mít ve stejný den dva různé vztahy (i když kdo ví, co všecho se dá stihnout za jeden den;) - tady nám ten příklad trochu zakulhá. :-)

 class Relat...
     ...
     relat_from = models.DateField()
     ...
     class Meta:
         unique_together = ('man', 'woman', 'relat_from')

(Opět se potvrzuje, že při školních příkladech je dobré se držet na hony daleko od časových údaju;).

1.3  V jedné tabulce

V přikladech se vztahy je zřejmé, že modely man i woman jsou vlastně stejné - tedy nebudeme-li sledovat pohlavní rozdíly. Je tedy zbytečné mít dvě tabulky, když nám může stačit jedna. Pak můžeme provádět vztahy odkazem na tu samou tabulku, tedy na sebe sama, anglicky self:

 class Person(models.Model):
     name = models.CharField(max_length=50)
     partner = models.ManyToManyField('self', symmetrical=False, through='PersRelat')

     def __unicode__(self):
         return self.name

 class PersRelat(models.Model):
     REL_CHOICES = (
         (1, 'friend'),
         (2, 'lover'),
         (3, 'married'),
         (4, 'registered'),
         (9, 'ex')
     )
     relation = models.SmallIntegerField(choices=REL_CHOICES)
     relat_from = models.DateField()
     partner1 = models.ForeignKey(Person, related_name='partner1')
     partner2 = models.ForeignKey(Person, related_name='partner2')

     class Meta:
         unique_together = ('partner1', 'partner2', 'relat_from')

     def __unicode__(self):
         return '{0} + {1} = {2}'.format(self.partner1, self.partner2, self.get_relation_display())

Takže máme jedinou tabulku (model) "Person", který má vztah ManyToMany na 'self'. Vztahy zprostředkováváme tabulkou PersRelat, v níž navíc sledujeme druh vztahu a datum počátku vztahu. Vytvořme si data:

 a = Person.objects.create(name='Alice')
 b = Person.objects.create(name='Betty')
 c = Person.objects.create(name='Cindy')
 d = Person.objects.create(name='Dana')
 s = Person.objects.create(name='Stan')
 t = Person.objects.create(name='Thomas')
 u = Person.objects.create(name='Ulysses')
 v = Person.objects.create(name='Victor')

 rel_as = PersRelat.objects.create(partner1=a, partner2=s, relation=1, relat_from=date(2008, 9, 1))
 rel_av = PersRelat.objects.create(partner1=a, partner2=v, relation=2, relat_from=date(2011, 4, 12))
 rel_av = PersRelat.objects.create(partner1=a, partner2=v, relation=3, relat_from=date(2013, 7, 10))
 rel_cv = PersRelat.objects.create(partner1=c, partner2=v, relation=2, relat_from=date(2013, 11, 7))
 rel_vu = PersRelat.objects.create(partner1=v, partner2=u, relation=1, relat_from=date(2010,7,15))

Údaje ze zprostředkující tabulky ukazují oba do tabulky Person, proto musíme použít parametr 'related_name', který říká, jak se bude jmenovat odkaz do nadřízené tabulky (bez něj by se oba odkazy musely jmenovat 'person' a nevěděli bychom, jestli myslíme jednu či druhou osobu ze vztahu).

Bez zprostředkující tabulky by byl život jednodušší, protože vztah je v zásadě symetrický, takže pro konkrétní instanci by existoval 'person_set' obsahující záznamy osob, s níž má daná instance vztah.

Život je ale složitější, a tak zprostředkující tabulku potřebujeme.

Instance "v" sice má "person_set", ale ten obsahuje jen některé vztahy. Všechny vztahy získáme ze dvou dotazů:

 v.partner1.all()
 v.partner2.all()

což je poněkud krkolomné. Jenže jednou je ve vztahu daná osoba v položce "parnter1", jindy zase v "partner2", takže jak jinak...? To teď nevím a jdu spát. To be continued... :-)