Может войдёшь?
Черновики Написать статью Профиль

Основы Laravel 5: Многие-ко-многим (с тегами)

перевод Основы Laravel 5 Laracasts

Это перевод видео-урока с Laracasts, серия Laravel 5 Fundamentals, урок №21Many to Many Relations (With Tags) от . Перевод обновлён . Опечатка? Выдели и нажми Ctrl+Enter.

(0:00)
Знаете, что я думаю? Если я строю настоящий блог, ну да, это здорово, что я могу создавать статьи и публиковать их, но как только у меня есть десятки и десятки статей, было бы неплохо дать пользователю возможность поиска среди них. Да, он мог бы искать их и мы узнаем о поиске в следующем уроке, но как насчёт простых меток или тегов? Если допустим я помечу эту как, personal (личный), то у пользователя должна быть возможность просмотреть все статьи с этим тегом.
Так как же именно нам это сделать?

(0:30)
Или, другими словами, как настроить нашу базу данных и Eloquent, чтобы позволить нечто подобное? И ответ в том, чтобы использовать то, что мы называем отношением многие-ко-многим (many to many). Позвольте привести пример. Я перейду к моей модели Eloquent для статьи, и вы уже узнали о некоторых отношениях.
В этом случае статья написана пользователем, так что мы могли бы сказать, что статья принадлежит пользователю — PHPbelongsTo(). Мы будем ссылаться на это, как на отношение PHPhasMany().

(1:00)
Пользователь имеет много статей и каждая статья, конечно же, принадлежит одному пользователю. Но, как вы можете себе представить, для некоторых вещей это не совсем так очевидно. Например, если мы хотим думать о тегах, то да, было бы здорово, если бы я мог выбрать коллекцию всех тегов, которые связаны с данной статьей. Но можем ли мы сказать, что тег принадлежит статье? Или, другими словами, будет ли это работать?

PHP
return $this->belongsTo('App\Tag');

Давайте представим, что у нас есть какой-то класс PHPTag, даже если мы его ещё не создали.

(1:30)
Так что остановитесь, если вам нужно, и задумайтесь, будет ли это иметь смысл? Итак, у пользователя есть много тегов и значит ли это, что каждый тег будет принадлежать пользователю?
Ну, нет, это не совсем верно, не правда ли? Пользователь может иметь много тегов, но тег может быть применён ко многим пользователям, не так ли? Это имеет смысл. Значит это отношение неправильное.

(2:00)
Вместо этого, как я сказал, мы хотим отношение PHPbelongsToMany() и чтобы разрешить такого типа отношение, нам нужна сводная таблица (pivot table). Думайте о сводной таблице как о справочной таблице. В ней просто два столбца, один столбец в этом случае идентификатор статьи и ещё один столбец – это идентификатор тега. Мы можем представить это вот так:

PHP
return $this->belongsToMany('App\Tag');

Мы хотим отношение PHPbelongsToMany(). Давайте создадим наш комментарий docblock и скажем:

* Получить теги, связанные с данной статьёй.

(2:30)
ОК, это выглядит хорошо. Теперь, почему бы нам не пойти дальше и не создать эту модель Eloquent?

shphp artisan make:model Tag

И готово. Итак, просто чтобы освежить вашу память, мы размещаем этот файл прямо в каталоге app, так что вот он – Tag.php.
Далее, мы ещё не создали миграцию для таблицы тегов и для сводной таблицы. Мы можем сделать это прямо сейчас:

shphp artisan make:migration create_tags_table --create=tags

(3:00)

(для построения шаблона мы создаём таблицу tags). Хорошо, давайте подумаем об этом.
Мы знаем, что у тега должен быть id с автоприращением (autoincrementing). Вы хотите временные штампы? Мой инстинкт – всегда держат их, если только у вас нет веских причин, почему они не нужны. Так что мы сохраним их, но я хочу также строку для фактического имени тега:

PHP
$table->string('name');

Далее нам нужна эта сводная таблица, чтобы мы могли бы для неё создать ещё один новый класс миграции.

(3:30)
Или почему бы нам просто не разместить его здесь, для скорости?
На этот раз мы его пропишем и создадим сводную таблицу, которая следует конкретному соглашению о присвоении имен. Это будут две таблицы, которые мы пытаемся соединить, в единственном числе. Так в нашем случае у нас есть articles и tags. Варианты их написания в единственном числе будут article и tag, мы соединим их подчеркиванием и в алфавитном порядке, так что в этом случае A предшествует T, поэтому правильным будет article_tag.

(4:00)
Если например, у вас есть ещё таблицы users и roles, то в этом случае имя сводной таблицы будет role_user, в единственном числе и в алфавитном порядке. ОК, это имеет смысл.
Так что давайте возьмём и создадим её:

PHP
Schema::create('article_tag', function(Blueprint $table)
{
    
$table->integer('article_id');
    
$table->integer('tag_id');
});

мы передадим здесь PHPBlueprint. По сути, что нам действительно нужно, это колонка целых чисел для id статей, и затем ещё одна специально для id тегов.

