Spec-Zone.ru › Phalcon 3

Взаимосвязи моделей

Взаимосвязи между моделями

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

Однонаправленные взаимосвязи

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

Двунаправленные взаимосвязи

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

Определение взаимосвязей

В 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`)
);
  • Модель «Роботы» имеет много «ДеталейРоботов».
  • Модель «Детали» имеет много «ДеталейРоботов».
  • Модель «ДеталиРоботов» относится к моделям «Роботы» и «Детали» как отношение много-к-одному.
  • Модель «Роботы» имеет отношение много-ко-многим к «Детали» через «ДеталиРоботов».

Проверьте диаграмму ER для лучшего понимания отношений:

../_images/eer-1.png

Модели с их взаимосвязями могут быть реализованы следующим образом:

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany(
            "id",
            "RobotsParts",
            "robots_id"
        );
    }
}
use Phalcon\Mvc\Model;

class Parts extends Model
{
    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany(
            "id",
            "RobotsParts",
            "parts_id"
        );
    }
}
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 моделей и определяют атрибуты, участвующие во взаимосвязи:

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

Использование взаимосвязей

При явном определении взаимосвязей между моделями легко найти связанные записи для конкретной записи.

use Store\Toys\Robots;

$robot = Robots::findFirst(2);

foreach ($robot->robotsParts as $robotPart) {
    echo $robotPart->parts->name, "\n";
}

Phalcon использует магические методы __set/__get/__call для хранения или извлечения связанных данных с помощью взаимосвязей.

Обращение к атрибуту с тем же именем, что и взаимосвязь, позволит получить все связанные записи.

use Store\Toys\Robots;

$robot = Robots::findFirst();

// All the related records in RobotsParts
$robotsParts = $robot->robotsParts;

Также можно использовать магический геттер:

use Store\Toys\Robots;

$robot = Robots::findFirst();

// All the related records in RobotsParts
$robotsParts = $robot->getRobotsParts();

// Passing parameters
$robotsParts = $robot->getRobotsParts(
    [
        "limit" => 5,
    ]
);

Если вызываемый метод имеет префикс «get», Phalcon\Mvc\Model вернёт результат findFirst()/find(). Следующий пример сравнивает получение связанных результатов с использованием магических методов и без них:

use Store\Toys\Robots;

$robot = Robots::findFirst(2);

// Robots model has a 1-n (hasMany)
// relationship to RobotsParts then
$robotsParts = $robot->robotsParts;

// Only parts that match conditions
$robotsParts = $robot->getRobotsParts(
    [
        "created_at = :date:",
        "bind" => [
            "date" => "2015-03-15"
        ]
    ]
);

$robotPart = RobotsParts::findFirst(1);

// RobotsParts model has a n-1 (belongsTo)
// relationship to RobotsParts then
$robot = $robotPart->robots;

Получение связанных записей вручную:

use Store\Toys\Robots;

$robot = Robots::findFirst(2);

// Robots model has a 1-n (hasMany)
// relationship to RobotsParts, then
$robotsParts = RobotsParts::find(
    [
        "robots_id = :id:",
        "bind" => [
            "id" => $robot->id,
        ]
    ]
);

// Only parts that match conditions
$robotsParts = RobotsParts::find(
    [
        "robots_id = :id: AND created_at = :date:",
        "bind" => [
            "id"   => $robot->id,
            "date" => "2015-03-15",
        ]
    ]
);

$robotPart = RobotsParts::findFirst(1);

// RobotsParts model has a n-1 (belongsTo)
// relationship to RobotsParts then
$robot = Robots::findFirst(
    [
        "id = :id:",
        "bind" => [
            "id" => $robotPart->robots_id,
        ]
    ]
);

Префикс «get» используется для find()/findFirst() связанных записей. В зависимости от типа взаимосвязи он будет использовать find() или findFirst().

Тип Описание Неявный метод
Принадлежит-к Возвращает экземпляр модели связанной записи напрямую findFirst
Один-к-одному Возвращает экземпляр модели связанной записи напрямую findFirst
Один-ко-многим Возвращает коллекцию экземпляров моделей сопоставленной модели find
Много-ко-многим Возвращает коллекцию экземпляров моделей сопоставленной модели, неявно выполняет «внутренние соединения» со связанными моделями (сложное запросы)

Вы также можете использовать префикс «count», чтобы получить целое число, обозначающее количество связанных записей:

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»:

../_images/eer-2.png

Модель, которая отображает эту таблицу и её взаимосвязи, выглядит следующим образом:

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

Поскольку оба отношения указывают на одну и ту же модель (Роботы), получение записей, связанных с отношением, может быть неясным:

$robotsSimilar = RobotsSimilar::findFirst();

// Returns the related record based on the column (robots_id)
// Also as is a belongsTo it's only returning one record
// but the name 'getRobots' seems to imply that return more than one
$robot = $robotsSimilar->getRobots();

// but, how to get the related record based on the column (similar_robots_id)
// if both relationships have the same name?

Псевдонимы позволяют переименовать оба отношения для решения этих проблем:

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",
            ]
        );
    }
}

С псевдонимами мы можем легко получить связанные записи:

$robotsSimilar = RobotsSimilar::findFirst();

// Returns the related record based on the column (robots_id)
$robot = $robotsSimilar->getRobot();
$robot = $robotsSimilar->robot;

// Returns the related record based on the column (similar_robots_id)
$similarRobot = $robotsSimilar->getSimilarRobot();
$similarRobot = $robotsSimilar->similarRobot;

Магические геттеры против явных методов

Большинство IDE и редакторов с функцией автозаполнения не могут определить правильные типы при использовании магических геттеров. Вместо магических геттеров можно явным образом определить эти методы с соответствующими docblock, чтобы IDE могла создавать более точный автозаполнения:

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany(
            "id",
            "RobotsParts",
            "robots_id"
        );
    }

    /**
     * Return the related "robots parts"
     *
     * @return \RobotsParts[]
     */
    public function getRobotsParts($parameters = null)
    {
        return $this->getRelated("RobotsParts", $parameters);
    }
}

Виртуальные внешние ключи

По умолчанию взаимосвязи не действуют как внешние ключи базы данных, то есть если вы попытаетесь вставить/обновить значение без наличия допустимого значения в связанной модели, Phalcon не выдаст сообщение об ошибке валидации. Вы можете изменить это поведение, добавив четвертый параметр при определении взаимосвязи.

Модель RobotsPart может быть изменена, чтобы продемонстрировать эту функцию:

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" => "The part_id does not exist on the Parts model"
                ]
            ]
        );
    }
}

Если вы измените взаимосвязь belongsTo(), чтобы она действовала как внешний ключ, она будет проверять, что вставленные/обновлённые значения в этих полях имеют допустимое значение в связанной модели. Аналогично, если взаимосвязь hasMany()/hasOne() изменена, она будет проверять, что записи не могут быть удалены, если эта запись используется в связанной модели.

use Phalcon\Mvc\Model;

class Parts extends Model
{
    public function initialize()
    {
        $this->hasMany(
            "id",
            "RobotsParts",
            "parts_id",
            [
                "foreignKey" => [
                    "message" => "The part cannot be deleted because other robots are using it",
                ]
            ]
        );
    }
}

Виртуальный внешний ключ можно настроить, чтобы разрешить нулевые значения следующим образом:

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"    => "The part_id does not exist on the Parts model",
                ]
            ]
        );
    }
}

Действия «каскадное удаление»/«ограничение»

Взаимосвязи, которые действуют как виртуальные внешние ключи по умолчанию, ограничивают создание/обновление/удаление записей для поддержания целостности данных:

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

Вышеприведенный код настроен на удаление всех связанных записей (запчастей), если удаляется главная запись (робот).

Сохранение связанных записей

Магические свойства могут использоваться для сохранения записи и её связанных свойств:

// Create an artist
$artist = new Artists();

$artist->name    = "Shinichi Osawa";
$artist->country = "Japan";

// Create an album
$album = new Albums();

$album->name   = "The One";
$album->artist = $artist; // Assign the artist
$album->year   = 2008;

// Save both records
$album->save();

Сохранение записи и связанных записей в отношении один ко многим:

// Get an existing artist
$artist = Artists::findFirst(
    "name = 'Shinichi Osawa'"
);

// Create an album
$album = new Albums();

$album->name   = "The One";
$album->artist = $artist;

$songs = [];

// Create a first song
$songs[0]           = new Songs();
$songs[0]->name     = "Star Guitar";
$songs[0]->duration = "5:54";

// Create a second song
$songs[1]           = new Songs();
$songs[1]->name     = "Last Days";
$songs[1]->duration = "4:29";

// Assign the songs array
$album->songs = $songs;

// Save the album + its songs
$album->save();

Одновременное сохранение альбома и исполнителя неявно использует транзакцию, поэтому, если что-то пойдет не так при сохранении связанных записей, родительская запись также не будет сохранена. Пользователю передаются сообщения об ошибках.

Примечание. Добавление связанных сущностей путём перегрузки следующих методов невозможно:

  • Phalcon\Mvc\Model::beforeSave()
  • Phalcon\Mvc\Model::beforeCreate()
  • Phalcon\Mvc\Model::beforeUpdate()

Для этого необходимо перегрузить Phalcon\Mvc\Model::save() в модели.

Операции над наборами результатов

Если набор результатов состоит из полных объектов, набор результатов способен выполнять операции над полученными записями простым способом:

Обновление связанных записей

Вместо этого:

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

можно сделать так:

$robots->getParts()->update(
    [
        "stock"      => 100,
        "updated_at" => time(),
    ]
);

«update» также принимает анонимную функцию для фильтрации записей, которые должны быть обновлены:

$data = [
    "stock"      => 100,
    "updated_at" => time(),
];

// Update all the parts except those whose type is basic
$robots->getParts()->update(
    $data,
    function ($part) {
        if ($part->type === Part::TYPE_BASIC) {
            return false;
        }

        return true;
    }
);

Удаление связанных записей

Вместо этого:

$parts = $robots->getParts();

foreach ($parts as $part) {
    if ($part->delete() === false) {
        $messages = $part->getMessages();

        foreach ($messages as $message) {
            echo $message;
        }

        break;
    }
}

можно сделать так:

$robots->getParts()->delete();

delete() также принимает анонимную функцию для фильтрации записей, которые должны быть удалены:

// Delete only whose stock is greater or equal than zero
$robots->getParts()->delete(
    function ($part) {
        if ($part->stock < 0) {
            return false;
        }

        return true;
    }
);

© 2011–2017 Phalcon Framework Team
Licensed under the Creative Commons Attribution License 3.0.
https://docs.phalconphp.com/en/latest/reference/model-relationships.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API