09 Связи Модели
Отношения между моделями
Существует четыре типа отношений: один-на-один, один-ко-многим, многие-к-одному и многие-ко-многим. Связь может быть однонаправленной или двунаправленной, и каждая из них может быть простой (от одной к одной модели) или более сложной (комбинация моделей). Менеджер модели управляет ограничениями внешнего ключа для этих отношений, определение которых помогает ссылочной целостности, а также легкий и быстрый доступ к соответствующим записям модели. Благодаря реализации отношений легко получить доступ к данным в связанных моделях из каждой записи в едином виде.
Однонаправленные отношения
Однонаправленные отношения - это те, которые генерируются по отношению от первой ко второй , но не наоборот.
Двунаправленные отношения
Двунаправленные отношения строят отношения в обеих моделях, и каждая модель определяет обратную связь другой.
Определение отношений
В Phalcon отношения должны быть определены в методе initialize() модели. Методы belongsTo(), hasOne(), hasMany() и hasManyToMany() определяют взаимосвязь между одним или несколькими полями текущей модели и полями в другой модели , Для каждого из этих методов требуются 3 параметра: локальные поля, ссылочная модель, ссылочные поля.
| Метод | Описание |
|---|---|
| hasMany | Определяет соотношение 1-n |
| hasOne | Определяет соотношение 1-1 |
| belongsTo | Определяет отношение n-1 |
| hasManyToMany | Определяет отношение n-n |
Следующая схема показывает 3 таблицы, отношения которых будут служить нам в качестве примера относительно отношений:
CREATE TABLE robots (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
name varchar(70) NOT NULL,
type varchar(32) NOT NULL,
year int(11) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE robots_parts (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
robots_id int(10) NOT NULL,
parts_id int(10) NOT NULL,
created_at DATE NOT NULL,
PRIMARY KEY (id),
KEY robots_id (robots_id),
KEY parts_id (parts_id)
);
CREATE TABLE parts (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
name varchar(70) NOT NULL,
PRIMARY KEY (id)
);
- Модель
Robotsимеет многоRobotsParts. - Модель
Partsимеет многоRobotsParts. - Модель
RobotsPartsотносится к моделямRobotsиPartsкак отношение "многие к одному". - Модель
Robotsимеет отношение многие-ко-многимPartsчерезRobotsParts.
Просмотрите диаграмму EER, чтобы лучше понять отношения:

Модели с их отношениями могут быть реализованы следующим образом:
<?php
namespace Store\Toys;
use Phalcon\Mvc\Model;
class Robots extends Model
{
public $id;
public $name;
public function initialize()
{
$this->hasMany(
'id',
'RobotsParts',
'robots_id'
);
}
}
<?php
use Phalcon\Mvc\Model;
class Parts extends Model
{
public $id;
public $name;
public function initialize()
{
$this->hasMany(
'id',
'RobotsParts',
'parts_id'
);
}
}
<?php
use Phalcon\Mvc\Model;
class RobotsParts extends Model
{
public $id;
public $robots_id;
public $parts_id;
public function initialize()
{
$this->belongsTo(
'robots_id',
'Store\Toys\Robots',
'id'
);
$this->belongsTo(
'parts_id',
'Parts',
'id'
);
}
}
Первый параметр указывает поле локальной модели, используемой в связи; второй указывает имя ссылочной модели и третье имя поля в ссылочной модели. Можно также использовать массивы для определения нескольких полей в связи.
Отношение “многие-ко-многим” требуют 3 модели и определение атрибутов, участвующих в отношениях:
<?php
namespace Store\Toys;
use Phalcon\Mvc\Model;
class Robots extends Model
{
public $id;
public $name;
public function initialize()
{
$this->hasManyToMany(
'id',
'RobotsParts',
'robots_id', 'parts_id',
'Parts',
'id'
);
}
}
Связи нескольких полей
Бывают случаи, когда отношения должны быть определены для комбинации полей, а не только для одного. Рассмотрим следующий пример:
<?php
namespace Store\Toys;
use Phalcon\Mvc\Model;
class Robots extends Model
{
public $id;
public $name;
public $type;
}
и
<?php
namespace Store\Toys;
use Phalcon\Mvc\Model;
class Parts extends Model
{
public $id;
public $robotId;
public $robotType;
public $name;
}
Выше мы имеем модель Robots , которая имеет три свойства. Уникальный id, name и type , который определяет, что этот робот (механические и др.); В модели Parts у нас также есть name для детали, но также поля, которые связывают робота и его тип с определенной частью.
Используя параметры отношений, описанные выше, связывание одного поля между двумя моделями не даст необходимых результатов. Для этого мы можем использовать массив в наших отношениях:
<?php
namespace Store\Toys;
use Phalcon\Mvc\Model;
class Robots extends Model
{
public $id;
public $name;
public $type;
public function initialize()
{
$this->hasOne(
['id', 'type'],
Parts::class,
['robotId', 'robotType'],
[
'reusable' => true, // кэшировать связанные данные
'alias' => 'parts',
]
);
}
}
Обратите внимание на поля отображения в отношениях один к одному, т. е. первое поле исходная модель массива соответствует первому полю конечного массива и т. д. Количество полей должно совпадать в исходной и целевой моделей.
Использование отношений.
При явном определении отношений между моделями легко найти связанные записи для конкретной записи.
<?php
use Store\Toys\Robots;
$robot = Robots::findFirst(2);
foreach ($robot->robotsParts as $robotPart) {
echo $robotPart->parts->name, "\n";
}
Phalcon использует магические методы __set/__get/__call для сохранения или получения связанных данных с использованием отношений.
При обращении к атрибуту с тем же именем, что и у отношения, будут получены все связанные записи.
<?php use Store\Toys\Robots; $robot = Robots::findFirst(); // Все связанные записи в RobotsParts $robotsParts = $robot->robotsParts;
Кроме того, вы можете использовать волшебный геттер:
<?php
use Store\Toys\Robots;
$robot = Robots::findFirst();
// Все связанные записи в RobotsParts
$robotsParts = $robot->getRobotsParts();
// Передача параметров
$robotsParts = $robot->getRobotsParts(
[
'limit' => 5,
]
);
Если вызываемый метод имеет префикс get, то Phalcon\Mvc\Model вернет результат findFirst()/find() . В следующем примере сравнивается получение связанных результатов с использованием магических методов и без них:
<?php
use Store\Toys\Robots;
$robot = Robots::findFirst(2);
// Модель Robots имеет отношение 1-n (hasMany)
// к RobotsParts, то
$robotsParts = $robot->robotsParts;
// Только детали, соответствующие условиям
$robotsParts = $robot->getRobotsParts(
[
'created_at = :date:',
'bind' => [
'date' => '2015-03-15'
]
]
);
$robotPart = RobotsParts::findFirst(1);
// Модель RobotsParts имеет отношение
// n-1 (belongsTo) к RobotsParts
$robot = $robotPart->robots;
Получение связанных записей вручную:
<?php
use Store\Toys\Robots;
$robot = Robots::findFirst(2);
// Модель Robots имеет отношение
// 1-n (hasMany) к RobotsParts, то
$robotsParts = RobotsParts::find(
[
'robots_id = :id:',
'bind' => [
'id' => $robot->id,
]
]
);
// Только детали, соответствующие условиям
$robotsParts = RobotsParts::find(
[
'robots_id = :id: AND created_at = :date:',
'bind' => [
'id' => $robot->id,
'date' => '2015-03-15',
]
]
);
$robotPart = RobotsParts::findFirst(1);
// Модель RobotsParts имеет отношение
// n-1 (belongsTo) к RobotsParts
$robot = Robots::findFirst(
[
'id = :id:',
'bind' => [
'id' => $robotPart->robots_id,
]
]
);
Префикс get используется для find()/findFirst() связанных записей. В зависимости от типа связи будет использоваться find() или findFirst():
| Тип | Описание | Неявный метод |
|---|---|---|
| Belongs-To | Возвращает экземпляр модели связанной записи напрямую | findFirst |
| Has-One | Возвращает экземпляр модели связанной записи напрямую | findFirst |
| Has-Many | Возвращает коллекцию экземпляров модели, на которую ссылается модель | find |
| Has-Many-to-Many | Возвращает коллекцию экземпляров модели, на которую ссылается модель, неявно выполняет 'inner joins' с вовлеченными моделями | (сложный запрос) |
Префикс count также можно использовать для возврата целого числа, обозначающего количество связанных записей:
<?php use Store\Toys\Robots; $robot = Robots::findFirst(2); echo 'The robot has ', $robot->countRobotsParts(), " parts\n";
Псевдонимы Отношений
Чтобы лучше объяснить, как работают псевдонимы, рассмотрим следующий пример:
Таблица robots_similar имеет функцию, чтобы определить, какие роботы похожи на другие:
mysql> desc robots_similar; +-------------------+------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------------+------------------+------+-----+---------+----------------+ | id | int(10) unsigned | NO | PRI | NULL | auto_increment | | robots_id | int(10) unsigned | NO | MUL | NULL | | | similar_robots_id | int(10) unsigned | NO | | NULL | | +-------------------+------------------+------+-----+---------+----------------+ 3 rows in set (0.00 sec)
И robots_id и similar_robots_id имеют отношение к модели Robots:

Модель, сопоставляющая эту таблицу и ее связи, выглядит следующим образом:
<?php
class RobotsSimilar extends Phalcon\Mvc\Model
{
public function initialize()
{
$this->belongsTo(
'robots_id',
'Store\Toys\Robots',
'id'
);
$this->belongsTo(
'similar_robots_id',
'Store\Toys\Robots',
'id'
);
}
}
Поскольку обе связи указывают на одну и ту же модель (Robots), получение записей, связанных с отношением, не может быть ясным:
<?php $robotsSimilar = RobotsSimilar::findFirst(); // Возвращает соответствующую запись на основе столбца (robots_id) // Также как и belongsTo, он возвращает только одну запись // но имя 'getRobots', по-видимому, означает, что возвращение более одного $robot = $robotsSimilar->getRobots(); // но, как получить соответствующую запись на основе столбца (similar_robots_id) // если оба отношения имеют одно и то же имя?
Эти псевдонимы позволяют нам переименовать оба отношения для решения этих проблем:
?php
use Phalcon\Mvc\Model;
class RobotsSimilar extends Model
{
public function initialize()
{
$this->belongsTo(
'robots_id',
'Store\Toys\Robots',
'id',
[
'alias' => 'Robot',
]
);
$this->belongsTo(
'similar_robots_id',
'Store\Toys\Robots',
'id',
[
'alias' => 'SimilarRobot',
]
);
}
}
С псевдонимом мы можем легко получить связанные записи. Вы также можете использовать метод getRelated() для доступа к отношениям с использованием имени псевдонима:
<?php
$robotsSimilar = RobotsSimilar::findFirst();
// Возвращает соответствующую запись на основе столбца (robots_id)
$robot = $robotsSimilar->getRobot();
$robot = $robotsSimilar->robot;
$robot = $robotsSimilar->getRelated('Robot');
// Возвращает соответствующую запись на основе столбца (similar_robots_id)
$similarRobot = $robotsSimilar->getSimilarRobot();
$similarRobot = $robotsSimilar->similarRobot;
$similarRobot = $robotsSimilar->getRelated('SimilarRobot');
Магические Геттеры против явных методов
Большинство IDE и редакторов с возможностями автоматического завершения не могут вывести правильные типы при использовании магических геттеров (оба метода и свойств). Чтобы преодолеть это, вы можете использовать класс docblock класса, который указывает, какие магические действия доступны, помогая IDE создать лучшее автозаполнение:
<?php
namespace Store\Toys;
use Phalcon\Mvc\Model;
/**
* Model class for the robots table.
* @property Simple|RobotsParts[] $robotsParts
* @method Simple|RobotsParts[] getRobotsParts($parameters = null)
* @method integer countRobotsParts()
*/
class Robots extends Model
{
public $id;
public $name;
public function initialize()
{
$this->hasMany(
'id',
'RobotsParts',
'robots_id'
);
}
}
Условия
Можно также создать связи на основе условных выражений. При запросе на основе отношения условие будет автоматически добавлено к запросу:
<?php
use Phalcon\Mvc\Model;
// Компании выставили им счета (оплаченные / неоплаченные)
// Модель счетов
class Invoices extends Model
{
}
// Модель компаний
class Companies extends Model
{
public function initialize()
{
// Все счета-фактуры
$this->hasMany(
'id',
'Invoices',
'inv_id',
[
'alias' => 'Invoices'
]
);
// Отношение оплаченных счетов
$this->hasMany(
'id',
'Invoices',
'inv_id',
[
'alias' => 'InvoicesPaid',
'params' => [
'conditions' => "inv_status = 'paid'"
]
]
);
// Отношение неоплаченных счетов + связанные параметры
$this->hasMany(
'id',
'Invoices',
'inv_id',
[
'alias' => 'InvoicesUnpaid',
'params' => [
'conditions' => "inv_status <> :status:",
'bind' => ['status' => 'unpaid']
]
]
);
}
}
Кроме того, можно использовать второй параметр getRelated() при доступе к отношениям из объекта модели для дальнейшей фильтрации или упорядочивания отношений:
<?php
// неоплаченный счет
$company = Companies::findFirst(
[
'conditions' => 'id = :id:',
'bind' => ['id' => 1],
]
);
$unpaidInvoices = $company->InvoicesUnpaid;
$unpaidInvoices = $company->getInvoicesUnpaid();
$unpaidInvoices = $company->getRelated('InvoicesUnpaid');
$unpaidInvoices = $company->getRelated(
'Invoices',
['conditions' => "inv_status = 'paid'"]
);
// Также заказывали
$unpaidInvoices = $company->getRelated(
'Invoices',
[
'conditions' => "inv_status = 'paid'",
'order' => 'inv_created_date ASC',
]
);
Виртуальные Внешние Ключи
По умолчанию связи не действуют как внешние ключи базы данных, то есть при попытке вставить/обновить значение без наличия допустимого значения в указанной модели Phalcon не выдаст сообщение о проверке. Это поведение можно изменить, добавив четвертый параметр при определении связи.
Модель RobotsPart может быть изменена для демонстрации этой функции:
<?php
use Phalcon\Mvc\Model;
class RobotsParts extends Model
{
public $id;
public $robots_id;
public $parts_id;
public function initialize()
{
$this->belongsTo(
'robots_id',
'Store\Toys\Robots',
'id',
[
'foreignKey' => true
]
);
$this->belongsTo(
'parts_id',
'Parts',
'id',
[
'foreignKey' => [
'message' => 'Part_id не существует в модели Parts'
]
]
);
}
}
При изменении отношения belongsTo() в качестве внешнего ключа выполняется проверка того, что значения, вставленные/обновленные в эти поля, имеют допустимое значение в модели, на которую ссылается ссылка. Точно так же, если hasMany()/hasOne() изменен, это проверит, что записи не могут быть удалены, если та запись используется на ссылочной модели.
<?php
use Phalcon\Mvc\Model;
class Parts extends Model
{
public function initialize()
{
$this->hasMany(
'id',
'RobotsParts',
'parts_id',
[
'foreignKey' => [
'message' => 'Деталь нельзя удалить, так как ее используют другие роботы',
]
]
);
}
}
Виртуальный внешний ключ можно настроить для разрешения значений null следующим образом:
<?php
use Phalcon\Mvc\Model;
class RobotsParts extends Model
{
public $id;
public $robots_id;
public $parts_id;
public function initialize()
{
$this->belongsTo(
'parts_id',
'Parts',
'id',
[
'foreignKey' => [
'allowNulls' => true,
'message' => 'Part_id не существует в модели деталей',
]
]
);
}
}
Cascade/Restrict действия
Связи, которые действуют как виртуальные внешние ключи по умолчанию, ограничивают создание/обновление / удаление записей для поддержания целостности данных:
<?php
namespace Store\Toys;
use Phalcon\Mvc\Model;
use Phalcon\Mvc\Model\Relation;
class Robots extends Model
{
public $id;
public $name;
public function initialize()
{
$this->hasMany(
'id',
'Parts',
'robots_id',
[
'foreignKey' => [
'action' => Relation::ACTION_CASCADE,
]
]
);
}
}
Приведенный выше код настроен на удаление всех связанных записей (деталей) при удалении основной записи (робота).
Сохранение Связанных Записей
Магические свойства можно использовать для сохранения записи и ее свойства:
<?php // Создание исполнителя $artist = new Artists(); $artist->name = 'Shinichi Osawa'; $artist->country = 'Japan'; // Создать альбом $album = new Albums(); $album->name = 'The One'; $album->artist = $artist; // Assign the artist $album->year = 2008; // Сохранить обе записи $album->save();
Сохранение записи и связанных с ней записей в отношении «есть-много»:
<?php
// Получить существующего исполнителя
$artist = Artists::findFirst(
'name = 'Shinichi Osawa''
);
// Создать альбом
$album = new Albums();
$album->name = 'The One';
$album->artist = $artist;
$songs = [];
// Создайте первую песню
$songs[0] = new Songs();
$songs[0]->name = 'Star Guitar';
$songs[0]->duration = '5:54';
// Создайте вторую песню
$songs[1] = new Songs();
$songs[1]->name = 'Last Days';
$songs[1]->duration = '4:29';
// Назначить массив композиций
$album->songs = $songs;
// Сохранить альбом + его песни
$album->save();
Сохранение альбома и художника в то же время неявно использует транзакцию, поэтому, если что-то пойдет не так, чтобы сохранить связанные записи, родитель также не будет сохранен. Сообщения передаются пользователю для получения информации о любых ошибках.
Примечание. Добавление связанных объектов путем перегрузки следующих методов невозможно:
Phalcon\Mvc\Model::beforeSave()Phalcon\Mvc\Model::beforeCreate()Phalcon\Mvc\Model::beforeUpdate()
Вам нужно перегрузить Phalcon\Mvc\Model::save() для этого, чтобы работать изнутри модели.
Операции над результатами
Если набор результатов состоит из полных объектов, на этих объектах могут выполняться операции модели. Например:
<?php
/** @var RobotType $type */
$type = $robots->getRelated('type');
$type->name = 'Some other type';
$result = $type->save();
// Получите связанный тип робота, но только столбец `name`
$type = $robots->getRelated('type', ['columns' => 'name']);
$type->name = 'Some other type';
// Это не сработает, потому что `$type` не является полным объектом
$result = $type->save();
Обновление связанных записей
Вместо этого:
<?php
$parts = $robots->getParts();
foreach ($parts as $part) {
$part->stock = 100;
$part->updated_at = time();
if ($part->update() === false) {
$messages = $part->getMessages();
foreach ($messages as $message) {
echo $message;
}
break;
}
}
вы можете сделать это:
<?php
$robots->getParts()->update(
[
'stock' => 100,
'updated_at' => time(),
]
);
update также принимает анонимную функцию для фильтрации того, какие записи необходимо обновить:
<?php
$data = [
'stock' => 100,
'updated_at' => time(),
];
// Обновите все детали, кроме тех, чей тип является основным
$robots->getParts()->update(
$data,
function ($part) {
if ($part->type === Part::TYPE_BASIC) {
return false;
}
return true;
}
);
Удаление связанных записей
Вместо этого:
<?php
$parts = $robots->getParts();
foreach ($parts as $part) {
if ($part->delete() === false) {
$messages = $part->getMessages();
foreach ($messages as $message) {
echo $message;
}
break;
}
}
вы можете сделать это:
<?php $robots->getParts()->delete();
delete() также принимает анонимную функцию для фильтрации того, какие записи необходимо удалить:
<?php
// Удалить только, чей запас больше или равен нулю
$robots->getParts()->delete(
function ($part) {
if ($part->stock < 0) {
return false;
}
return true;
}
);