(4:30)
Так мы соединяем обе таблицы, мы создаем эту новую запись в новой таблице, которая связана со статьёй и тегом для неё. Дальше, если вы думаете, что это полезно, мы могли бы добавить временные штампы:

PHP
$table->timestamps();

и по сути, это действительно всё, что вам нужно. Но мы могли бы сделать ещё пару вещей. Например числа должны быть без знака, и они всегда будут положительными числами. Далее, вы можете захотеть добавить для них индекс, если вы предполагаете, что будет много поисковых запросов и т.п., тогда добавление индекса может быть хорошим выбором:

PHP
$table->integer('article_id')->unsigned()->index();
$table->integer('tag_id')->unsigned()->index();

(5:00)
И, наконец, что насчёт тех ситуаций, когда мы удаляем статью? Хорошо, мы удаляем статью с id равным 5, но у нас потенциально может ещё быть его запись в сводной таблице. Так что же мы хотим делать в таких ситуациях? Давайте создадим внешний ключ вот таким образом. И мы уже рассматривали это подробно:

PHP
$table->foreign('article_id')->references('id')->on('articles')->onDelete('cascade');

(5:30)
И наконец, что мы должны сделать, если мы удаляем статью? Давайте пойдём каскадом вниз и также удалим соответствующие записи в сводной таблице.
И потом я сделаю то же самое здесь для tag_id. Он ссылается на колонку id в таблице tags:

PHP
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');

так что надеюсь, это натолкнёт вас на некоторые мысли, но да по своей сути вы просто добавляете колонки article_id и tag_id.

(6:00)
И в конце концов, если нам нужно сделать откат назад, то мы удалим таблицу tags, и можем также удалить нашу сводную таблицу:

PHP
Schema::drop('tags');
Schema::drop('article_tag');

Хорошо, почему бы нам не пойти дальше и не сделать миграцию нашей базы данных?

shphp artisan migrate

ОК, это сделано, почему бы нам не вернуться к... в нашей модели PHPArticle, отношение получает теги, связанные со статьей, и если задуматься, то для нашей модели PHPTag мы хотим обратное действие.
Так что, если у меня есть объект PHP$tag, то было бы здорово сделать что-то вроде этого:

PHP
$tag->articles

(6:30)
дайте мне все статьи, связанные с данным тегом, то есть то же действие, но наоборот. Так что мой метод будет:

PHP
public function articles()
{
    return 
$this->belongsToMany('App\Article');
}

Да, и кстати, быстрый совет, мы следуем тому соглашению для имени сводной таблицы, но, возможно, вы уже работаете с чем-то или, может быть, вы просто не хотите следовать соглашению. Это нормально. Просто передайте его здесь в качестве второго параметра.

(7:00)
Так что если вы хотите сказать tags_pivot или как там вы захотите это назвать, оно будет рассматриваться как имя таблицы, а затем вы могли бы также передать 3-й или 4-й параметр, с именами, которые вы хотите дать им для работы.
Так что если у вас есть нестандартное имя для идентификатора статьи, вроде article_identifier, или что-то типа того, что-то такое же странное, то вы могли бы представить его прямо здесь:

PHP
return $this->belongsToMany('App\Article'"tags_pivot""article_identifier");

и то же самое верно и для tag_id. Затем, наконец, я настрою мой docblock и буду хорошим программистом:

(7:30)

* Получить статьи, связанные с данным тегом.

ОК, я думаю мы готовы к настройке.
Перед тем как попробовать работу кода в браузере, я только хочу показать вам использование shphp artisan tinker. Это лучше проиллюстрирует, как здесь всё соединяется. Хорошо, итак:

shphp artisan tinker

и почему бы нам не начать с создания нового тега?

PHP
$tag = new App\Tag;

Хорошо, мы хотим установить имя, этот тег назовём personal («личное»):

PHP
$tag->name 'personal';

и теперь, наконец, я думаю, что у нас есть всё что нужно, так что мы можем сохранить его:

PHP
$tag->save();

(8:00)
Теперь, если я скажу:

PHP
App\Tag::all()->toArray();

и дальше, что кстати классно, мы могли бы сказать:

PHP
App\Tag::lists('name');

мы могли бы использовать метод PHPlists(), о котором мы ещё не говорили. Так что, если мы запустим это, нам будет выдан массив всех значений из данного столбца. В этом случае у нас есть лишь один тег, так что это всё, что мы получаем.
Это может быть очень полезно, когда вам нужно заполнить поле выбора select, тогда вы можете сделать один запрос PHPApp\Tag::lists('name') и затем передать вашему элементу select все различные варианты значения.

(8:30)
Хорошо, но в любом случае двигаемся дальше, у нас есть тег, и теперь я хочу связать статью с тегом. Мы получаем первую статью, и нам не важно какую именно, так что я сделаю:

PHP
$article App\Article::first();

Давайте посмотрим, с чем мы здесь имеем дело:

PHP
$article->toArray();

