PingOne Advanced Identity Cloud

Query relationships bidirectionally

Relationships between two managed objects come in two forms: forward and reverse. Forward relationships mean one side points to the other in a uni-directional flow. Reverse relationships means both sides point to the other in a bidirectional flow.

Most cases use reverse or bidirectional relationships for efficient querying of objects. For example, a relationship between a user and his manager might indicate a reverse relationship between the manager and her direct report. You may want to query jdoe's user entry to discover who his manager is, or query bjensen's user entry to discover all the users who report to bjensen.

You define a reverse relationship within a relationship definition. Consider the following sample excerpt of the default managed object configuration:

"reports" : {
  "description" : "Direct Reports",
  "title" : "Direct Reports",
  ...
  "type" : "array",
  "returnByDefault" : false,
  "items" : {
    "type" : "relationship",
    "reverseRelationship" : true,
    "reversePropertyName" : "manager",
    "validate" : true,
    ...
  }
  ...

The reports property is a relationship type between users and managers, so you can refer to a managed user’s reports by referencing the reports object. However, the reports property is also a reverse relationship ("reverseRelationship":true), which means you can list all users that reference that report.

You can list all users whose manager property is set to the currently queried user.

The reverse relationship includes an optional resourceCollection that lets you query a set of objects, based on specific fields:

"resourceCollection" : [
  {
    "path" : "managed/realm-name_user",
    "label" : "User",
    "query" : {
      "queryFilter" : "true",
      "fields" : [
        "userName",
        "givenName",
        "sn"
      ]
    }
  }
]

The path property of the resourceCollection points to the set of objects to be queried. If this path is not in the local repository, the link expansion can incur a significant performance cost. Although the resourceCollection is optional, the same performance cost is incurred if the property is absent.

The query property indicates how you will query this resource collection to configure the relationship. In this case, "queryFilter":true indicates that you can search on any of the properties listed in the fields array when you are assigning a manager to a user or a new report to a manager.

Relationship storage considerations and limits

The two sides of a relationship differ in how they store data. The forward (owning) side is where Advanced Identity Cloud stores the relationship. The reverse side stores nothing of its own and points back to the forward side.

Each forward (owning) custom relationship property defined in the managed object configuration is backed by an internal reference attribute on the owning managed object’s entry. Each stored value is a JSON envelope that identifies the related object and includes the relationship’s own _id and _rev.

Built-in relationships are stored in their own attributes. For example, the default manager property on user objects has a dedicated attribute.

Reverse relationship properties don’t have a stored reference of their own. They’re computed at read time by finding every owning entry whose stored reference points back to the target.

Each managed object type is limited to 10 custom relationship properties on its forward side, because the underlying entries provide a fixed number of reference slots. Built-in and reverse relationship properties don’t count against this limit.

Concurrent writes to a shared owning entry

Because every forward relationship is stored on the owning object’s entry, every operation that adds or removes a relationship to the same owning object must update that single entry. Advanced Identity Cloud uses optimistic concurrency on the entry’s _rev to detect conflicting updates. When two updates target the same entry at the same time, only the first commits. The others fail their _rev precondition.

These failures surface as:

  • 412 Precondition Failed over the IDM REST API.

  • A precondition assertion failure on the owning entry in the IDM logs.

Example

Consider a custom relationship between two managed object types custom_Application and custom_Organization, configured so that custom_Organization.applications is the forward (owning) side and custom_Application.organization is the reverse:

"managed/custom_Organization": {
  "schema": {
    "properties": {
      "applications": {
        "type": "array",
        "items": {
          "type": "relationship",
          "reverseRelationship": false
        }
      }
    }
  }
},
"managed/custom_Application": {
  "schema": {
    "properties": {
      "organization": {
        "type": "relationship",
        "reverseRelationship": true,
        "reversePropertyName": "applications"
      }
    }
  }
}

Now create ten custom_Application objects concurrently, each linking to the same custom_Organization. Every create updates the same custom_Organization entry, and most of those updates lose the _rev race and fail with 412 Precondition Failed.

Mitigation: distribute writes across many entries

Configure the forward (owning) relationship property on the side that is created or modified concurrently, so each write lands on its own entry. For the example above, make custom_Application.organization the forward (owning) property and custom_Organization.applications the reverse view:

"managed/custom_Application": {
  "schema": {
    "properties": {
      "organization": {
        "type": "relationship",
        "reverseRelationship": false
      }
    }
  }
},
"managed/custom_Organization": {
  "schema": {
    "properties": {
      "applications": {
        "type": "array",
        "items": {
          "type": "relationship",
          "reverseRelationship": true,
          "reversePropertyName": "organization"
        }
      }
    }
  }
}

Each parallel custom_Application create now writes to its own entry, so the updates don’t collide.

This mitigation works because the concurrent writes are now spread across many distinct entries and not because the previous configuration was inherently wrong.

If you concurrently update the same custom_Application entry from multiple threads (for example, ten threads each modifying a different relationship on the same application), the same 412 Precondition Failed errors return. Optimistic concurrency on a single entry is the underlying constraint. Ownership choice only changes which entry that constraint applies to.

Choose an ownership direction

Use these guidelines when deciding which side of a relationship to make the forward (owning) side.

Data structure
  • Many-to-one and one-to-many: Place the forward property on the many side, where each object stores one reference and the shared object stores none. For example, each user stores a reference to their shared role, and the role’s entry stores none.

  • Many-to-many: Both sides store many references. Place the forward property on the side with the shortest lifecycle.

Concurrency
  • Place the forward property on the object type that is created or modified concurrently. This spreads writes across many entries.

  • Place the reverse view on the object type that is long-lived and shared by many other objects (for example, an organization, a role, or an application catalog entry).

Constraints and conflicts
  • Keep the 10-property limit in mind. The limit applies to the forward side of each object type. Moving ownership to the other type moves the slot usage to that type.

  • If the object type that is created or modified concurrently is near the limit, treat the concurrency guideline as the priority:

    1. Free a slot by deleting an unused relationship property.

    2. If you can’t free a slot, place the forward side on the shared object and avoid concurrent create operations that target the same shared entry.

If you need to change the ownership direction of an existing relationship, delete the existing property first and re-create it with the new configuration.