多字段运算和级联更新

决条件:您必须熟悉教程中使用的语法 并且已经创建了一个扩展.

学习:根据相关对象更新对象

水平:高级

域:PHP,  Automation 

最低版本:2.3.0

设您要在A类上定义一个“计算”字段,这依赖于在B类对象上定义的数据,这比仅一个ExternalField要复杂得多。举些例子:

  1. 在工单上,您需要花费在所有WorkOrder上的时间
  2. 在工单上,您需要手动添加的配置项的计数。
  3. 在FunctionalCI上,您需要一个Location字段,该字段仅是PhysicalDevice的位置,以及基于Logical正在运行的物理设备的位置在Logical Device上的计算位置。

您希望能够在“已计算”字段上使用搜索。
您希望能够针对该“计算”字段运行审计。

请注意,在更新大量B类对象时,此类“计算”字段会在性能上引发风险

通用策略

这是实现这种“计算”字段的通用策略。

  1. 为了能够在其上使用查询,我们必须将其存储在数据库中,因此使其成为持久字段。
  2. 为了确保它总是准确的,您需要确定何时必须重新计算它并执行。为此,我们将定义覆盖这些功能:

在A班

  • ComputeValues():根据其他与字段相关的对象,计算“已计算”字段价值。
  • GetInitialStateAttributeFlags():指定在创建表单中隐藏“计算所得”字段
  • GetAttributeFlag():指定修改时“计算所得”字段为只读

在依赖B类上

  • OnUpdate(),OnDelete():检查字段是否已被修改(这将使影响度为A类的“计算”字段),设置一个内部变量以在AfterUpdate()AfterDelete()中记住它。
  • AfterInsert,AfterUpdate(),AfterDelete:如果需要(基于上面设置的内部变量),则对具有受“变更”影响的“已计算”字段的所有对象A强制重新计算。

提示与解释

  • 我们使用AfterXXX()将变更设置为级联,而不使用OnXXX(),因为否则远程对象尚未保存在数据库中,因此基于ComputeValues的计算将使用数据库中发现的不准确的数据。
  • 我们使用OnUpdate()来检查是否需要级联和变更,因为在更新后删除数组$ this→ListChanges()为空。
  • 通常会调用ComputeValues(),但是在将对象加载到内存中时不会调用。

用例:总和

在此用例中,我们希望在工单上求和该工单的所有工作订单上花费的时间。

  • 首先,我们需要在WorkOrder和工单类上添加一个time_spent整数属性
  • 为了确保工单:: time_spent始终准确,我们需要确定何时必须重新计算它:
    • 工单更新时
    • 创建工作订单后→我们需要重新计算其工单
    • 更新工作订单后→我们需要重新计算其先前版本和新版本工单
    • 删除工作订单后→我们需要重新计算其以前的工单
  • 然后定义重写这些功能:

在工单类上

  • ComputeTimeSpent():基于WorOrders的“ time_spent”来计算价值的“ time_spent”字段。
  • OnUpdate():无法从默认数据模型中的工单修改工作单列表,但是如果在编辑模式下启用了该工作单,则需要此职能。
  • GetInitialStateAttributeFlags():指定在工单创建表单上隐藏“计算的”字段
  • GetAttributeFlag():指定在工单修改时,“计算的”字段为只读
class::Ticket