ОК, статья Джона должна иметь тег personal, так что просто чтобы убедиться в этом, давайте будем ссылаться на наше созданное отношение PHPtags().

(9:00)
Так что я скажу:

PHP
$article->tags()->attach(1);

правда на этот раз, если мы хотим вставить модель на отношение многие-ко-многим, мы можем использовать метод PHPattach(). И думайте об PHPattach() как о сводной таблице, в которой есть крючки, так что мы будем присоединять значения от каждой из этих моделей в новую запись сводной таблицы. И в этом случае мы должны лишь передать идентификатор тега, который мы хотим присоединить.

(9:30)
Я не знаю, какой он там точно, но он был первым, что мы создали, поэтому должен быть равным 1. Ага! Это не сработало, но на самом деле это будет для нас хорошим уроком, потому что даст вам пример запроса SQL, который выполнялся за кадром. Во-первых, причина, почему всё не сработало, в том, что у нас в таблице есть временные штампы, но на самом деле мы должны сказать Laravel что они у нас есть, по умолчанию их там не ожидается, так что мы должны их, так сказать, подключить.
Но более конкретно заметьте, что мы запускаем простой запрос:

(10:00)
вставить в нашу сводную таблицу идентификатор статьи и идентификатор тега, которые являются для данного случая равны 1 и 1. Так как же нам здесь исправить эту маленькую проблему? Ну, это очень просто. Прямо здесь я могу сказать with и, обратите внимание, PHPwithTimestamps() – это именно то, что мы хотим:

PHP
return $this->belongsToMany('App\Tag')->withTimestamps();

Это всё, что мы должны сделать. Так что давайте вернёмся, я выйду, и мы снова запустим:

shphp artisan tinker

Всякий раз, когда вы вносите такие изменения, вам нужно выходить и перезагружать Artisan.

(10:30)
Хорошо, ещё один раз:

PHP
$article App\Article::first();

И теперь мы скажем:

PHP
$article->tags()->attach(1);

(прикрепить новый тег к данной статье, где tag_id равен 1). Сохраним его, и мы ничего не получаем, но я уверен, что это сработало. Давайте подумаем, почему бы нам не использовать общий класс DB в данном случае?

PHP
DB::select('select * from article_tag');

и готово, одна запись в нашей сводной таблице.

(11:00)
Так что теперь посмотрим. Хорошо, у нас всё ещё есть наш объект PHP$article, не так ли?

PHP
$article->toArray();

Ага. Теперь, если мы скажем:

PHP
$article->tags->toArray();

готово, всё работает! И вот быстрое примечание. Если мы ещё раз запустим PHP$article->toArray(), Laravel достаточно умён, чтобы сохранить результат, так что он не будет выполнять тот же SQL-запрос каждый раз. Мы его уже получили. Так что, мы могли бы сказать:

PHP
$article->tags->lists('name');

(11:30)
и это даст нам массив всех тегов для данной статьи.
Давайте теперь сделаем обратное, скажем:

PHP
$tag App\Tag::first();

ОК, у нас тот же тег:

PHP
$tag->toArray();

и теперь мы хотим получить все статьи, которым был присвоен этот тег. Так что мы скажем:

PHP
$tag->articles->toArray;

И готово. В этом случае просто массив с одной статьёй. ОК, вы знаете, я думаю, что мы уже рассмотрели много всего здесь.
Давайте закончим на этом первую часть.

(12:00)
Но, как вы можете себе представить, нам ещё много всего предстоит сделать. Так что убедитесь, что вам понятно всё что мы рассмотрели в этом видео. Вам ясна идея о том, что такое сводная таблица. И вы понимаете, как отразить эти отношения с помощью Eloquent (вот так), где мы создаём отношение PHPbelongsToMany().
ОК, продолжайте с этим экспериментировать, и далее, в следующем видео во 2-й части мы на самом деле подключим всё это к нашему пользовательскому интерфейсу.

Как вы считаете, полезен ли этот материал? Да Нет

Комментарии (3)

iy_expert

Отлично! Спасибо за работу.
Но как быть если у нас не только статьи, а еще и новости и каталог какой то, и к ним тоже можно к каждому элементу добавлять теги.
Понимаю что это полиморфные связи, но хотелось бы более подробно. По документации пока не все понимаю в этом вопросе(.

Volodec

Laravel 5.3, метод PHPlists() удалён в пользу метода PHPpluck().

Anzor

Для полиморфных связей лучше использовать конструктор запросов. К примеру из действующего проекта:

PHP
$tags = ['tag1','tag2','tag3'];

file_card::selectRaw('file_cards.*')
                                ->
join('file_card_key_word''file_card_key_word.file_card_id''=''file_cards.id')
                                ->
join('key_words''file_card_key_word.key_word_id''=''key_words.id')
                                ->
whereIn('key_words.name'$tags )
                                ->
groupBy('file_cards.id')
                                ->
get();

выбираются все статьи по тегам исключая дубликаты

Написать комментарий

Разметка: ? ?

Авторизуйся, чтобы прокомментировать.