Pokračujeme dalším dílem o vazbách v Eloquentu a tentokrát se detailněji podíváme na vazbu n:n a také si vysvětlíme vztah hasManyThrough, který může být v určitých případech velmi užitečný. Opět si vše ukážeme na praktických příkladech.

Vazba n:n

Vazba n:n nebo také many to many, by se volně dalo přeložit jako mnoho rodičovských záznamů může mít mnoho dětských záznamů a naopak. Příkladem mohou být studenti a školní předměty. Student může být zapsán do více školních předmětů a zároveň jeden předmět mohou studovat více studentů. Dalším příkladem může být produkt a objednávka, kde objednávka může mít více produktů a jeden produkt může být ve více objednávkách. V našem případě jsem si zvolil autora a knihu, respektive autor může napsat více knih a zároveň jednu knihu mohlo napsat více autorů.

Budeme pokračovat ve stejné struktuře jako v minulém díle, tedy prvně si vytvoříme migrace, poté modely a popíšeme si, jak data vytahovat a ukládat, abychom zachovali vazbu n:n.

Migrace


Schema::create('authors', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
});
Schema::create('books', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
});
Schema::create('author_book', function (Blueprint $table) {
    $table->increments('id');
    $table->integer('book_id')->unsigned()->index();
    $table->foreign('book_id')->references('id')->on('books')->onDelete('cascade');
    $table->integer('author_id')->unsigned()->index();
    $table->foreign('author_id')->references('id')->on('authors')->onDelete('cascade');
});

Na rozdíl od jiných vazeb je u této potřeba vytvořit ještě třetí, propojovací tabulku, tzv. pivot. Tato tabulka nejčastěji slouží jako prostředník a kromě cizích klíčů další sloupce nepotřebuje. V našem případě jsem ještě přidal primární klíč id kvůli jednoznačné identifikaci vazby, nicméně jako klíč bychom mohli i použít kombinaci cizích klíčů, která by vždy měla být unikátní. Dalším sloupcem v pivotu by pak mohl být například datum vytvoření vazby. Propojovací tabulka je v této vazbě velmi důležitá, protože obsahuje různé kombinace cizích klíčů mezi autory a knihami.

Eloquent modely


class Autor
{
    public function books()
    {
       return $this->belongsToMany(Book::class);
    }
}

class Book
{
    public function authors()
    {
        return $this->belongsToMany(Author::class);
    }
}

Pro definování vazby many to many je potřeba použít v obou modelech metodu belongsToMany().  Pokud by jste chtěli vazby hned vyzkoušet, zjistili by jste, že vše je již nastavené. Jak jste si už určitě všimli, nikde jsem nedefinoval naši pivot tabulku a přesto vše funguje. To je proto, že Eloquent určí název propojovací tabulky na základě jmen modelů a to navíc v abecedním pořadí. To znamená, že Eloquent předpokládá, že propojovací tabulka se bude jmenovat autor_book. Pokud by jste chtěli tabulku pojmenovat jinak, např. jako books_authors_pivot, pak je potřeba tento název definovat jako druhý parametr:


return $this->belongsToMany(Book::class, 'books_authors_pivot');

Dále můžete upravit názvy sloupců, které se v pivotu využívají jako cizí klíče. Třetí argument je cizí klíč modelu, ve kterém definujeme vazbu, zatímco čtvrtý parametr je název cizího klíče modelu, ke kterému se připojujeme:


return $this->belongsToMany(Book::class, 'books_authors_pivot', 'author_id', 'book_id');

Získávání záznamu

Díky tomu, že jsme si definovali správně vazby, můžeme získat záznamy stejně, jako by se jednalo o vazbu 1:n. Opět musíme počítat s tím, že se nám vrátí kolekce záznamů:


// ziskame knihy autora
$author->books

// ziskame autory knihy
$book->authors

Pokud bychom potřebovali získat informaci z pivot tabulky, pak stačí jít skrze pivot atribut:



$author = Author::find(1);

foreach ($author->books as $book) {
    echo $book->pivot->created_at;
}

