n-n reflexive & symmetrical
Prerequisite: You must be familiar with the Syntax used in Tutorials and have already created an extension.
- learning:
- Define a new type of relationship, reflexive and symetrical
- level:
- Advanced
- domains:
- PHP, Automation
- min version:
- 2.3.0
Person's colleagues
Assuming you want to define a symmetrical relationship between a Class A and it-self, , let say you want to represent the colleagues of a person and display them in a tab.
-
We will assume that the relationship is symmetrical, if John is the colleague of Anna, then Anna is a colleague of John.
-
If you use a simple n-n iTop relationship, then you will see that this it is not symmetrical out of the box.
-
In the
lnkPersonToPerson
class, there are two AttributeExternalKey, lets call themsrc_person_id
anddest_person_id
pointing to the Person class -
On the Person class, you can create a AttributeLinkedSetIndirect, for this specify the ExternalKey pointing to the Person class <ext_key_to_me> so you can choose
scr_…
or the other, but the result will be the same, you will see only one part of the relation. -
In this tutorial, we will see how to make it symmetrical.
The trick is just to duplicate the relationship:
-
If the user creates (src_person = John, dest_person = Anna), then in the background, we will create its twin: (src_person = Anna, dest_person = John).
-
If the user deletes (src_person = Anna, dest_person = John), then we need to delete as well its twin: (src_person = John, dest_person = Anna)
-
If the user modifies the relation, it becomes a bit more tricky but this can be handled as well
As in that tutorial Calculated field & Cascading update, we will need to intercept some events and act to keep the data aligned.
- lnkPersonToPerson
-
public function AfterInsert() { // When a colleague's relationship is created, we automatically try to create its symmetrical relation $oSearch = DBSearch::FromOQL('SELECT lnkPersonToPerson WHERE src_person_id = :src_id AND dest_person_id = :dest_id'); $oSet = new DBObjectSet($oSearch, array(), array( // Search for the twin, so invert source and dest 'src_id' => $this->Get('dest_person_id'), 'dest_id' => $this->Get('src_person_id'), ), ); // Create a twin if it does not exist already if ($oSet->Count()==0) { $oLnk = MetaModel::NewObject("lnkPersonToPerson", array('src_person_id' => $this->Get('dest_person_id'), 'dest_person_id' => $this->Get('src_person_id'))); $oLnk->DBInsert(); } } public function AfterUpdate() { $aChanges = $this->ListPreviousValuesForUpdatedAttributes(); $sPrevSrc = array_key_exists('src_person_id',$aChanges) ? $aChanges['src_person_id'] : $this->Get('src_person_id'); $sPrevDest = array_key_exists('dest_person_id',$aChanges) ? $aChanges['dest_person_id'] : $this->Get('dest_person_id'); // Search for the old twin $oSearch = DBSearch::FromOQL('SELECT lnkPersonToPerson WHERE src_person_id = :src_id AND dest_person_id = :dest_id'); $oSet = new DBObjectSet($oSearch, array(), array( // Search for the twin, so invert source and dest 'src_id' => $sPrevDest, 'dest_id' => $sPrevSrc, ), ); // Update old twin link found while($oLnk = $oSet->Fetch()) { $oLnk->Set('src_id', $this->Get('dest_person_id')); $oLnk->Set('dest_id', $this->Get('src_person_id')); $oLnk->DBUpdate(); } } public function AfterDelete() { // When a colleague relationship is deleted, we automatically try to delete its symmetrical relation $oSearch = DBSearch::FromOQL('SELECT lnkPersonToPerson WHERE src_person_id = :src_id AND dest_person_id = :dest_id'); $oSet = new DBObjectSet($oSearch, array(), array( // Search for the twin, so invert source and dest 'src_id' => $this->Get('dest_person_id'), 'dest_id' => $this->Get('src_person_id'), ), ); // Delete any found if ($oSet->Count()>0) { while($oLnk = $oSet->Fetch()) { $oLnk->DBDelete(); } } }
-
lnkPersonToPerson
by the name of your relationship class -
src_person_id
by the code of one of the ExternalKeys of your relationship class -
dest_person_id
by the code of the other ExternalKey of your relationship class
NetworkDevice & ConnectableCI
This usecase comes from iTop default datamodel and is just a bit more complicated than the above one, because it is sometimes symmetrical and sometimes not, depending if the ConnectableCI (which is a parent class of the NetworkDevice), is or is not a NetworkDevice as well, if it is the twin must be handled, if it is not then no twin is needed.
Let's take some examples:
-
Let's assume that Router1 is connected to Server1 with a downlink, Server1 is not a Network Device, so no twin is needed.
-
networkdevice_id
= Router1 -
connectableci_id
= Server1
-
-
Now we also have created this downlink between Router1 and Switch1,
-
networkdevice_id
= Router1 -
connectableci_id
= Switch1
-
-
this time Switch1 is a NetworkDevice, so when looking at its details, as we want to be able to see the connection to its Router within its
Devices
tab and that LinkedSet will search for lnkConnectableCIToNetworkDevice havingnetworkdevice_id
= Switch1, so unless we create a twin, Router1 won't be found.
AfterInsert
- lnkConnectableCIToNetworkDevice
-
public function AfterInsert() { $oDevice = MetaModel::GetObject('ConnectableCI', $this->Get('connectableci_id')); if (is_object($oDevice) && (get_class($oDevice) == 'NetworkDevice')) { $sOQL = "SELECT lnkConnectableCIToNetworkDevice WHERE connectableci_id = :device AND networkdevice_id = :network AND network_port = :nwport AND device_port = :devport"; $oConnectionSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array(), array( 'network' => $this->Get('connectableci_id'), 'device' => $this->Get('networkdevice_id'), 'devport' => $this->Get('network_port'), 'nwport' => $this->Get('device_port'), ) ); if ($oConnectionSet->Count() == 0) { $sLink = $this->Get('connection_type'); $sConnLink = ($sLink == 'uplink') ? 'downlink' : 'uplink'; $oNewLink = new lnkConnectableCIToNetworkDevice(); $oNewLink->Set('networkdevice_id', $this->Get('connectableci_id')); $oNewLink->Set('connectableci_id', $this->Get('networkdevice_id')); $oNewLink->Set('network_port', $this->Get('device_port')); $oNewLink->Set('device_port', $this->Get('network_port')); $oNewLink->Set('connection_type', $sConnLink); $oNewLink->DBInsert(); } } }
AfterDelete
- lnkConnectableCIToNetworkDevice
-
public function AfterDelete() { // The device might be already deleted (reentrance in the current procedure when both device are NETWORK devices!) $oDevice = MetaModel::GetObject('ConnectableCI', $this->Get('connectableci_id'), false); if (is_object($oDevice) && (get_class($oDevice) == 'NetworkDevice')) { // Track and delete the counterpart link $sOQL = "SELECT lnkConnectableCIToNetworkDevice WHERE connectableci_id = :device AND networkdevice_id = :network AND network_port = :nwport AND device_port = :devport"; $oConnectionSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array(), array( 'network' => $this->Get('connectableci_id'), 'device' => $this->Get('networkdevice_id'), 'devport' => $this->Get('network_port'), 'nwport' => $this->Get('device_port'), ) ); // There should be one link - do it in a safe manner anyway while ($oConnection = $oConnectionSet->Fetch()) { $oConnection->DBDelete(); } } } }
AfterUpdate
In all versions below those one, the code of this method is buggy and it is really visible in 3.0.x
- lnkConnectableCIToNetworkDevice
-
public function AfterUpdate() { UpdateConnectedNetworkDevice(); } protected function UpdateConnectedNetworkDevice() { $aFields = array('networkdevice_id','connectableci_id','network_port','device_port','connection_type'); $aChanges = $this->ListPreviousValuesForUpdatedAttributes(); $aPrev = array(); // Previous values of the current link object before it was modified foreach ($aFields as $sFieldCode) { $aPrev[$sFieldCode] = array_key_exists($sFieldCode, $aChanges) ? $aChanges[$sFieldCode] : $this->Get($sFieldCode); } $sPrevLink = ($aPrev['connection_type'] == 'uplink') ? 'downlink' : 'uplink'; $sConnLink = ($this->Get('connection_type') == 'uplink') ? 'downlink' : 'uplink'; $oNewDevice = MetaModel::GetObject('ConnectableCI', $this->Get('connectableci_id'), false); $oPrevDevice = MetaModel::GetObject('ConnectableCI', $aPrev['connectableci_id'], false); $bNew = (is_object($oNewDevice) && (get_class($oNewDevice) == 'NetworkDevice')); $bPrev = (is_object($oPrevDevice) && (get_class($oPrevDevice) == 'NetworkDevice')); $sOQL = "SELECT lnkConnectableCIToNetworkDevice WHERE connectableci_id = :device AND networkdevice_id = :network AND network_port = :nwport AND device_port = :devport AND connection_type = :link"; if ($bPrev) { // There was a twin // Retrieve twin link using previous values of the current link $oConnectionSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array(), array( 'network' => $aPrev['connectableci_id'], 'device' => $aPrev['networkdevice_id'], 'devport' => $aPrev['network_port'], 'nwport' => $aPrev['device_port'], 'link' => $sPrevLink, ) ); if ($bNew) { // and a twin must still exist, so update the existing while ($oConnection = $oConnectionSet->Fetch()) { $oConnection->Set('networkdevice_id', $this->Get('connectableci_id')); $oConnection->Set('connectableci_id', $this->Get('networkdevice_id')); $oConnection->Set('network_port', $this->Get('device_port')); $oConnection->Set('device_port', $this->Get('network_port')); $oConnection->Set('connection_type',$sConnLink); $oConnection->DBUpdate(); } } else { // and no twin is needed anymore, so delete the existing while ($oConnection = $oConnectionSet->Fetch()) { $oConnection->DBDelete(); } } } elseif ($bNew) { // There was no twin but a twin must exist now // Search for a twin link using current values inverted $oConnectionSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array(), array( 'network' => $this->Get('connectableci_id'), 'device' => $this->Get('networkdevice_id'), 'devport' => $this->Get('device_port'), 'nwport' => $this->Get('network_port'), 'link' => $sConnLink, ) ); if ($oConnectionSet->Count() == 0) { $oNewLink = new lnkConnectableCIToNetworkDevice(); $oNewLink->Set('networkdevice_id', $this->Get('connectableci_id')); $oNewLink->Set('connectableci_id', $this->Get('networkdevice_id')); $oNewLink->Set('network_port', $this->Get('device_port')); $oNewLink->Set('device_port', $this->Get('network_port')); $oNewLink->Set('connection_type', $sConnLink); $oNewLink->DBInsert(); } } }