Relationships between Models 模型关系
There are four types of relationships: one-on-one, one-to-many, many-to-one and many-to-many. The relationship may be unidirectional or bidirectional, and each can be simple (a one to one model) or more complex (a combination of models). The model manager manages foreign key constraints for these relationships, the definition of these helps referential integrity as well as easy and fast access of related records to a model. Through the implementation of relations, it is easy to access data in related models from each record in a uniform way.
有四种关系类型:one-on-one, one-to-many, many-to-one and many-to-many。这些关系可以是单向或双向,可以简单或复杂。模型管理器为这些关系管理外键约束,这些定义 引用完整性 更简单和快速访问相关记录到模型。通过这些关系的实现,可以使用统一的方式在相关模型中容易地获取来自每条记录的数据。
Unidirectional relationships 单向关系
Unidirectional relations are those that are generated in relation to one another but not vice versa反过来不行.
Bidirectional relations 双向关系
The bidirectional relations build relationships in both models and each model defines the inverse relationship of the other.
Defining relationships 定义关系
In Phalcon, relationships must be defined in the initialize() method of a model. 必须在模型的 initialize()方法中定义关系。The methods belongsTo(), hasOne(), hasMany() and hasManyToMany() define the relationship between one or more fields from the current model to fields in another model. Each of these methods requires 3 parameters: local fields, referenced model, referenced fields.(三个参数,本地字段,引用模型,引用字段)
Method |
Description |
hasMany |
Defines a 1-n relationship |
hasOne |
Defines a 1-1 relationship |
belongsTo |
Defines a n-1 relationship |
hasManyToMany |
Defines a n-n relationship |
(如果某个模型被其它模型引用,就需要定义hasMany,就是所谓的一对多,同理,如果模型引用到另外模型,那么它就需要定义belongsTo,如果通过一个中间表引用到另外的模型,就需要定义hasManyToMany)
The following schema shows 3 tables whose relations will serve us as an example regarding relationships:
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`)
);
这三张表就是数据库里面常见的多对多关系,在数据库中只要维护中间表的引用即可。不过在这里就要复杂一些。
The model “Robots” has many “RobotsParts”.
The model “Parts” has many “RobotsParts”.
The model “RobotsParts” belongs to both “Robots” and “Parts” models as a many-to-one relation.
The model “Robots” has a relation many-to-many to “Parts” through “RobotsParts”
(被引用的是老子,它有多个仔,老子自然就是有多个仔,仔自然就属于老子,但是多到多就不好理解了,实际它是指要用中间表建立多对多关系,这些模型管理器才知道这些关系)
The models with their relations could be implemented as follows:
<?php
class Robots extends \Phalcon\Mvc\Model
{
public $id;
public $name;
public function initialize()
{
$this->hasMany("id", "RobotsParts", "robots_id");
}
}
<?php
class Parts extends \Phalcon\Mvc\Model
{
public $id;
public $name;
public function initialize()
{
$this->hasMany("id", "RobotsParts", "parts_id");
}
}
<?php
class RobotsParts extends \Phalcon\Mvc\Model
{
public $id;
public $robots_id;
public $parts_id;
public function initialize()
{
$this->belongsTo("robots_id", "Robots", "id");
$this->belongsTo("parts_id", "Parts", "id");
}
}
The first parameter indicates the field of the local model used in the relationship; the second indicates the name of the referenced model and the third the field name in the referenced model. You could also use arrays to define multiple fields in the relationship.
Many to many relationships require 3 models and define the attributes involved in the relationship:
<?php
class Robots extends \Phalcon\Mvc\Model
{
public $id;
public $name;
public function initialize()
{
$this->hasManyToMany(
"id",
"RobotsParts",
"robots_id", "parts_id",
"Parts",
"id"
);
}
}
//如果在Parts中定义这个多到多,看起来应该是可以的
<?php
class Parts extends \Phalcon\Mvc\Model
{
public $id;
public $name;
public function initialize()
{
$this->hasManyToMany(
"id",
"RobotsParts",
"parts_id", "robots_id ",
"Robots",
"id"
);
}
}
Taking advantage of relationships
When explicitly defining the relationships between models, it is easy to find related records for a particular record.
<?php
$robot = Robots::findFirst(30);
foreach ($robot->robotsParts as $robotPart) {
echo $robotPart->parts->name, "\n";
}
优点先不说了,先看看产生的SQL吧
Wed, 03 Sep 14 09:06:05 +0800][INFO] SELECT `robots`.`id`, `robots`.`name`, `robots`.`type`, `robots`.`year` FROM `robots` WHERE `robots`.`id` = 30 LIMIT 1
[Wed, 03 Sep 14 09:06:05 +0800][INFO] SELECT `robots_parts`.`id`, `robots_parts`.`robots_id`, `robots_parts`.`parts_id`, `robots_parts`.`created_at` FROM `robots_parts` WHERE `robots_parts`.`robots_id` = :0
[Wed, 03 Sep 14 09:06:05 +0800][INFO] SELECT `parts`.`id`, `parts`.`name` FROM `parts` WHERE `parts`.`id` = :0 LIMIT 1
[Wed, 03 Sep 14 09:06:05 +0800][INFO] SELECT `parts`.`id`, `parts`.`name` FROM `parts` WHERE `parts`.`id` = :0 LIMIT 1
[Wed, 03 Sep 14 09:06:05 +0800][INFO] SELECT `parts`.`id`, `parts`.`name` FROM `parts` WHERE `parts`.`id` = :0 LIMIT 1
预计会产生联合查询,实际结果让人失望。代码是方便了,实际效率很差。从robot获取robot_part,在从robot_part获取part。
Phalcon uses the magic methods __set/__get/__call to store or retrieve related data using relationships.
By accessing an attribute with the same name as the relationship will retrieve all its related record(s).
Also, you can use a magic getter:
<?php
$robot = Robots::findFirst();
$robotsParts = $robot->getRobotsParts(); // all the related records in RobotsParts
$robotsParts = $robot->getRobotsParts(array('limit' => 5)); // passing parameters
If the called method has a “get” prefix Phalcon\Mvc\Model will return a findFirst()/find() result. The following example compares retrieving related results with using magic methods and without:
<?php
$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 = '2012-03-15'");
// Or using bound parameters
$robotsParts = $robot->getRobotsParts(array(
"created_at = :date:",
"bind" => array("date" => "2012-03-15")
));
$robotPart = RobotsParts::findFirst(1);
// RobotsParts model has a n-1 (belongsTo)
// relationship to RobotsParts then
$robot = $robotPart->robots;
##########
##Getting related records manually:
<?php
$robot = Robots::findFirst(2);
// Robots model has a 1-n (hasMany)
// relationship to RobotsParts, then
$robotsParts = RobotsParts::find("robots_id = '" . $robot->id . "'");
// Only parts that match conditions
$robotsParts = RobotsParts::find(
"robots_id = '" . $robot->id . "' AND created_at = '2012-03-15'"
);
$robotPart = RobotsParts::findFirst(1);
// RobotsParts model has a n-1 (belongsTo)
// relationship to RobotsParts then
$robot = Robots::findFirst("id = '" . $robotPart->robots_id . "'");
The prefix “get” is used to find()/findFirst() related records. Depending on the type of relation it will use ‘find’ or ‘findFirst’:
Type |
Description |
Implicit Method |
Belongs-To |
Returns a model instance of the related record directly |
findFirst |
Has-One |
Returns a model instance of the related record directly |
findFirst |
Has-Many |
Returns a collection of model instances of the referenced model |
find |
Has-Many-to-Many |
Returns a collection of model instances of the referenced model, it implicitly does ‘inner joins’ with the involved models |
(complex query) |
You can also use “count” prefix to return an integer denoting the count of the related records:
<?php
$robot = Robots::findFirst(2);
echo "The robot has ", $robot->countRobotsParts(), " parts\n";
Aliasing Relationships 关系别名
To explain better how aliases work, let’s check the following example:
Table “robots_similar” has the function to define what robots are similar to others:
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)
A model that maps this table and its relationships is the following:
<?php
class RobotsSimilar extends Phalcon\Mvc\Model
{
public function initialize()
{
$this->belongsTo('robots_id', 'Robots', 'id');
$this->belongsTo('similar_robots_id', 'Robots', 'id');
}
}
Since both relations point to the same model (Robots), obtain the records related to the relationship could not be clear:
<?php
$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?
The aliases allow us to rename both relationships to solve these problems:
<?php
class RobotsSimilar extends Phalcon\Mvc\Model
{
public function initialize()
{
$this->belongsTo('robots_id', 'Robots', 'id', array(
'alias' => 'Robot'
));
$this->belongsTo('similar_robots_id', 'Robots', 'id', array(
'alias' => 'SimilarRobot'
));
}
}
With the aliasing we can get the related records easily:
<?php
$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;
Magic Getters vs. Explicit methods
Most IDEs and editors with auto-completion capabilities can not infer提示 the correct types when using magic getters, instead of use the magic getters you can optionally define those methods explicitly明确的 with the corresponding相应的 docblocks(DOC块) helping the IDE to produce a better auto-completion:
<?php
class Robots extends \Phalcon\Mvc\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);
}
}
Virtual Foreign Keys
By default, relationships do not act like database foreign keys, that is, if you try to insert/update a value without having a valid value in the referenced model, Phalcon will not produce a validation message. You can modify this behavior by adding a fourth parameter when defining a relationship.
默认定义的关系没有扮演类似数据库中的外键,如果插入或更新一个在引用模型中没有验证的值,Phalcon不会产生验证消息。你可以在定义关系时添加第四个参数修改这个行为。
The RobotsPart model can be changed to demonstrate this feature:
<?php
class RobotsParts extends \Phalcon\Mvc\Model
{
public $id;
public $robots_id;
public $parts_id;
public function initialize()
{
$this->belongsTo("robots_id", "Robots", "id", array(
"foreignKey" => true
));
$this->belongsTo("parts_id", "Parts", "id", array(
"foreignKey" => array(
"message" => "The part_id does not exist on the Parts model"
)
));
}
}
If you alter a belongsTo() relationship to act as foreign key, it will validate that the values inserted/updated on those fields have a valid value on the referenced model. 如果在belongsTo()中定义了外键,那么在插入或更新值时将在引用模型中验证值。 Similarly, if a hasMany()/hasOne() is altered it will validate that the records cannot be deleted if that record is used on a referenced model. 类似,如果hasMany()/hasOne()定义了外键,记录被删除时将检查记录是否被引用。
(belongsTo()/hasMany()/hasOne()之间是逻辑上有外键约束的,但是hasManyToMany()两个实体间被没有这个逻辑关系)
<?php
class Parts extends \Phalcon\Mvc\Model
{
public function initialize()
{
$this->hasMany("id", "RobotsParts", "parts_id", array(
"foreignKey" => array(
"message" => "The part cannot be deleted because other robots are using it"
)
));
}
}
(这个被引用的表设置关联外键之后(严格来说对它不是外键约束,是外键关联),当记录被删除时将被阻止)
————————————————————————
为了更好理解这个阻止操作,使用如下代码测试:
class PushController extends \Phalcon\Mvc\Controller{
public function deletePartAction(){
$part = Parts::findFirst(7);
$ros = '';
foreach($part->robots as $robot){
$ros .= $robot->id."($robot->name) ";
}
echo "The robots --> ".$ros." has the same part($part->id $part->name)\n\n";
if($part->delete() == false){
foreach($part->getMessages() as $m){
echo "Message: ", $m->getMessage(), "\n";
echo "Field: ", $m->getField(), "\n";
echo "Type: ", $m->getType(), "\n";
}
}
$this->view->disable()
}
}
#########
##输出
The robots --> 31(new robot) 36(new robot) has the same part(7 part_6)
Message: The part cannot be deleted because other robots are using it
Field: id
Type: ConstraintViolation
首先使用part_6的机器人有31 和 36,当删除part_6时,这个操作被阻止了,因为它被引用。下面看看产生的SQL:
[Wed, 03 Sep 14 14:15:40 +0800][INFO] SELECT `parts`.`id`, `parts`.`name` FROM `parts` WHERE `parts`.`id` = 7 LIMIT 1
[Wed, 03 Sep 14 14:15:40 +0800][INFO] SELECT `robots`.`id`, `robots`.`similar_robots_id`, `robots`.`name`, `robots`.`type`, `robots`.`year` FROM `robots` INNER JOIN `robots_parts` ON `robots_parts`.`robots_id` = `robots`.`id` WHERE `robots_parts`.`parts_id` = :0
[Wed, 03 Sep 14 14:15:40 +0800][INFO] SELECT COUNT(*) AS `rowcount` FROM `robots_parts` WHERE `robots_parts`.`parts_id` = :0
先定位具体的part,然后通过part找出使用了它的robot(inner join操作),在删除part前到表robots_parts中检查对应的part是否被引用,如果被引用删除就被阻止了。
从这里可以看到,所谓的外键,是程序维护的,不是数据库意义上的外键,所有这里叫虚拟外键。所谓外键引用完整性就是这里的避免被引用的part被删除。
逻辑上,part属于robot一部分,part被引用时它不应该被删除,robot有多个part组成,robot删除时,它的引用信息也应该被删除(不是删除part哦)。
————————————————————————
Cascade/Restrict actions
Relationships that act as virtual foreign keys by default restrict the creation/update/deletion of records to maintain the integrity of data:
<?php
namespace Store\Models;
use Phalcon\Mvc\Model,
Phalcon\Mvc\Model\Relation;
class Robots extends Model
{
public $id;
public $name;
public function initialize()
{
$this->hasMany('id', 'Store\\Models\Parts', 'robots_id', array(
'foreignKey' => array(
'action' => Relation::ACTION_CASCADE
)
));
}
}
The above code set up to delete all the referenced records (parts) if the master record (robot) is deleted.
同样的,这个级联删除也是程序维护的。实际上,外键约束 和 级联删除这些,现代数据库系统都支持这些特征,可以把这些交给数据库系统来做,如果程序来维护开销是很大的,比如插入一条记录,要验证引用ID的有效性,需要针对被引用表来发起一次查询。
————————————————————————
注意到以上代码通过part找到使用了这个part的robots,实际过程产生了一条inner join查询,如果Parts只设置了hasMany()和RobotsParts产生对应,但是它是如果直接可以访问robots($part->robots)的呢?实际是不可以的,需要添加它跟robots的多对多关系才可以:
$this->hasManyToMany(
"id",
"RobotsParts",
"parts_id", "robots_id",
"Robots",
"id"
);
这个就高速模型管理器,Parts和Robots的多对多关系通过RobotsParts实现,同理,要在Robot中获取Part,也需要在Robots中定义多对多关系,它们是产生JOIN的依据。
另外,我们在控制器代码中通过$part->robots获取使用这个part的robots,这个逻辑应该写入到自己的模型中:
public function getRobots ($parameters=null)
{
return $this->getRelated('Robots', $parameters);
}
这里的内容,就是Phalcon提供的ORM方案的核心部分。实际上,数据库查询操作还有更加复杂的内容,这些大概就是ORM无法封装的了,这个时候就要使用原生SQL了。
最后,这里提到了一对一的关系,但是没有具体内容。查看hasOne()方法有如下例子:
<?php
class Robots extends \Phalcon\Mvc\Model
{
public function initialize()
{
$this->hasOne('id', 'RobotsDescription', 'robots_id');
}
}
这里的意思就是Robots有一个对应的RobotsDescription。正如文档开头描述那样,hasOne应该是一个双向关系,hasManyToMany也是双向关系,而hasMany和belongsTo是单向的,所以在RobotsDescription中也应该定义hasOne。