深入理解Yii2
导读
Yii 是什么
Yii2.0 的亮点
背景知识
如何阅读本书
Yii基础
属性(Property)
事件(Event)
行为(Behavior)
Yii约定
Yii应用的目录结构和入口脚本
别名(Alias)
Yii的类自动加载机制
环境和配置文件
配置项(Configuration)
Yii模式
MVC
依赖注入和依赖注入容器
服务定位器(Service Locator)
请求与响应(TBD)
路由(Route)
Url管理
请求(Reqeust)
Web应用Request
Yii与数据库(TBD)
数据类型
事务(Transaction)
AcitveReocrd事件和关联操作
乐观锁与悲观锁
附录
附录1:Yii2.0 对比 Yii1.1 的重大改进
附录2:Yii的安装
本文档使用 MrDoc 发布
-
+
首页
AcitveReocrd事件和关联操作
> ActiveRecord预定义的事件,都在 yiidbBaseActiveRecord 中进行了明确: ``` abstract class BaseActiveRecord extends Model implements ActiveRecordInterface { const EVENT_INIT = 'init'; // 初始化对象时触发 const EVENT_AFTER_FIND = 'afterFind'; // 执行查询结束时触发 const EVENT_BEFORE_INSERT = 'beforeInsert'; // 插入结束时触发 const EVENT_AFTER_INSERT = 'afterInsert'; // 插入之前触发 const EVENT_BEFORE_UPDATE = 'beforeUpdate'; // 更新记录前触发 const EVENT_AFTER_UPDATE = 'afterUpdate'; // 更新记录后触发 const EVENT_BEFORE_DELETE = 'beforeDelete'; // 删除记录前触发 const EVENT_AFTER_DELETE = 'afterDelete'; // 删除记录后触发 // ... ... } ``` > 上述常量,定义了ActiveRecord对象常用的几个事件。这是预定义事件,我们可以直接拿来 用。事件的定义具体看 _事件(Event)_ 部分的内容。 > 此外,作为ActiveRecord类的祖宗, yiibaseModel 类也定义了2个事件: ``` class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayable { const EVENT_BEFORE_VALIDATE = 'beforeValidate'; // 在验证之前触发 const EVENT_AFTER_VALIDATE = 'afterValidate'; // 在验证之后触发 // ... ... } ``` > 因此,上述总共10个事件,可供开发者在写入业务流程时使用。 > 从上述事件来看,可以看出大部分事件是分别以before和after打头的成对事件。 有些是读操作时才会触发的事件,有些是写操作时发生的事件。 > 而且,写与写之间也是相互区别的。比如,增、改、删3个写操作, 都各自有一对事件先后在不同场景下触发。但这3种写操作不会被同时触发。 ### 初始化事件 > 首先,第一个事件,无可争议的,是 EVENT_INIT 。这是由 yii\\base\\Object 所决定的。该事件在init() 方法中被触发。而我们在 _属性(Property)_ 中已经说过这个方法是最早调用的几个方法之一。具体代码: ``` public function init() { parent::init(); // 这里触发EVENT_INIT事件 $this->trigger(self::EVENT_INIT); } ``` > 虽然这个事件触发得早,但是实际使用中,这个事件使用频率不高。 仅是因为有的代码不得不在初始化阶段执行,所以才提供了这个事件。 而且,这个事件由于所处阶段特殊,不像有的事件,可以有一定的替代性。 > 比如, EVENT_AFTER_VALIDATE 和 EVENT_BEFORE_UPDATE 尽管泾渭分明, 但是由于是相继触发,所以某些情况下可以在一定程度上互相替代。但是, 上述10个事件中,仅有 EVENT_INIT 是在初始化阶段触发。所以,其具有不可替代性。 > EVENT_INIT 事件通常用于初始化一些东西,从模块化的角度, 可以简单看成是将当前类的 init()方法的内容, 作为一个Event Handler单独划分为一个模块。 ### AfterFind事件 > EVENT_AFTER_FIND 事件在完成查询后触发,注意该事件少有地没有对应的Before事件。 > 另外一个区别于其他事件的不同在于,该事件并非由 ActiveRecord 自身触发。 而是由yii\\db\\ActiveQuery 触发。准确的触发时点,是在查询完成后, 向ActiveRecord填充字段全部内容后触发。 > 具体代码在 yii\\db\\ActiveQuery::populate() ``` // 该方法为ActiveQuery将查询到的内容 $rows 填充到ActiveReocrd中去的方法 public function populate($rows) { if (empty($rows)) { return []; } $models = $this->createModels($rows); if (!empty($this->join) && $this->indexBy === null) { $models = $this->removeDuplicatedModels($models); } if (!empty($this->with)) { $this->findWith($this->with, $models); } if (!$this->asArray) { // 重点在这个foreach里面的afterFind(), // afterFind()不干别的,就是专门调用 // $this->trigger(self::EVENT_AFTER_FIND) 来触发事件的。 foreach ($models as $model) { $model->afterFind(); } } return $models; } ``` > 上面的代码我们可以看出,在完成查询之后,查询到了多少个记录, 就会触发多少次实例级别的EVENT_AFTER_FIND 事件。 事件的级别,请看 _事件(Event)_ 部分的内容。 > EVENT_AFTER_FIND 事件,用于查询后一些内容的填充。比如,有一个ActiveRecord, 专门用于表示博客文章,那么通常他有一个字段,用于表示发布的确切时间。 > 假设客户希望在前台显示文章时,不直接显示确切时间,而是显示如3分钟前 2个月前之类的相对时间。那么,我们就需要有一个将绝对时间转化成相对时间的过程。 > 那么,就可以把这个转换过程的代码,写在 EVENT_AFTER_FIND 事件的Event Handler里。 ### 验证事件 > 验证事件是在验证时先后触发的2个事件,这2个事件均由 yii\\base\\Model::validate 触发: ``` public function validate($attributeNames = null, $clearErrors = true) { if ($clearErrors) { $this->clearErrors(); } // 这里的 beforeValidate() 会调用 // $this->trigger(self::EVENT_BEFORE_VALIDATE, $event) // 来触发 EVENT_BEFORE_VALIDATE 事件。 if (!$this->beforeValidate()) { return false; } // 下面是后续的验证代码,这里不用过多关注 $scenarios = $this->scenarios(); $scenario = $this->getScenario(); if (!isset($scenarios[$scenario])) { throw new InvalidParamException("Unknown scenario: $scenario"); } if ($attributeNames === null) { $attributeNames = $this->activeAttributes(); } foreach ($this->getActiveValidators() as $validator) { $validator->validateAttributes($this, $attributeNames); } // 这里的 afterValidate() 会调用 // $this->trigger(self::EVENT_AFTER_VALIDATE) // 来触发 EVENT_AFTER_VALIDATE 事件。 $this->afterValidate(); return !$this->hasErrors(); } ``` > 这两个事件正如其名称所表示的,触发顺序为先 EVENT_BEFORE_VALIDATE 后 EVENT_AFTER_VALIDATE 。 > 这两个事件中, EVENT_BEFORE_VALIDATE 常用于验证前的一些规范化处理。 仍以博客文章的发布时间字段为例,在接收用户输入时, 我们的应用接收一个字符类似2015年3月8日之类的字符串。 > 但是数据库中我们一般并不以字符串形式保存时间,而是使用一个整型字段来保存。 这主要涉及存储空间,日期比较和排序,检索效率等数据库优化问题,具体不展开。 反正我们就是想把时间以整型形式进行保存。 > 那么,在验证用户输入之前,我们就需要将字符串类型的日期时间, 转换成整型类型的日期时间。否则的话,验证就通不过。 > 这个转换过程,就可以写在 EVENT_BEFORE_VALIDATE 的 Event Handler里面。 > EVENT_BEFORE_VALIDATE 还有一个比较吸引人的特性, 它的Event Handler可以返回一个 boolean 值,当为 false 时, 表示后续的验证没必要进行了: ``` public function beforeValidate() { // 创建一个 ModelEvent,并交给 trigger 在触发事件时使用 $event = new ModelEvent; $this->trigger(self::EVENT_BEFORE_VALIDATE, $event); return $event->isValid; } ``` > 上面的代码中, trigger() 将传入的第二个 $event 传递给 Event Handler, 使得相关的这些个 Event Handler 可以在必要时修改 $event->isValid 的值。 以此来决定是否可以取消后续的验证,直接视为验证没有通过。 > EVENT_AFTER_VALIDATE 通常用于用户输入验证后的一些处理。比如, 用于写入操作前的一些通用处理。因为后头接下来的事件, 会分成插入、更新等独立事件。如果有一些写入前的通用处理,放在EVENT_AFTER_VALIDATE 阶段是比较合适的。 > 至于验证通过与否,与 EVENT_AFTER_VALIDATE 事件没有关系,只要执行完所有验证了, 这个事件就会被触发。而且,该事件的Event Handler没有返回值,无法干预验证结果。 ### 写事件 > 写事件是指插入、更新、删除等写入操作时触发的事件。一般情况下, 验证事件先于写事件被触发。 > 但这不是绝对的。Yii允许在执行写操作时,不调用 validate() 进行验证, 也就不触发验证事件。 > 下面,我们以更新操作update为例,来分析写事件。 > 首先,来看看 yii\\db\\BaseActiveRecord 里的有关代码: ``` public function save($runValidation = true, $attributeNames = null) { // insert() 和 update() 具体实现由ActiveRecord定义 if ($this->getIsNewRecord()) { return $this->insert($runValidation, $attributeNames); } else { return $this->update($runValidation, $attributeNames) !== false; } } // updateInternal() 由 update() 调用, // 类似的有deleteInternal() ,由ActiveRecord定义,这里略去。 protected function updateInternal($attributes = null) { // beforeSave() 会触发相应的before事件 // 而且如果beforeSave()返回false,就可以中止更新过程。 if (!$this->beforeSave(false)) { return false; } $values = $this->getDirtyAttributes($attributes); // 没有字段有修改,那么实际上是不需要更新的。 // 因此,直接调用afterSave()来触发相应的after事件。 if (empty($values)) { $this->afterSave(false, $values); return 0; } // 以下为实际更新操作,不必细究。 $condition = $this->getOldPrimaryKey(true); $lock = $this->optimisticLock(); if ($lock !== null) { $values[$lock] = $this->$lock + 1; $condition[$lock] = $this->$lock; } $rows = $this->updateAll($values, $condition); if ($lock !== null && !$rows) { throw new StaleObjectException('The object being updated is outdated.'); } $changedAttributes = []; foreach ($values as $name => $value) { $changedAttributes[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; $this->_oldAttributes[$name] = $value; } // 重点看这里,触发了事件 $this->afterSave(false, $changedAttributes); return $rows; } // 下面的beforeSave()和afterSave() 会根据判断是更新操作还是插入操作, // 以此来决定是触发 INSERT 事件还是 UPDATE 事件。 public function beforeSave($insert) { $event = new ModelEvent; // $insert 为 true 时,表示当前是插入操作,是个新记录,要触发INSERT事件 // $insert 为 false时,表示当前是插入操作,是个新记录,要触发INSERT事件 $this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event); return $event->isValid; } public function afterSave($insert, $changedAttributes) { // $insert 为 true 时,表示当前是插入操作,是个新记录,要触发INSERT事件 // $insert 为 false时,表示当前是插入操作,是个新记录,要触发INSERT事件 $this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE, new AfterSaveEvent([ 'changedAttributes' => $changedAttributes ])); } ``` > 就写操作而言,表面上调用的是 ActiveRecord 的 update() insert() delete() 等方法。 > 但是,更新最终调用的是 BaseActiveRecord::updateInteranl() , 插入最终调用的是ActiveRecord::insertInternal() , 而删除最终调用的是 ActiveRecord::deleteInternal() 。 > 这些 internal 方法,会触发相应的写事件,但不会调用验证方法, 也不会触发验证事件。验证方法 validation() 由 update() insert() 调用。 因此,验证事件也由这两个方法触发。 > 而且,这些 update() insert() 可以选择不进行验证,在压根不触发验证事件的情况下,就可以完成写操作。 > 因此,虽然 EVENT_AFTER_VALIDATE 和 EVENT_BEFORE_UPDATE 相继发生, 在使用上有时可以有一定程度的替代。但是,其实两者是有严格界限的。 原因就是验证事件可能在写操作过程中不被触发。 > 此外,删除过程不触发验证事件。都要删掉的东西了,还需要验证么? > 对于 internal 方法们,只是触发了相应的before和after写事件。 > 其中,before事件的Event Handler可以通过将 $event->isValid 设为 false 来中止写操作。 > 与在验证事件阶段中止时,视为验证没通过不同,这里的中止视为写操作失败。 > 与验证事件阶段类似,after事件时由于生米已成熟饭,再也无法干预写操作的结果。 ### 响应事件 > 前面提到的诸多预定义事件,为我们开发提供了方便。基本上使用这些预定义事件, 就可以满足各种开发需求了。 > 但是凡事总有例外,特别是对于业务逻辑复杂的情况。 比如,默认的删除事件,会在确确实实地要从数据表中删除记录时触发。 但是,有的时候,我们并非真的想从数据表中删除记录,我们可能使用一个类似于状态 的字段,在想要删除时,只是将记录的状态标记为删除。 > 这种需求并不少见。这样便于我们在后悔时,可以恢复删除。 > 从实质上是看,这其实是一个更新操作。那么预定义的 EVENT_BEFORE_DELETE 和 EVENT_AFTER_DELETE就不适用了。 > 对此,我们可以自己定义事件来使用。具体的方法可以参见 _事件(Event)_ 部分的内容。 > 大致的代码可以是这样的: ``` class Post extends \\yii\\db\\ActiveRecord { // 第一步:定义自己的事件 const EVENT_BEFORE_MARK_DELETE = 'beforeMarkDelete'; const EVENT_AFTER_MARK_DELETE = 'afterMarkDelete'; // 第二步:定义Event Handler public function onBeforeMarkDelete () { // ... do sth ... } // 第三步:在初始化阶段绑定事件和Event Handler public function init() { parent::init(); $this->trigger(self::EVENT_INIT); // 完成绑定 $this->on(self::EVENT_BEFORE_MARK_DELETE, [$this, 'onBeforeMarkDelete']); } // 第四步:触发事件 public function beforeSave($insert) { // 注意,重载之后要调用父类同名函数 if (parent::beforeSave($insert)) { $status = $this->getDirtyAttributes(['status']); // 这个判断意会即可 if (!empty($status) && $status['status'] == self::STATUS_DELETE) { // 触发事件 $this->trigger(self::EVENT_BEFORE_MARK_DELETE); } return true; } else { return false; } } } ``` > 上面的代码理解个大致流程就OK了,不用细究。 > 在事件的响应上,我们有2个方法来写入我们的代码。 > 最直观的方式,是使用 _事件(Event)_ 中介绍的 Event Handler。也就是上面代码展现的, 为类定义一个成员函数,作为Event Handler。同时,在类的构造函数或初始化方法中, 把事件和Event Handler绑定起来。最后,在合适的时候,触发事件即可。 > 另一种方式,是直接重载上面多次提到的各种 beforeSave() afterSave() beforeValidate()afterValidate() 等方法。比如,上面的例子可以改成: ``` class Post extends \\yii\\db\\ActiveRecord { // 不需要定义自己的事件 //const EVENT_BEFORE_MARK_DELETE = 'beforeMarkDelete'; //const EVENT_AFTER_MARK_DELETE = 'afterMarkDelete'; // 不需要定义Event Handler //public function onBeforeMarkDelete () { // ... do sth ... //} // 不需要绑定事件和Event Handler //public function init() //{ // parent::init(); // $this->trigger(self::EVENT_INIT); // $this->on(self::EVENT_BEFORE_MARK_DELETE, [$this, 'onBeforeMarkDelete']); //} // 只需要重载 public function beforeSave($insert) { // 注意,重载之后要调用父类同名函数 if (parent::beforeSave($insert)) { $status = $this->getDirtyAttributes(['status']); // 这个判断意会即可 if (!empty($status) && $status['status'] == self::STATUS_DELETE) { // 不需要触发事件 //$this->trigger(self::EVENT_BEFORE_MARK_DELETE); // 但是需要把原来 Event Handler的内容放到这里来 // ... do sth ... } return true; } else { return false; } } } ``` > 对比来看,重载 beforeSave() 的方式要简洁很多。但是这种方式从严格意义上来讲, 并不是正规的事件处理机制。只不过是利用了Yii已经预先定义好的函数调用流程。 在使用中,需要格外注意的是,一定要在重载的函数中,调用父类的同名函数。否则的话, trigger() 不再被自动调用,相关事件就不会再被触发。整个类的事件机制, 就全被破坏了。 ### 关联操作 > 在实际开发中,有一种典型的场景,即对数据库某个表的某个记录进行修改时,需要对关联 的表中的相关记录做相应的修改。 > 比如,一个典型的博客,表示文章的数据表中有一个字段用于记录当前文章有多少条评论。 那么,当用户发表新评论时,另一个用于表示评论的表中,理所当然地要插入一条新记录。 不可避免的,文章表中,被评论文章所对应的记录,其评论计数字段应当加1。 > 那么这一过程怎么编程实现呢? > 最直白的方法,是在操作评论记录的代码之前(后),写入相应的增加文章评论计数的代码 。 这样好理解,但是不同功能代码的界限不清晰。 > 另一种方法,是借助事件(Event),将增加文章评论计数的代码,写到 评论ActiveReocrd的相应Event Handler中。比如, EVENT_AFTER_INSERT 。 > 这样子代码功能界限清晰,便于查找、修改和扩展。 缺点是可能需要多看几个方法才能了解整个业务流程。实际中我们多采用这种方法。 > 在实现数据库记录的关联操作时,第一步就是要利用上述的各种事件,来产生关联性。 其次,是要把这些关联性绑死在一起。也就是用数据库的事务。具体的原理, 参考 _事务(Transaction)_ 部分的内容。 > 下面,我们以上面提到的博客文章新增一个评论为例,讲解如何实现关联操作。 ### 声明需要事务支持的操作 > 在ActiveRecord中有一个方法,用于告诉Yii我们的哪些操作需要事务支持。对于插入、 更新、删除的1个或多个操作需要事务支持时,可以在这个方法中进行声明。 这个方法就是ActiveRecord::transactions() ``` class ActiveRecord extends BaseActiveRecord { // 定义插入、更新、删除操作,及表示3合1的ALL const OP_INSERT = 0x01; const OP_UPDATE = 0x02; const OP_DELETE = 0x04; const OP_ALL = 0x07; // 需要事务支持时,重载这个方法。 public function transactions() { return []; } // ... ... } ``` > 默认情况下,这个 transactions() 返回一个空数组,表示不需要任何的事务支持。 > 我们的博客文章增加评论的案例中是要用到的,那么,我们可以在评论模型 Comment 中,作如下声明: ``` public function transactions() { return [ 'addComment' => self::OP_INSERT, ]; } ``` > 这个方法所返回的数组中,元素的键,如上面的 addComment 表示场景(scenario), 元素值,表示的是操作的类型,即 OP_INSERT 等。 > ActiveRecord定义了3种可能会用到事务支持的操作 OP_INSERT OP_UPDATE OP_DELETE 分别表示插入、更新、删除。 > 可以把这3个操作两两组合作为 transactions() 所返回数组元素的值。 如,self::OP_INSERT|self::OP_UPDATE `表示插入和更新操作。 也可以直接使用`OP_ALL 表示3种操作都包含。 ### 启用事务 > 上一步中的 transactions() 被 ActiveRecord::isTransactional() 所调用: ``` // $operation就是预定义的OP_INSERT 等3种单一操作类型 public function isTransactional($operation) { // 获取当前的scenario $scenario = $this->getScenario(); $transactions = $this->transactions(); return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation); } ``` > 这个 isTransactional() 就是判断当前场景下,当前操作类型,是否已经在 transactions() 中声明为需要事务支持。 > 而这个 isTransactional() 又被各种写操作方法所调用。 在我们的博客文章新增评论的案例中,就是被 insert() 所调用: ``` public function insert($runValidation = true, $attributes = null) { if ($runValidation && !$this->validate($attributes)) { Yii::info('Model not inserted due to validation error.', __METHOD__); return false; } // 这里调用了 isTransactional(),判断当前场景下, // 插入操作是否需要事务支持 if (!$this->isTransactional(self::OP_INSERT)) { // 无需事务支持,那就直接insert了事 return $this->insertInternal($attributes); } // 以下是需要事务支持的情况,那就启用事务 $transaction = static::getDb()->beginTransaction(); try { $result = $this->insertInternal($attributes); if ($result === false) { $transaction->rollBack(); } else { $transaction->commit(); } return $result; } catch (\\Exception $e) { $transaction->rollBack(); throw $e; } } ``` > 很明显的,我们只需要在 transactions() 中声明需要事务支持的操作就足够了。 后续的怎么使声明生效的,Yii框架已经替我们写好了。 > 在上面 insert() 的代码中,通过我们的声明,Yii发现需要事务支持, 于是就调用了static::getDb()->beginTransaction() 来启用事务。 事务的原理,请看 _事务(Transaction)_ 部分的内容。 ### 在事件响应中写入关联操作 > 接下来,我们在关联的事件,如 EVENT_AFTER_INSERT 中,写入关联操作。 这里,我们就是要更新博客文章模型 Post 的评论计数字段。 > 因此,可以在评论模型 Comment 完成插入后的 EVENT_AFTER_INSERT 阶段, 写入更新Post::comment_counter 的代码。如果使用简洁形式的事件响应方式, 那么代码可以是: ``` class Comment extends \\yii\\db\\ActiveRecord { // 通过重载afterSave来“响应”事件 public function afterSave($insert) { if (parent::beforeSave($insert)) { // 新增一个评论 if ($insert) { // 关联Post的操作,评论计数字段+1 $post = Post::find($this->postId); $post->comment_counter += 1; $post->save(false); } } } } ``` > 回顾下实现关联操作的过程,其实就2步: * 先是在 transactions() 中声明要事务支持的操作类型,比如上面的例子, 声明的是插入操作。 * 在合适事件响应函数中,写下关联操作代码。
admin
2022年12月19日 14:11
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码