Eloquent defaultně předpokládá, že v propojovací tabulce jsou pouze sloupce s cizími klíči. Pokud pivot tabulka obsahuje další sloupce, je potřeba definovat je již v relaci:


return $this->belongsToMany(Book::class)->withPivot('column1', 'column1');

Vlastní název pivot atributu

Jak jsem dříve psal, atributy z propojovací tabulky získáme pomocí atributu pivot. Nicméně je možné zvolit si vlastní název, které bude lépe odpovídat účelu v aplikaci. Například uživatelé se můžou přihlásit k odběru novinek. V tom případě bude vhodnější, abychom propojovací atribut přejmenovali na subscriptionnamísto pivot. Toho dosáhneme pomocí metody as()definovanou ve vazbě:


return $this->belongsToMany(Newsletter::class)->as('subscription');

Uložení záznamu

Pro uložení záznamu můžeme využít dvě metody - attach() a sync(). Metoda attach()pouze přidává další vazbu, nijak neřeší, jestli daná kombinace v pivotu již existuje a zároveň se nijak nestará o ostatní kombice. Naopak sync()udělá to, že pro daný model smaže všechny vazby a přidá nové. Pokud bychom chtěli, aby metoda původní vazby nemazala (a jen přidala nové), pak je potřeba nastavit druhý parametr jako false.


// vytvoreni vazby mezi autorem a knihami
$author->books()->attach([
   $book1->id,
   $book2->id,
]);

// nebo pouzijeme metodu sync() abychom zabranili duplicitnim vazbam
$author->books()->sync([
   $book1->id,
   $book2->id,
]);

// vytvoreni vazby mezi knihou a autory
$book->authors()->attach([
   $author1->id,
   $author2->id,
]);

// nebo opet sync() ale tentokrat nechceme, aby metoda smazala puvodni vazby - jen pridala nove
$book->authors()->sync([
   $author1->id,
   $author2->id,
], false);

Vazba has-may-through

Tato metoda nám poskytuje velmi jednoduchou zkratku, jak se dostat ke vzdálené relaci pomocí propojovací tabulky, která ale neslouží jako pivot v předešlém vztahu. Například mějme uživatele z různých zemí a chtěli bychom zobrazit všechny příspěvky, jejichž uživatelé jsou z Německa. Tato vazba nám tento úkol velmi usnadní.

Migrace


Schema::create('users', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->integer('country_id')->unsigned()->index()->nullable();
    $table->foreign('country_id')->references('id')->on('countries');
});

Schema::create('posts', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->integer('user_id')->unsigned()->index()->nullable();
    $table->foreign('user_id')->references('id')->on('users');
});

Schema::create('countries', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
});

Uživatel má informaci o tom, z jaké je země. Článek má informaci o tom, kdo je jeho autor. Nicméně článek už nemá žádnou informaci, z které země je jeho autor, respektive uživatel.

Eloquent modely

Pro jednoduchost si zobrazíme pouze model Country, jelikož vytvořit modely pro uživatele i příspěvky by již neměl být problém:


class Country extends Model
{
    /**
     * ziskani vsech prispevku od uzivatelu dane zeme
     */
    public function posts()
    {
        return $this->hasManyThrough(Post::class, User::class);
    }
}

Protože tabulka postsneobsahuje sloupec country_id, vazba hasManyThrough poskytuje přístup k příspěvkům z dané země pomocí $country->posts. Eloquent tento dotaz vykoná tak, že se prvně zkontroluje sloupec $country->postsv propojovací tabulce users. Jakmile získá tyto záznamy, tak podle id uživatelů se najdou záznamy v tabulce posts. První argument v metodě  hasManyThrought je tedy název konečné tabulky, ke které se chceme dostat, a druhý argument je název propojovací tabulky.

Získávání záznamu

Vraťme se k našemu úkolu, tedy chceme zobrazit všechny příspěvky, které napsali uživatelé z Německa. Díky definování vazby můžeme výsledek získat velmi jednoduše:


$germans = Country::whereName('germany')->first();
$germans->posts();

Není vám něco jasné, mám někde chybu nebo chcete přidat svůj názor na článek? Napište do komentářů pod článkem.