Skip to content

Instantly share code, notes, and snippets.

@greabock
Last active April 15, 2024 12:21
Show Gist options
  • Star 80 You must be signed in to star a gist
  • Fork 22 You must be signed in to fork a gist
  • Save greabock/3d1611c1125f5340f491 to your computer and use it in GitHub Desktop.
Save greabock/3d1611c1125f5340f491 to your computer and use it in GitHub Desktop.
Построение моделей

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

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

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

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

  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:

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

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

  1. students
  2. groups
  3. 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 без соблюдения строгой нотации - курим мануал.

#####З.З.Ы. Хотя материалу уже без малого год, информация до сих пор актуальна. Единственное, что можно отметить в связи с изменениями в "Пятом Пришествии Великого и Могучего", это появление нейм-спейсов, в пользовательском слое (технически, их и раньше никто не запрещал использовать, но в 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, спасибо за корректорскую работу.

Copy link

ghost commented Dec 1, 2014

В главе "Многие к одному" опечатка в коде модели Student:
вместо "public function cabinet()" должно по логике быть "public function class()".
Однако, PHP не даст назвать метод "class", т.к. "class" зарезервировано, можно назвать "studentClass" :)
А вообще статейка хорошая, особенно порадовал пример в виде школьной библиотеки, на котором, имхо, объяснение полиморфических связей гораздо более понятно, чем на примере изображений из официальной документации фреймворка.

@greabock
Copy link
Author

greabock commented Dec 1, 2014

Поправил. Спасибо за отзыв.

@pilot911
Copy link

pilot911 commented Dec 2, 2014

спасибо, полезный пример

@artifexrefercio
Copy link

Спасибо, за статью

@sidigi
Copy link

sidigi commented Jan 24, 2015

Пивотные (стержневые) таблицы, выражающие отношение "Многие ко многим", называются в единственном числе, нижней змеиной нотации по именам связанных моедлей, в произвольном порядке: user_role, или role_user. - Это всё можно переименовать

@greabock
Copy link
Author

@sidigi а еще там написано:

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

а в самом конце:

Что бы узнать о том, как правильно указывать поля в отношенияx без соблюдения строгой нотации - курим мануал.

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

@constb
Copy link

constb commented Jan 27, 2015

В изображении из примера "многие-ко-многим" имя пивотной таблицы - student_group, в то время как имя по умолчанию элоквент сгенерирует как group_student. Либо в коде надо явно указать имя таблицы, либо переименовать её на картинке.

@greabock
Copy link
Author

@constb, поправил, спасибо.

@aab32
Copy link

aab32 commented Jan 27, 2015

Спасибо за статью, как раз для меня 😄 Вопрос по картинкам, красивые такие. Есть какая то прога по визуальному проектированию баз данных и генерации классов ?

@greabock
Copy link
Author

@aab32 для статьи, я использовал вот этот инструмент.
Что касается генерации, то есть вот такой сервис. Но сам я не пользуюсь генераторами - все руками.

@aab32
Copy link

aab32 commented Jan 28, 2015

greabock спасибо

@qant
Copy link

qant commented Jan 30, 2015

Просто Спасибо за столь понятную статью и ваше время, но у меня вопрос по элоквент :
Если все вот так логично и просто магия и тп, то нет ли какого то способа сгенерировать подобные связи через артисан или еще как то?

Причины:

  1. Что бы сделать прототип чего то, не особо разбираюсь в бд.
  2. Просто что бы чисто по человечески не ошибиться.
  3. Конечно что для экономии времени, в случаях когда нет необходимости продумывать структуру базы из-за ее простоты.

Либо возможно уже кто-то создал такой инструмент вручную и он есть где-то?

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

@greabock
Copy link
Author

@quant и тебе спасибо за отзыв (так сложилось, что в сообществе принято обращаться "на ты").
Да, есть генератор, я уже упамянул о нем выше. http://laravelsd.com/
Но я бы не советовал им увлекаться ) Руками тоже нужно уметь. Ну и я не проверял, умеет ли он с разными NS работать.

@Baksalyar
Copy link

Это тебе спасибо за отличный материал и ссылку на него, именно он помог понять мне СУТЬ. :)

@Retsediv
Copy link

Retsediv commented Mar 1, 2015

Хорошая статья, помогли наконец то разобратся со связями!

@greabock
Copy link
Author

greabock commented Mar 1, 2015

@Retsediv я рад что помог! И рад, что даже спустя три месяца материал по-прежнему актуален.

@art-snail
Copy link

Спасибо!

@ElForastero
Copy link

Прочел. Спасибо.

внешние ключи называютсяв единственном
внешние ключи называются в единственном

понимания вещей с фасдами
понимания вещей с фасадами

@SvSerg
Copy link

SvSerg commented Jun 24, 2015

Хорошо объяснил, спасибо!

@projct1
Copy link

projct1 commented Aug 12, 2015

Супер, когда продолжение?

@voidshah
Copy link

Благодарю за статью!

@sawerus
Copy link

sawerus commented Feb 14, 2016

Роман, спасибо! Пиши, как можно больше! Отлично получается!
В введении на последней строке ошибка в слове " контролллеров" - 3 буквы "л".

@greabock
Copy link
Author

@sawerus исправил, спасибо

@a1lan1
Copy link

a1lan1 commented Nov 22, 2017

Спасибо!

@Div-Man
Copy link

Div-Man commented Jun 28, 2018

Спасибо, очень хорошая статья, намного лучше объяснено, чем в официальной доке.

@codeturn
Copy link

Спасибо, теперь все понятно!

@Atmden
Copy link

Atmden commented Feb 6, 2020

Привет!
Спасибо за такую великолепную статью!
Поправьте пожалуйста изображения в статье...

@butcherfirewaters
Copy link

Подскажите, а как реализовать связь многие ко многим для одной таблицы?

@MariiaPonomarenkoUA
Copy link

Прочитав оф документацию, просмотрев штук 20 разных ссылок, наконец наткнулась на понятное объяснение С ПРИМЕРАМИ моделей. Огромное спасибо!

@greabock
Copy link
Author

@MariiaPonomarenkoUA
Шесть лет прошло уже... даже не думал, что это кому-то еще нужно...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment