#Волшебный Eloquent. ##Дисклеймер Данный материал абсолютно не претендует на уникальность, и не является попыткой открыть для кого-то Америку. Все ниже изложенное (прямо или косвенно) можно легко почерпнуть из официального мануала. А для чего же оно тогда написано? Попытка подать информацию в чуть более развернутом виде, систематезировать собственные знания, и снять острый приступ графоманства. Если это вдруг окажется кому-то полезным, то мне будет приятно.
##Введение
TL;DR
Так уж сложилось, что слоняясь по "интернетам", в поисках сообщников в ограблении банка единомышленников в изучении framework'a Laravel, я забрел в чат хоть и праздно прозябающего, но (стараниями Алексея) живого и дружелюбного Cообщества, и плотно там осел. А через какое-то время заметил, что отвечаю на чьи-то вопросы гораздо чаще, чем задаю их. Хотя мой замысел был иной: изначально, я хотел добраться до "знающих людей" и, как вампир, высосать через чат все их "знания тайной силы"... Но не тут-то было. Как оказалось, таких же как я "акакиев", там хватало и до меня и без меня. Ну что же делать? Будем решать вопросы.
Отвечая на разные вопросы, я обратил внимание на тот факт, что большинство проблем связаны не с пониманием устройства среды Laravel в частности, а с пониманием принципа устройства данных вообще. Это касалось даже не заумных и действительно сложных для понимания вещей с фасдами и поставщиками в различных пространствах имен, а простых, на первый взгляд, роутов, контролллеров и моделей. О последних здесь, как раз, и пойдет речь.
##Ребят, а куда я попал?.. Ох, и классно же тут у вас!
Когда я впервые читал мануал по "Ларе", я думал: "хм, это прикольно", "ну, и это так ничего". Ну то есть, я вовсе не был чем-то шокирован. Про Composer
я знал и раньше, Artisan
мне не казался чем-то уж особенным. Не было для меня какой-то киллер-фичи, из-за которой бы я слез с CodeIgniter'a. Все изменилось, когда я добрался до спецификации моделей... То, как устроены модели в Laravel - это просто магия и волшебство! Но как и со всякой магией, с ней нужно уметь обращаться.
###Eloquent
Если забить в гугл-переводчик слово eloquent
, то мы увидим, что наиболее употребляемым переводом этого слова является красноречивый
. Но я не думаю, что создатели Laravel вкладывали именно этот смысл в название своей ORM. Так сложилось, что для русскоязычного мира "красноречивый" - почти синоним слова (простите) "пиздабол". А это совсем не то, что хотелось бы слышать об изучаемом инструменте.
Другой, менее употребляемый, вариант перевода - Выразительный
. И он подходит куда больше. Мы часто слышим: "выразительный взгляд", "выразительный портрет", "выразительная речь". Выразительно - это то, что не требует пояснения, дополнительных комментариев - оно понятно само по себе.
Но чтобы ощутить всю эту выразительность, применительно к коду в Laravel, нужно соблюдать некоторые несложные правила.
###Строгое именование (строгая нотация)
Один программист назвает модель данных статей PostModel
, другой - PostsModel
, десятый пишет в змеиной нотации Post_Model
.
В Laravel простая самостоятельная модель данных для таблицы posts
может быть создана приблизительно так:
Шаг первый:
<?php
class Post extends Eloquent{
}
Шаг второй:
Осознать, что больше ничего делать не нужно, и все уже и так работает.
Нет, я серьезно. Это все - Вы уже можете работать со своей моделю и делать с ней все, что душе угодно: выбирать данные, записвыать, обновлять, фильтровать по условиям, разбивать постранично...
Но, как всегда, есть одно "но". Для того, чтобы эта (и другая подобная) "магия" работала, нужно соблюдать соглашение строго именования:
- Одна модель данных соответствует лишь одной таблице.
- Модель данных называется в единственном числе, в
ВерхнейВерблюжейНотации
:Category
,ShopCategory
- Таблица данных называется во множественном числе в
нижней_змеиной_нотации
:categories
,shop_categories
- В отношениях типа "один ко многим/одному", Название полей, являющихся внешними ключами, ссылающимися на определитель во внешней таблице, пишутся в
нижней_змеиной_нотации
, единственном числе по имени вызывающего метода и постфиксом_id
:categоry_id
,product_id
. - Пивотные (стержневые) таблицы, выражающие отношение "Многие ко многим", называются в единственном числе, нижней змеиной нотации по именам связанных моедлей, в алфавитном порядке:
role_user
, но неuser_role
, . - В отношениях типа "многие ко многим", внешние ключи называютсяв единственном числе, нижней змеиной нотации, по именам моделей и постфиксом
_id
. - Таблица данных должна содержать поля:
id
(int, unsigned, auto-increment)created_at
(timestamp) опционально, при использовании таймштамповupdated_at
(timestamp) опционально, при использовании таймштамповdeleted_at
(timestamp) опционально, при использовании трейта "мягкого удаления"
- При создании полиморфических связей, поля типа морфемы и (условного) внешнего ключа морфемы, должны называться идентично по имени морфирующего метода и заканчиваться постфиксами "_type" и "_id"(или другого референсного поля-определителя), кроме того, они должны иметь тип данных
varchar
(или другой string) иinteger
соответствнно:morph_type
,morph_id
- При посеве данных, в полиморфических таблицах, поле типа морфемы должно заполняться названием связанной модели буквально, включая путь к пространству имен:
User
,Shop\Product
Соблюдение всех этих правил не является абсолютно обязательным для работы с Eloquent, более того - в "Ларе" предусмотрено все необходимое для обхода этих правил. Так, что именование полей, таблиц и моделей по своему вкусу не будет "сверх-геморроем". Но, соблюдая их (правила), Вы сможете избежать ненужных уточнений и добавить немного волшебства. Кроме того, это поможет в общении с другими разработчиками под Laravel, и людьми его изучающими.
Немного о внешних ключах:
Строго говоря, Laravel не требует (но и не запрещает) буквального наличия внешних ключей на полях таблиц, как такового; и ему вполне достаточно правильно именованных полей, или точного их указания в связях моделей. Благодаря этой своей особенности, "Лара" одинаково хорошо работает как с реляционными, так и нереляционными типами таблиц. Здесь и далее, говоря "внешний ключ", я не буду подразумевать буквального его наличия в таблице - я буду иметь ввиду поля в таблицах, по которым будет реализовываться связь моделей.
###Построение отношений Здесь я напомню какие типы связей бывают, и то как они выражаются в Eloquent.
Итак, типы связей бывают:
- Один к одному
- Многие к одному
- Многие ко многим
- Полиморфические
Первые три связи могут быть реализованы в реляционных базах данных, а последняя - лишь эмулируется.
####Один к одному
В школе есть ученики, каждый год приходят новые ученики, старые уходят. Текучка, в общем. За каждым учеником в школе может быть закреплен шкафчик. У шкафчиков есть какие-то свои параметры: инвентарный (не порядковый) номер или степень износа. В каждый момент времени за одним учеником может быть закреплен один шкафчик. Шкафчиков меньше, чем учеников, по этой причине, на всех не хватает, и они всегда заняты.
Налицо связь один к одному. Внешний ключ, соответственно, будет в таблице шкафчиков, потому как шкафчик принадлежит ученику, а не наоборот.
Таблица students
:
id | name |
---|---|
1 | Василий |
2 | Геннадий |
3 | Евлампий |
Таблица cabinets
id | inventory_number | student_id |
---|---|---|
1 | AS-23 | 3 |
2 | AS-65 | 1 |
3 | BG-15 | 2 |
Связи в моделях Student
и Cabinet
будут обозначены соответственно:
class Student extends Eloquent{
public function cabinet()
{
return $this->hasOne('Cabinet');
}
}
class Cabinet extends Eloquent{
public function student()
{
return $this->belongsTo('Student');
}
}
Эти модели связываются по внешнему ключу в поле student_id
в таблице cabintets
. Внешний ключ автоматически (на уровне ORM) завязывается на поле id
таблицы students
. Метод hasOne()
, говорит нам о том, что объект Student
может иметь (впрочем, может и не иметь) лишь один подчиненный объект Cabinet
.
Не смотря на то, что связь "один к одному" звучит как равноправная, на самом деле она такой не является. В данном конкретном случае объект Cabinet
подчинен/принадлежит объекту Student
. Это означает, что именно в таблице cabinets
мы будем искать внешний ключ на таблицу users
, а не наоборот.
Если перевести belongs to
на русский язык, то мы получим принадележит к
, а has one
= имеет один
соответственно. Эти два метода моделей и выражают отношения между моделями.
Ученик (Student
) имеет один (hasOne()
) Шкафчик (Cabinet
).
Шкафчик (Cabinet
) принадлежит (belongsTo()
) Ученику (Student
).
Я думаю, что это вполне Выразительно.
####Многие к одному
Повторим то же самое для классов в школе и учеников в классах.
В школе есть классы (я не имею ввиду аудитории, я имею ввиду группы в потоке). В одном классе много учеников, один ученик принадлежит лишь к одному классу.
Многие к одному. Приведем таблицы.
Таблица classes
:
id | title |
---|---|
1 | 11А |
2 | 6Б |
3 | 3В |
Таблица students
:
id | name | class_id |
---|---|---|
1 | Василий | 1 |
2 | Геннадий | 3 |
3 | Евлампий | 2 |
И модели со связями:
class Class extends Eloquent{
public function students()
{
return $this->hasMany('Student');
}
}
class Student extends Eloquent{
public function studentClass()
{
return $this->belongsTo('Class');
}
}
спасибо plakhin за указание на опечатки
Как можно заметить, этот код немногим отличается от предыдущего. Единственное отличие - метод hasMany()
вместо hasOne()
. Разница между ними лишь в том, что отношение выраженное через hasOne()
при выборке ищет один единственный объект и возвращает его. В то время как hasMany()
возвращает коллекцию объектов, даже если в выборку попадет один единственный результат, или результатов не будет вообще - в этом случае коллекция будет иметь один объект или будет пуста, соответственно.
Стоит отметить, что я называю эту связь именно "Многие к одному", а не "Один ко многим" (последнее выражение встречается гораздо чаще и вводит людей в заблуждение). Дело в том, что именно ученики принадлежат к классам а не наоборот. К слову сказать, связь "Один ко многим" также может существовать, но ввиду избыточности (требуется дополнительная таблица) и слабого логического обоснования, она используется крайне редко, если вообще используется. За все время, что я изучаю структуры данных, мне еще ни разу не приходилось с ней столкнуться. Связь "Многие к одному" вполне достаточна, для всех юзкейсов выражающих отношения с общей вершиной (я имею ввиду Adjacency. Это термин из теории древовидных структур, о нем мы поговорим в другой раз).
Итого:
Класс(Class
) имеет много (hasMany()
) учеников (Student
).
Ученик(Student
) принадлежит к (belongsTo()
) классу (Class
).
Стоит обратить внимание, что метод belongsTo()
пишется в подчиненных моделях, вне зависимости от того, является ли их отношение c подчиняющией моделю "Один к одному" или "Многие к одному". Как я уже писал выше, это поиск единственного соответствия по внешнему ключу.
###Многие ко многим
Немного о пивотах
В то время, как связи "Один к одному" и "Один ко многим" выражают подчиненность объектов, связь "Многие ко многим", является обоюдной и равноправной. Это не означает, что обе таблицы будут иметь внешние ключи (это было бы неудобно, ведь тогда пришлось бы дублировать записи). Вместо этого, внешние ключи выносятся в третью - "пивотную" (стержневую) таблицу. Пивотная таблица не обязана (но может) иметь собственную модель. Как правило, она не несет в себе никаких данных кроме связи между моделями. Реже, она может содержать идентификатор связи, некоторые данные о порядке сортировки (приоритете вывода), или уровне вложенности для таблиц-замыканий (о таблицах замыканий будет отдельный разговор, когда мы будем обсуждать древовидные структуры). Но в большинстве случаев, она содержит лишь два поля со внешними ключами.
В школе есть кружки дополнительных занятий (танцы, рисование, самбо). Каждый ученик может посещать несколько кружков или не посещать их вообще. Каждый кружок может обучать множество учеников. Назовем эти кружки группами по интересам. Или просто группами (
Group
).
Итак, для выражения связи "Многие ко многим", понадобится три таблицы:
students
groups
group_student
- пивот
пивотная таблица должна содержать соответствующие внешние ключи: student_id
и group_id
.
Таблица students
:
id | name |
---|---|
1 | Василий |
2 | Геннадий |
3 | Евлампий |
Таблица groups
:
id | title |
---|---|
1 | Рисование |
2 | Плавание |
3 | Сделай сам |
Таблица group_student
:
student_id | group_id |
---|---|
1 | 2 |
1 | 3 |
1 | 1 |
2 | 1 |
Модели данных, в этом случае будут выглядеть так:
class Student extends Eloquent{
public function groups()
{
return $this->belongsToMany('Group');
}
}
class Group extends Eloquent{
public function students()
{
return $this->belongsToMany('Student');
}
}
Ученик (Student
) принадлежит ко множеству (belongsToMany()
) Групп (Group
).
Группа (Group
) принадлежит множеству (belongsToMany()
) Учеников (Student
).
Согласно примеру из приведенных выше таблиц: Василий посещает все три кружка, а Геннадий ходит лишь на рисование.
###Полиморфические (полиморфные) связи
Как я писал выше, полиморфические связи не поддерживаются реляционными структурами данных и лишь выражают логическое отношение между моделями данных на уровне понимания (а в случае с Eloquent и на уровне ORM). По сути, наличие подобных отношений в моделях данных означает сниженный (намеренно или по недосмотру) уровень абстракции самих моделей данных и/или намеренное упрощение структуры данных для понимания. Кроме того, при попытке избавится от полиморфической связи и ввести дополнительный слой абстракции, в базе данных появляется множество таблиц (а в коде - множество моделей), отношения между которыми не всегда очевидны и поняны. Так или иначе, полиморфические связи бывают удобны, и нужно уметь их использовать. В Eloquent предусмотрен функционал для работы с такими связями.
В школе есть библиотека, в библиотеке выдают книги. Книги могут быть выданы как ученику, так и учителю. Одна книга одновременно может принадлежать одному человеку, один человек может иметь много книг.
К сожалению, на этапе проектирования базы данных, мы не предусмотрели наличия подобного функционала. И у нас нет модели "Человек", и отдельной модели "Роль" - как было бы "по уму".
Сейчас у нас есть модель учителя и модель ученика. Переделывать всю базу данных и переписывать код нет никакого желания. Как же быть? Все верно - нужно ввести полиморфическую связь. Допустим, у нас есть модель Book
и соответствующая ей таблица books
:
id |
title |
student_id |
---|---|---|
15 | Вий | 22 |
13 | Отцы и дети | 14 |
32 | Пушкин. Поэмы | 19 |
Сейчас мы выдаем книги только ученикам. Как же сделать так, чтобы можно было выдавать книги и учителям? Допустим, что есть модель учителя Teacher
и соответствующая таблица teachers
. Мы могли бы ввести дополнительное поле teacher_id
в нашу таблицу:
id |
title |
student_id |
teacher_id |
---|---|---|---|
1 | Вий | 22 | null |
13 | Педагогическое пособие | null | 25 |
А если у нас появятся другие модели, к которым можно отнести книги? Снова добавлять поля? Бред. Мы могли бы выделить отношения между книгами и учителями в отдельную пивотную таблицу book_teacher:
book_id |
teacher_id |
---|---|
15 | 22 |
13 | 25 |
32 | 11 |
Но тогда при движении книги (я имею ввиду передачу от одного объекта другому) пришлось бы отслеживать состояние и других таблиц, чтобы книга не оказалась у двоих сразу или вообще пропала. Это все неправильно... А правильно будет вот так:
таблица books
:
id |
title |
holder_type |
holder_id |
---|---|---|---|
1 | Вий | Student | 17 |
13 | Выстрел | Teacher | 25 |
В данном случае, поле holder_id
не завязано на определенное поле определенной таблицы, но оно будет привязано к соответствующему полю соответствующей таблицы, на основании значения поля holder_type
. То есть, каждый экземпляр объекта Book
может принадлежать как объекту класса Student
, так и объекту класса Teacher
(но не обоим сразу), в заисимости от значения поля holder_type
. Модели данных в этом случае будут выглядеть приблизительно так:
class Book extends Eloquent {
public function holder()
{
return $this->morphTo();
}
}
class Student extends Eloquent {
public function books()
{
return $this->morphMany('Book', 'holder');
}
}
class Teacher extends Eloquent {
public function books()
{
return $this->morphMany('Book', 'holder');
}
}
вот такая она - "Полиморфия".
###Другие виды связей Помимо основных, уже перечисленных видов связей, в "Ларе" есть необычные "Сквозные" виды связей. Но они требуют отдельного разговора.
###Заключение
Пока это все, что хотелось рассказать о построении связей в моделях данных Laravel. В следующем материале я хочу рассказать о работе с уже построенными моделями, правильной выборке и ошибках, которые часто допускаются (Ковырял пару чужих проектов на ларе).
#####З.Ы. Чтобы узнать о том, как правильно указывать поля в отношенияx без соблюдения строгой нотации - курим мануал.
@Baksalyar, спасибо за корректорскую работу.