🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] # 模型关系 ## 模型之间的关系 有四种类型的关系:一对一,一对多,多对一和多对多。关系可以是单向的或双向的,并且每个可以是简单的(一对一模型)或更复杂的(模型的组合)。模型管理器管理这些关系的外键约束,这些关系的定义有助于参照完整性以及相关记录对模型的轻松快速访问。通过关系的实现,可以很容易地以统一的方式从每个记录中访问相关模型中的数据。 ### 单向关系 单向关系是彼此相关而不是相反生成的关系。 ### 双向关系 两个模型和每个模型中的双向关系构建关系定义了另一个的反向关系。 ### 定义关系 在Phalcon中,必须在模型的 `initialize()` 方法中定义关系。方法`belongsTo()`,`hasOne()`,`hasMany()` 和 `hasManyToMany()` 定义从当前模型到另一个模型中的字段的一个或多个字段之间的关系。这些方法中的每一个都需要3个参数:本地字段,引用模型,引用字段。 | 方法 | 描述 | | ------------- | -------------------------- | | hasMany | 定义1对n的关系 | | hasOne | 定义1对1的关系 | | belongsTo | 定义n对1的关系 | | hasManyToMany | 定义n对n的关系 | 以下模式显示了3个表,其关系将作为关系的示例提供给我们: ```sql 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`. 利用EER图以更好地理解关系: ![](https://docs.phalconphp.com/images/content/models-relationships-eer-1.png) 具有关系的模型可以实现如下: ```php <?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 <?php use Phalcon\Mvc\Model; class Parts extends Model { public $id; public $name; public function initialize() { $this->hasMany( 'id', 'RobotsParts', 'parts_id' ); } } ``` ```php <?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 <?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 <?php use Store\Toys\Robots; $robot = Robots::findFirst(2); foreach ($robot->robotsParts as $robotPart) { echo $robotPart->parts->name, "\n"; } ``` Phalcon使用魔术方法`__set`/`__get`/`__call` 来使用关系存储或检索相关数据。 通过访问与关系同名的属性将检索其所有相关记录。 ```php <?php use Store\Toys\Robots; $robot = Robots::findFirst(); // All the related records in RobotsParts $robotsParts = $robot->robotsParts; ``` 此外,你可以使用魔术getter: ```php <?php 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()`结果。以下示例将使用魔术方法和不使用魔术方法检索相关结果进行比较: ```php <?php 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; ``` 手动获取相关记录: ```php <?php 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()`: | 类型 | 描述 | 隐含方法 | | ---------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------- | | Belongs-To | 直接返回相关记录的模型实例 | findFirst | | Has-One | 直接返回相关记录的模型实例 | findFirst | | Has-Many | 返回引用模型的模型实例的集合 | find | | Has-Many-to-Many | 返回引用模型的模型实例的集合,它隐含地与所涉及的模型进行“内部联接” | (complex query) | 您还可以使用 `count` 前缀返回一个表示相关记录计数的整数: ```php <?php use Store\Toys\Robots; $robot = Robots::findFirst(2); echo 'The robot has ', $robot->countRobotsParts(), " parts\n"; ``` ### 别名关系 为了更好地解释别名的工作原理,让我们检查以下示例: `robots_similar` 表具有定义哪些机器人与其他机器人相似的功能: ```sql 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`都与模型机器人有关: ![](https://docs.phalconphp.com/images/content/models-relationships-eer-1.png) 映射此表及其关系的模型如下: ```php <?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 <?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? ``` 别名允许我们重命名这两个关系来解决这些问题: ```php <?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 <?php $robotsSimilar = RobotsSimilar::findFirst(); // Returns the related record based on the column (robots_id) $robot = $robotsSimilar->getRobot(); $robot = $robotsSimilar->robot; $robot = $robotsSimilar->getRelated('Robot'); // Returns the related record based on the column (similar_robots_id) $similarRobot = $robotsSimilar->getSimilarRobot(); $similarRobot = $robotsSimilar->similarRobot; $similarRobot = $robotsSimilar->getRelated('SimilarRobot'); ``` #### Magic Getters vs.明确的方法 大多数具有自动完成功能的IDE和编辑器在使用魔法getter(方法和属性)时都无法推断出正确的类型。要解决这个问题,您可以使用类docblock来指定可用的魔术操作,从而帮助IDE生成更好的自动完成: ```php <?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 <?php use Phalcon\Mvc\Model; // Companies have invoices issued to them (paid/unpaid) // Invoices model class Invoices extends Model { } // Companies model class Companies extends Model { public function initialize() { // All invoices relationship $this->hasMany( 'id', 'Invoices', 'inv_id', [ 'alias' => 'Invoices' ] ); // Paid invoices relationship $this->hasMany( 'id', 'Invoices', 'inv_id', [ 'alias' => 'InvoicesPaid', 'params' => [ 'conditions' => "inv_status = 'paid'" ] ] ); // Unpaid invoices relationship + bound parameters $this->hasMany( 'id', 'Invoices', 'inv_id', [ 'alias' => 'InvoicesUnpaid', 'params' => [ 'conditions' => "inv_status <> :status:", 'bind' => ['status' => 'unpaid'] ] ] ); } } ``` 此外,在从模型对象访问关系时,可以使用 `getRelated()` 的第二个参数来进一步过滤或排序关系: ```php <?php // Unpaid Invoices $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'"] ); // Also ordered $unpaidInvoices = $company->getRelated( 'Invoices', [ 'conditions' => "inv_status = 'paid'", 'order' => 'inv_created_date ASC', ] ); ``` ## 虚拟外键 默认情况下,关系不像数据库外键,也就是说,如果您尝试在引用的模型中插入/更新值而没有有效值,Phalcon将不会生成验证消息。您可以通过在定义关系时添加第四个参数来修改此行为。 可以更改RobotsPart模型以演示此功能: ```php <?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' => 'The part_id does not exist on the Parts model' ] ] ); } } ``` 如果更改 `belongsTo()` 关系以充当外键,它将验证在这些字段上插入/更新的值在引用的模型上是否具有有效值。同样,如果更改了`hasMany()`/`hasOne()` ,它将验证如果在引用的模型上使用该记录,则无法删除记录。 ```php <?php 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', ] ] ); } } ``` 可以设置虚拟外键以允许空值,如下所示: ```php <?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' => 'The part_id does not exist on the Parts model', ] ] ); } } ``` ### 级联/限制操作 默认情况下充当虚拟外键的关系会限制记录的创建/更新/删除以维护数据的完整性: ```php <?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, ] ] ); } } ``` 如果删除主记录(robot),上面的代码设置为删除所有引用的记录(部分)。 ## 存储相关记录 Magic属性可用于存储记录及其相关属性: ```php <?php // 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(); ``` 将记录及其相关记录保存在有多个关系中: ```php <?php // 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(); ``` 保存album和artist同时隐含地使用事务,因此如果保存相关记录出现任何问题,父级也不会保存。消息将传递回用户以获取有关任何错误的信息。 注意:无法通过重载以下方法添加相关实体: * `Phalcon\Mvc\Model::beforeSave()` * `Phalcon\Mvc\Model::beforeCreate()` * `Phalcon\Mvc\Model::beforeUpdate()` 您需要重载`Phalcon\Mvc\Model::save()`才能在模型中工作。 ## 对结果集的操作 如果结果集由完整对象组成,则它可以对记录执行操作: ### 更新相关记录 不要这样做: ```php <?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 <?php $robots->getParts()->update( [ 'stock' => 100, 'updated_at' => time(), ] ); ``` `update` 还接受匿名函数来过滤必须更新的记录: ```php <?php $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; } ); ``` ### 删除相关记录 不要这样做: ```php <?php $parts = $robots->getParts(); foreach ($parts as $part) { if ($part->delete() === false) { $messages = $part->getMessages(); foreach ($messages as $message) { echo $message; } break; } } ``` 你可以这样做: ```php <?php $robots->getParts()->delete(); ``` `delete()` 还接受匿名函数来过滤必须删除的记录: ```php <?php // Delete only whose stock is greater or equal than zero $robots->getParts()->delete( function ($part) { if ($part->stock < 0) { return false; } return true; } ); ```