You are browsing the documentation for iTop 3.0 which is not the current version.

Consider browsing to iTop 3.1 documentation

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 them src_person_id and dest_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();
        }
    }
}
Also with iTop 2.7 and before, it was difficult to modify an instance of the n:n relationship class, in 3.0, this is happening often as it's easy to do. As a result, handling the AfterUpdate() situation could be forgotten in 2.7, but in 3.0 it is just mandatory
To reuse this example on another class, replace everywhere in the above code:
  • 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
Modify a bit this code, if your relationship has additional fields

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 having networkdevice_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

The code below in not available in iTop 2.7.6 nor in 3.0.1.
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();
        }
    }
}
3_0_0/customization/n-n-reflexive-relation.txt · Last modified: 2022/04/26 12:57 (external edit)
Back to top
Contact us