Отношения между моделями

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

Однонаправленные отношения

Однонаправленные отношения - это те, которые генерируются по отношению от первой ко второй , но не наоборот.

Двунаправленные отношения

Двунаправленные отношения строят отношения в обеих моделях, и каждая модель определяет обратную связь другой.

Определение отношений

В 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 , которая имеет три свойства. Уникальный idname и 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;
    }
);