SergeyMiracle
11/14/2014 - 10:51 AM

Построение моделей

Построение моделей

#Волшебный 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{
}

Шаг второй:
Осознать, что больше ничего делать не нужно, и все уже и так работает.

Нет, я серьезно. Это все - Вы уже можете работать со своей моделю и делать с ней все, что душе угодно: выбирать данные, записвыать, обновлять, фильтровать по условиям, разбивать постранично...


Но, как всегда, есть одно "но". Для того, чтобы эта (и другая подобная) "магия" работала, нужно соблюдать соглашение строго именования:

  1. Одна модель данных соответствует лишь одной таблице.
  2. Модель данных называется в единственном числе, в ВерхнейВерблюжейНотации: Category, ShopCategory
  3. Таблица данных называется во множественном числе в нижней_змеиной_нотации: categories, shop_categories
  4. В отношениях типа "один ко многим/одному", Название полей, являющихся внешними ключами, ссылающимися на определитель во внешней таблице, пишутся в нижней_змеиной_нотации, единственном числе по имени вызывающего метода и постфиксом _id: categоry_id, product_id.
  5. Пивотные (стержневые) таблицы, выражающие отношение "Многие ко многим", называются в единственном числе, нижней змеиной нотации по именам связанных моедлей, в алфавитном порядке: role_user, но не user_role, .
  6. В отношениях типа "многие ко многим", внешние ключи называютсяв единственном числе, нижней змеиной нотации, по именам моделей и постфиксом _id.
  7. Таблица данных должна содержать поля:
  • id(int, unsigned, auto-increment)
  • created_at(timestamp) опционально, при использовании таймштампов
  • updated_at(timestamp) опционально, при использовании таймштампов
  • deleted_at(timestamp) опционально, при использовании трейта "мягкого удаления"
  1. При создании полиморфических связей, поля типа морфемы и (условного) внешнего ключа морфемы, должны называться идентично по имени морфирующего метода и заканчиваться постфиксами "_type" и "_id"(или другого референсного поля-определителя), кроме того, они должны иметь тип данных varchar(или другой string) и integer соответствнно: morph_type, morph_id
  2. При посеве данных, в полиморфических таблицах, поле типа морфемы должно заполняться названием связанной модели буквально, включая путь к пространству имен: User, Shop\Product

Соблюдение всех этих правил не является абсолютно обязательным для работы с Eloquent, более того - в "Ларе" предусмотрено все необходимое для обхода этих правил. Так, что именование полей, таблиц и моделей по своему вкусу не будет "сверх-геморроем". Но, соблюдая их (правила), Вы сможете избежать ненужных уточнений и добавить немного волшебства. Кроме того, это поможет в общении с другими разработчиками под Laravel, и людьми его изучающими.

Немного о внешних ключах:
Строго говоря, Laravel не требует (но и не запрещает) буквального наличия внешних ключей на полях таблиц, как такового; и ему вполне достаточно правильно именованных полей, или точного их указания в связях моделей. Благодаря этой своей особенности, "Лара" одинаково хорошо работает как с реляционными, так и нереляционными типами таблиц. Здесь и далее, говоря "внешний ключ", я не буду подразумевать буквального его наличия в таблице - я буду иметь ввиду поля в таблицах, по которым будет реализовываться связь моделей.

###Построение отношений Здесь я напомню какие типы связей бывают, и то как они выражаются в Eloquent.

Итак, типы связей бывают:

  1. Один к одному
  2. Многие к одному
  3. Многие ко многим
  4. Полиморфические

Первые три связи могут быть реализованы в реляционных базах данных, а последняя - лишь эмулируется.

####Один к одному

В школе есть ученики, каждый год приходят новые ученики, старые уходят. Текучка, в общем. За каждым учеником в школе может быть закреплен шкафчик. У шкафчиков есть какие-то свои параметры: инвентарный (не порядковый) номер или степень износа. В каждый момент времени за одним учеником может быть закреплен один шкафчик. Шкафчиков меньше, чем учеников, по этой причине, на всех не хватает, и они всегда заняты.

Налицо связь один к одному. Внешний ключ, соответственно, будет в таблице шкафчиков, потому как шкафчик принадлежит ученику, а не наоборот.

Таблица students:

idname
1Василий
2Геннадий
3Евлампий

Таблица cabinets

idinventory_numberstudent_id
1AS-233
2AS-651
3BG-152

Связи в моделях 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:

idtitle
111А
2
3

Таблица students:

idnameclass_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).

Итак, для выражения связи "Многие ко многим", понадобится три таблицы:

  1. students
  2. groups
  3. group_student - пивот

пивотная таблица должна содержать соответствующие внешние ключи: student_id и group_id.

Таблица students:

idname
1Василий
2Геннадий
3Евлампий

Таблица groups:

idtitle
1Рисование
2Плавание
3Сделай сам

Таблица group_student:

student_idgroup_id
12
13
11
21

Модели данных, в этом случае будут выглядеть так:

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:

idtitlestudent_id
15Вий22
13Отцы и дети14
32Пушкин. Поэмы19

Сейчас мы выдаем книги только ученикам. Как же сделать так, чтобы можно было выдавать книги и учителям? Допустим, что есть модель учителя Teacher и соответствующая таблица teachers. Мы могли бы ввести дополнительное поле teacher_id в нашу таблицу:

idtitlestudent_idteacher_id
1Вий22null
13Педагогическое пособиеnull25

А если у нас появятся другие модели, к которым можно отнести книги? Снова добавлять поля? Бред. Мы могли бы выделить отношения между книгами и учителями в отдельную пивотную таблицу book_teacher:

book_idteacher_id
1522
1325
3211

Но тогда при движении книги (я имею ввиду передачу от одного объекта другому) пришлось бы отслеживать состояние и других таблиц, чтобы книга не оказалась у двоих сразу или вообще пропала. Это все неправильно... А правильно будет вот так:

таблица books:

idtitleholder_typeholder_id
1ВийStudent17
13ВыстрелTeacher25

В данном случае, поле 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 без соблюдения строгой нотации - курим мануал.

#####З.З.Ы. Хотя материалу уже без малого год, информация до сих пор актуальна. Единственное, что можно отметить в связи с изменениями в "Пятом Пришествии Великого и Могучего", это появление нейм-спейсов, в пользовательском слое (технически, их и раньше никто не запрещал использовать, но в 5 они рекомендованы явным образом), А так же php 5.5 в требованиях (появление магической константы class).

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

use Illuminate\Database\Eloquent\Model;
use SomeNameSpace\Relation;

class Entity extends Model {
  
  public function relations()
  {
     return $this->hasMany(Relation::class);
  }
}

@Baksalyar, спасибо за корректорскую работу.