AcitveReocrd事件和關聯操做

ActiveRecord預約義的事件,都在 yiidbBaseActiveRecord 中進行了明確:html


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) 部分的內容。linux

此外,做爲ActiveRecord類的祖宗, yiibaseModel 類也定義了2個事件:ios


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

雖然這個事件觸發得早,可是實際使用中,這個事件使用頻率不高。 僅是由於有的代碼不得不在初始化階段執行,因此才提供了這個事件。 並且,這個事件因爲所處階段特殊,不像有的事件,能夠有必定的替代性。yii

好比, EVENT_AFTER_VALIDATE 和 EVENT_BEFORE_UPDATE 儘管涇渭分明, 可是因爲是相繼觸發,因此某些狀況下能夠在必定程度上互相替代。可是, 上述10個事件中,僅有 EVENT_INIT 是在初始化階段觸發。因此,其具備不可替代性。ide

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 。

這樣子代碼功能界限清晰,便於查找、修改和擴展。 缺點是可能須要多看幾個方法才能瞭解整個業務流程。實際中咱們多采用這種方法。

在實現數據庫記錄的關聯操做時,第一步就是要利用上述的各類事件,來產生關聯性。 其次,是要把這些關聯性綁死在一塊兒。也就是用數據庫的事務。具體的原理, 參考 事務 部分的內容。

下面,咱們以上面提到的博客文章新增一個評論爲例,講解如何實現關聯操做。

聲明須要事務支持的操做

在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() 來啓用事務。 事務的原理,請看 事務 部分的內容。

在事件響應中寫入關聯操做

接下來,咱們在關聯的事件,如 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() 中聲明要事務支持的操做類型,好比上面的例子, 聲明的是插入操做。

  • 在合適事件響應函數中,寫下關聯操做代碼。

相關文章
相關標籤/搜索