Calculated field & Cascading update
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:
-
On a Ticket, you want the time spent on all of its WorkOrders
-
On a Ticket, you want the count of manually added CIs.
-
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
This is one strategy to implement such “calculated” field. We will store it in database. In order to ensure that it is always accurate, you need to identify when it must be recomputed and do it. For this 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 a flag to say it. -
AfterInsert
,AfterUpdate()
,AfterDelete
: If needed (based on above set flag), 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 notOnXXX()
, 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 empy. -
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 to identify 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 theedit 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
-
public function ComputeTimeSpent() { $iSum = 0; $oWorkOrderSet = $this->Get('workorder_list'); while($oWorkOrder = $oWorkOrderSet->Fetch()) { $iSum += $oWorkOrder->Get('time_spent'); } $this->Set('time_spent', $iSum); } public function OnUpdate() { $aChanges = $this->ListChanges(); if (array_key_exists('workorder_list', $aChanges)) { $this->ComputeTimeSpent(); } } public function GetInitialStateAttributeFlags($sAttCode, &$aReasons = array()) { // 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 = array(), $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 fieldticket_id
has been modified then store temporarily the previous value. -
AfterInsert
,AfterUpdate
,AfterDelete
: Force recomputation of current and former Ticket.
- class::WorkOrder
-
public function UpdateTicket($id) { if ($id != 0) { $oObject = MetaModel::GetObject('Ticket', $id, false); // FYI: MetaModel::GetObject('Ticket', 0); generates a FatalError if (is_object($oObject)) // in case the user is not allowed to see the object { $oObject->ComputeTimeSpent(); $oObject->DBUpdate(); } } } public function OnUpdate() { $aChanges = $this->ListChanges(); if (array_key_exists('ticket_id', $aChanges)) { // store in the WorkOrder memory object the previous value $this->i_PreviousTicketId = $this->GetOriginal('ticket_id'); } if (array_key_exists('time_spent', $aChanges)) { // record in the WorkOrder memory object that time spent was changed $this->i_TimeChanged = true; } } public function AfterUpdate() { // The WorkOrder is updated in DB and Time spent was changed, if (isset($this->i_TimeChanged)) { // 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 (isset($this->i_PreviousTicketId )) $this->UpdateTicket($this->i_PreviousTicketId); } public 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')); } public 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->i_PreviousTicketId = $this->GetOriginal('ticket_id'); } } public function AfterDelete() { // If a recomputation of the former Ticket is needed, let's do it if (isset($this->i_PreviousTicketId )) $this->UpdateTicket($this->i_PreviousTicketId); }
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
2_6_0/customization/cascade-update.txt · Last modified:
2020/02/05 11:42 by 127.0.0.1