protected function ComputeTimeSpent() { $iSum = 0; $oWorkOrderSet = $this->Get('workorders_list'); while($oWorkOrder = $oWorkOrderSet->Fetch()) { $iSum += $oWorkOrder->Get('time_spent'); } $this->Set('time_spent', $iSum); } protected function OnUpdate() { $aChanges = $this->ListChanges(); if (('workorders_list', $aChanges)) { $this->ComputeTimeSpent(); } } public function GetInitialStateAttributeFlags($sAttCode, &$aReasons = ()) { // Hide the calculated field in object creation form if (($sAttCode == 'time_spent')) return(OPT_ATT_HIDDEN | parent::GetInitialStateAttributeFlags($sAttCode, $aReasons)); return parent::GetInitialStateAttributeFlags($sAttCode, $aReasons); } public function GetAttributeFlags($sAttCode, &$aReasons = (), $sTargetState = '') { // Force the computed field to be read-only, preventing it to be written if (($sAttCode == 'time_spent')) return(OPT_ATT_READONLY | parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState)); return parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState); }

在class的工作指令

  • OnUpdate,OnDelete:如果字段工单_id已被修改,则暂时存储先前的价值。
  • AfterInsert,AfterUpdate,AfterDelete:强制重新计算当前和以前的工单。
class::WorkOrder
 
 
protected function UpdateTicket($id)
{
   if ($id != 0) 
   {
      $oObject = MetaModel::GetObject('Ticket', $id, false);
      // FYI: MetaModel::GetObject('Ticket', 0); generates a FatalError
      if (($oObject))  // in case the user is not allowed to see the object
      {
         $oObject->ComputeTimeSpent();
         $oObject->DBUpdate();
      }
   }              
}
 
protected function OnUpdate()
{
  $aChanges = $this->ListChanges();
  if (('ticket_id', $aChanges))
  {
      // store in the WorkOrder memory object the previous value
      $this->iPreviousTicketId = $this->GetOriginal('ticket_id');
  }
  if (('time_spent', $aChanges))
  {
      // record in the WorkOrder memory object that time spent was changed
      $this->bTimeChanged = true;
  }
}
 
protected function AfterUpdate()
{
   // The WorkOrder is updated in DB and Time spent was changed,
   if (($this->bTimeChanged))
   {
       // we need to recompute TimeSpent on the Ticket
       $this->UpdateTicket($this->Get('ticket_id'));
   }
   // If there was a "former" Ticket then we also need to update it
   if (($this->iPreviousTicketId )) $this->UpdateTicket($this->iPreviousTicketId);
}
 
protected function AfterInsert()
{
   // A new Workorder was created with time_spent, so let's recompute the Ticket 
   if ($this->Get('time_spent') > 0)   $this->UpdateTicket($this->Get('ticket_id'));
}
 
protected function OnDelete()
{
   // If the deleted Workorder had some time spent set, 
   // then let's flag that a recomputation is needed, and store the former Ticket id for later use
   if ($this->GetOriginal('time_spent') > 0)   
   {
      $this->iPreviousTicketId = $this->GetOriginal('ticket_id');
   }
}
 
protected function AfterDelete()
{
   // If a recomputation of the former Ticket is needed, let's do it
   if (($this->iPreviousTicketId )) $this->UpdateTicket($this->iPreviousTicketId);
}

通过检查是否在OnXXXX()职能中修改了WorkOrder :: time_spent,然后设置一个标志并在AfterXXX()中使用它以仅在真正需要时才调用UpdateTicket()来改进此示例。

原贴链接:https://www.itophub.io/wiki/page?id=2_7_0%3Acustomization%3Acascade-update


Calculated field & Cascading update

Prerequisite: You must be familiar with the Syntax used in Tutorials and have already created an extension.

learning:
Update an object based on related objects
level:
Advanced
domains:
PHPAutomation
min version:
2.3.0

Assuming you want to define a “calculated” field on Class A, which would depend on data defined on class(es) B object(s), something more complex than just an ExternalField. For examples:

  1. On a Ticket, you want the time spent on all of its WorkOrders

  2. On a Ticket, you want the count of manually added CIs.

  3. On FunctionalCI, you want a Location field, which would be simply the location for PhysicalDevice, and a computed location on Logical Device, based on the location of the Physical device on which the Logical is running.

You want to be able to search on that “calculated” field.
You want to be able to run audit against that “calculated” field.

Note that such “calculated” field induces risks on performance while updating large number of Class B objects

Generic Strategy

Here is the generic strategy to implement such “calculated” field.

  1. To be able to query on it, we must store it in database, so make it a persistent field.

  2. In order to ensure that it is always accurate, you need to identify when it must be recomputed and do it. For this we will define/overwrite those functions:

On Class A

  • ComputeValues(): calculate the “calculated” field value based on other fields / related objects.

  • GetInitialStateAttributeFlags(): to specify that the “calculated” field is hidden in creation form

  • GetAttributeFlag(): to specify that the “calculated” field is read-only on modification

On depending Classes B

  • OnUpdate(), OnDelete(): Check if a field has been modified which would impact a “calculated” field of Class A, set an internal variable to remember it within the AfterUpdate() / AfterDelete().

  • AfterInsert,AfterUpdate(),AfterDelete: If needed (based on the internal variable set above), force recomputation on all objects A which have a “calculated” field which are impacted by that change.

Tips & explanations

  • We use AfterXXX() to cascade the change, and not OnXXX(), because otherwise the remote object would not yet be saved in database, so the computation based on ComputeValues would use the inaccurate data found in database.

  • We use OnUpdate() to check if we need to cascade the change, because after update/delete the array $this→ListChanges() is empty.

  • ComputeValues() is called quite often, but not when loading the object in memory.

Use Case: Sum

In this use case, we want to Sum on a Ticket the timespent on all WorkOrders of this Ticket.

  • First we need to add a time_spent integer attribute on WorkOrder and Ticket classes

  • In order to ensure that Ticket::time_spent is always accurate, we need toidentify when it must be recomputed:

    •  

    When the Ticket is updated

    •  

    When a WorkOrder is created → we need to recompute its Ticket

    •  

    When a WorkOrder is updated → we need to recompute its former and its new Ticket

    •  

    When a WorkOrder is deleted → we need to recompute its former Ticket

  • Then define/overwrite those functions:

On Class Ticket

  • ComputeTimeSpent(): calculate the “time_spent” field value based on “time_spent” of WorOrders.

  • OnUpdate(): the list of Workorders cannot be modified from a Ticket in the default datamodel, but if this is enable with the edit mode then this function is needed.

  • GetInitialStateAttributeFlags(): to specify that the “calculated” field is hidden on Ticket creation form

  • GetAttributeFlag(): to specify that the “calculated” field is read-only on Ticket modification

class::Ticket
 

protected function ComputeTimeSpent() { $iSum = 0; $oWorkOrderSet = $this->Get('workorders_list'); while($oWorkOrder = $oWorkOrderSet->Fetch()) { $iSum += $oWorkOrder->Get('time_spent'); } $this->Set('time_spent', $iSum); } protected function OnUpdate() { $aChanges = $this->ListChanges(); if (('workorders_list', $aChanges)) { $this->ComputeTimeSpent(); } } public function GetInitialStateAttributeFlags($sAttCode, &$aReasons = ()) { // Hide the calculated field in object creation form if (($sAttCode == 'time_spent')) return(OPT_ATT_HIDDEN | parent::GetInitialStateAttributeFlags($sAttCode, $aReasons)); return parent::GetInitialStateAttributeFlags($sAttCode, $aReasons); } public function GetAttributeFlags($sAttCode, &$aReasons = (), $sTargetState = '') { // Force the computed field to be read-only, preventing it to be written if (($sAttCode == 'time_spent')) return(OPT_ATT_READONLY | parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState)); return parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState); }

On Class WorkOrder

  • OnUpdate, OnDelete: If field ticket_id has been modified then store temporarily the previous value.

  • AfterInsert,AfterUpdate,AfterDelete: Force recomputation of current and former Ticket.

class::WorkOrder
 

protected function UpdateTicket($id) { if ($id != 0) { $oObject = MetaModel::GetObject('Ticket', $id, false); // FYI: MetaModel::GetObject('Ticket', 0); generates a FatalError if (($oObject)) // in case the user is not allowed to see the object { $oObject->ComputeTimeSpent(); $oObject->DBUpdate(); } } } protected function OnUpdate() { $aChanges = $this->ListChanges(); if (('ticket_id', $aChanges)) { // store in the WorkOrder memory object the previous value $this->iPreviousTicketId = $this->GetOriginal('ticket_id'); } if (('time_spent', $aChanges)) { // record in the WorkOrder memory object that time spent was changed $this->bTimeChanged = true; } } protected function AfterUpdate() { // The WorkOrder is updated in DB and Time spent was changed, if (($this->bTimeChanged)) { // we need to recompute TimeSpent on the Ticket $this->UpdateTicket($this->Get('ticket_id')); } // If there was a "former" Ticket then we also need to update it if (($this->iPreviousTicketId )) $this->UpdateTicket($this->iPreviousTicketId); } protected function AfterInsert() { // A new Workorder was created with time_spent, so let's recompute the Ticket if ($this->Get('time_spent') > 0) $this->UpdateTicket($this->Get('ticket_id')); } protected function OnDelete() { // If the deleted Workorder had some time spent set, // then let's flag that a recomputation is needed, and store the former Ticket id for later use if ($this->GetOriginal('time_spent') > 0) { $this->iPreviousTicketId = $this->GetOriginal('ticket_id'); } } protected function AfterDelete() { // If a recomputation of the former Ticket is needed, let's do it if (($this->iPreviousTicketId )) $this->UpdateTicket($this->iPreviousTicketId); }

This example was improved by checking if the WorkOrder::time_spent was modified in OnXXXX() function, then set a flag and use it in AfterXXX() to call UpdateTicket() only when truly needed

标签:
由 superadmin 在 2020/08/27, 17:27 创建
    

需要帮助?

如果您需要有关XWiki的帮助,可以联系:

深圳市艾拓先锋企业管理咨询有限公司