Store Count of 1:n relation
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:
- PHP, Automation
- min version:
- 2.3.0
This example is another specific example of that tutorial Calculated field on 1:n
Here we want to see in the details of an object A, the count of objects B which are linked to the object A through an one-to-many relationship (an external key field on class B pointing to A).
We will see two flavors of that particular case:
-
On Ticket count the associated WorkOrders
-
On User Request count of sub-UserRequests (special case where classes A and B are the same class)
That information is easily visible on the details of a single Ticket/User Request, but you have no mean to retrieve quickly all the Ticket/User Request with more that 2 WorkOrders/sub-requests or having no WorkOrders/sub-requests at all. This tutorial, will explain how to resolve that issue
WorkOrders on Ticket
In order to be able to do this:
-
We need to create a field
count_workorders_list
on the Ticket class, to store the number permanently so it can be queried. -
We have a
workorders_list
field on Ticket, providing the list of sub-request under the current one -
We have a
ticket_id
field on WorkOrder, providing the Ticket which work-order count must be updated
Lets define when we need to recompute?
-
When a Work Order is created, modified or deleted alone
-
When the list of work orders is modified with the Ticket it-self
- itop_design / classes / class@Ticket
-
<fields> <field id="workorders_list" xsi:type="AttributeLinkedSet"> <with_php_computation _delta="force">true</with_php_computation> </field> <field id="count_workorders" xsi:type="AttributeInteger" _delta="define"> <sql>count_workorders</sql> <default_value>0</default_value> <is_null_allowed>false</is_null_allowed> <tracking_level>all</tracking_level> </field> <fields> <event_listeners> <event_listener id="EvtOnLinksChanged" _delta="define"> <event>EVENT_DB_LINKS_CHANGED</event> <callback>OnLinksChanged</callback> <rank>0</rank> </event_listener> <event_listener id="EvtBeforeWrite" _delta="define"> <event>EVENT_DB_BEFORE_WRITE</event> <callback>BeforeWrite</callback> <rank>0</rank> </event_listener> </event_listeners> <methods>
- Ticket callbacks
-
public function OnLinksChanged(?Combodo\iTop\Service\Events\EventData $oEventData) { // Note the ? in the method declaration, allowing to be called with null as parameter $oSet = $this->Get('workorders_list'); $this->Set('count_workorders', $oSet->count()); // $this->DBUpdate(); useless when $this is modified as it's automatically called at the end of callbacks } public function BeforeWrite(?Combodo\iTop\Service\Events\EventData $oEventData) { $aChanges = $this->ListChanges(); if (array_key_exists('workorders_list', $aChanges)) { $this->OnLinksChanged(null); // We have no EventData to provide, which is anyhow not used in this method } }
When a WorkOrder
object is created, deleted or
modified alone and not within a modification of its Ticket, the
callback Ticket::OnLinksChanged() will be called automatically
Child UserRequest
This use case is very similar to the above one. With previous methods because classes A and B are the same it was a bit different, now it is no more.
In order to be able to do this:
-
We need to create a field
count_related_request_list
on the UserRequest class, to store the number of sub requests permanently so it can be queried. -
We have a
related_request_list
field on UserRequest, providing the list of sub-request under the current one -
We have a
parent_request_id
field on UserRequest, providing the parent
Lets define when we need to compute what?
- itop_design / classes / class@UserRequest
-
<fields> <field id="related_request_list" xsi:type="AttributeLinkedSet"> <with_php_computation _delta="force">true</with_php_computation> </field> <field id="count_related_request" xsi:type="AttributeInteger" _delta="define"> <sql>count_related_request</sql> <default_value>0</default_value> <is_null_allowed>false</is_null_allowed> <tracking_level>all</tracking_level> </field> <fields> <event_listeners> <event_listener id="EvtOnLinksChanged" _delta="define"> <event>EVENT_DB_LINKS_CHANGED</event> <callback>OnLinksChanged</callback> <rank>0</rank> </event_listener> <event_listener id="EvtBeforeWrite" _delta="define"> <event>EVENT_DB_BEFORE_WRITE</event> <callback>BeforeWrite</callback> <rank>0</rank> </event_listener> </event_listeners> <methods>
- UserRequest callbacks
-
public function BeforeWrite(?Combodo\iTop\Service\Events\EventData $oEventData) { $aChanges = $this->ListChanges(); if (array_key_exists('related_request_list', $aChanges)) { $this->OnLinksChanged(null); // Never call $this->DBUpdate(); within a callback, it's useless and ignored } } public function OnLinksChanged(?Combodo\iTop\Service\Events\EventData $oEventData) { $oSet = $this->Get('related_request_list'); $this->Set('count_related_request', $oSet->count()); // Calling ->DBUpdate() is useful ONLY if the updated object is not $oEventData->Get('object') }
You can look at how it was done before, to realize how it has been simplified