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; } );