Back to Blog

RelationType and Knowledge Graphs: Connecting Business with Relations

In traditional relational databases, relationships between tables are expressed through foreign keys:

CoomiaPublished on August 7, 202513 min read
Share this articleTwitter / X

RelationType and Knowledge Graphs: Connecting Business with Relations

Series: S4 Ontology Modeling · Article 4 | Level: Intermediate | Reading Time: 18 min

#TL;DR

  • RelationType is the "glue" of the Ontology — it connects discrete ObjectTypes into a knowledge graph, transforming "data silos" into a "data network" with 1-5 hop traversal queries that let business analysts describe entity relationships in natural language.
  • Four graph layouts (Hierarchical / Force-Directed / Circular / Geographic) cover every visualization scenario from org charts to supply chain networks, each with distinct use cases and performance characteristics.
  • Cardinality constraints (1:1 / 1:N / M:N) + cascade policies (CASCADE / SET_NULL / RESTRICT) ensure referential integrity, eliminating dangling references at the Schema level.

#1. From E-R Diagrams to Knowledge Graphs: The Evolution of Relation Modeling

In traditional relational databases, relationships between tables are expressed through foreign keys:

Code
Traditional Approach (Foreign Keys):
┌──────────┐     FK     ┌──────────┐
│  Order    │──────────►│ Customer │
│           │           │          │
│ cust_id   │           │ id       │
└──────────┘            └──────────┘

Problems:
├── Ambiguous semantics (is cust_id "buyer" or "recipient"?)
├── Multi-hop queries require hand-written JOINs (5 hops = 5 JOINs)
├── Reverse traversal needs extra indexes
└── Relations can't carry properties ("when was this relationship created?")

coomia-dip's RelationType fundamentally redefines relation modeling:

Code
coomia-dip Approach (RelationType):
┌──────────┐   placedBy    ┌──────────┐
│  Order    │═════════════►│ Customer │
│           │  cardinality │          │
│           │  = MANY_TO_1 │          │
└──────────┘              └──────────┘
      │
      │ contains
      ▼
┌──────────┐   produces    ┌──────────┐
│ LineItem  │═════════════►│ Product  │
│           │              │          │
└──────────┘              └──────────┘

Advantages:
├── Relations have names (placedBy, contains, produces)
├── Relations have cardinality constraints (MANY_TO_ONE)
├── Relations have cascade policies (CASCADE)
├── Reverse traversal APIs are auto-generated
└── 1-5 hop graph traversal supported

#2. RelationType Data Model

#2.1 Core Field Definitions

Python
from ontology_sdk import OntologyClient

client = OntologyClient(base_url="http://localhost:8080")

# Create a complete RelationType
relation = client.schema.create_relation_type({
    "apiName": "Equipment_installedAt_Factory",
    "displayName": "Installed At",
    "description": "Equipment installed at a specific factory floor",

    # Source and target types
    "sourceObjectType": "Equipment",
    "targetObjectType": "Factory",

    # Cardinality constraint
    "cardinality": "MANY_TO_ONE",   # Many equipment at one factory

    # Cascade policies
    "onDeleteSource": "NO_ACTION",   # Deleting equipment doesn't affect factory
    "onDeleteTarget": "RESTRICT",    # Can't delete factory with equipment

    # Relation properties (optional)
    "properties": {
        "installedDate": {"type": "DATE", "required": True},
        "installedBy": {"type": "STRING"},
        "warrantyExpiry": {"type": "DATE"},
    },

    # Inverse relation name
    "inverseApiName": "Factory_hasEquipment",
    "inverseDisplayName": "Has Equipment",
})

#2.2 Schema in Protobuf

PROTOBUF
message RelationType {
    string api_name = 1;
    string display_name = 2;
    string description = 3;

    string source_object_type = 4;
    string target_object_type = 5;

    Cardinality cardinality = 6;

    CascadePolicy on_delete_source = 7;
    CascadePolicy on_delete_target = 8;

    map<string, PropertyDef> properties = 9;

    string inverse_api_name = 10;
    string inverse_display_name = 11;

    LifecycleState lifecycle = 12;
    AuditInfo audit = 13;
}

enum Cardinality {
    ONE_TO_ONE = 0;
    ONE_TO_MANY = 1;
    MANY_TO_ONE = 2;
    MANY_TO_MANY = 3;
}

enum CascadePolicy {
    NO_ACTION = 0;
    CASCADE = 1;
    SET_NULL = 2;
    RESTRICT = 3;
}

#3. Four Cardinality Constraints Explained

#3.1 ONE_TO_ONE: One-to-One

Code
Scenario: Employee ↔ Badge

┌──────────┐   1:1    ┌──────────┐
│ Employee  │════════►│  Badge   │
│ emp-001   │         │ badge-A  │
│ emp-002   │════════►│ badge-B  │
│ emp-003   │════════►│ badge-C  │
└──────────┘         └──────────┘

Constraints:
├── One employee can only have one badge
├── One badge can only belong to one employee
├── Creating duplicate relations throws CARDINALITY_VIOLATION
└── Use cases: ID card↔person, license plate↔vehicle
Python
# Create a 1:1 relation
client.schema.create_relation_type({
    "apiName": "Employee_hasBadge_Badge",
    "sourceObjectType": "Employee",
    "targetObjectType": "Badge",
    "cardinality": "ONE_TO_ONE",
    "onDeleteSource": "CASCADE",  # Employee leaves, badge deactivated
})

# Attempt to assign a second badge to the same employee
try:
    client.objects.create_relation("Employee_hasBadge_Badge", {
        "sourceId": "emp-001",
        "targetId": "badge-D",  # emp-001 already has badge-A
    })
except CardinalityViolation as e:
    print(f"Cardinality violation: {e}")
    # "Employee emp-001 already has a Badge relation"

#3.2 ONE_TO_MANY: One-to-Many

Code
Scenario: Department → Employees

┌──────────┐   1:N    ┌──────────┐
│Department │════════►│ Employee  │
│ dept-eng  │    ┌───►│ emp-001   │
│           │    ├───►│ emp-002   │
│           │    └───►│ emp-003   │
│ dept-mkt  │════════►│ emp-004   │
└──────────┘         └──────────┘

Constraints:
├── One department can have many employees
├── One employee can only belong to one department
└── Use cases: parent dir→files, factory→production lines

#3.3 MANY_TO_ONE: Many-to-One

Code
Scenario: Orders → Customer

┌──────────┐   N:1    ┌──────────┐
│  Order    │════════►│ Customer  │
│ order-A   │    ┌───►│ cust-001  │
│ order-B   │    │    │           │
│ order-C   │────┘    │ cust-002  │
│ order-D   │════════►│           │
└──────────┘         └──────────┘

Constraints:
├── One customer can have many orders
├── One order can only belong to one customer
└── Essentially the inverse of ONE_TO_MANY

#3.4 MANY_TO_MANY: Many-to-Many

Code
Scenario: Students ↔ Courses

┌──────────┐   M:N    ┌──────────┐
│ Student   │════════►│  Course   │
│ stu-A     │──┐ ┌───►│ course-1  │
│ stu-B     │──┼─┤    │ course-2  │
│ stu-C     │──┘ └───►│ course-3  │
└──────────┘         └──────────┘

Implementation:
├── System auto-creates a junction table
├── Junction table can carry relation properties (enrollment date, grade)
└── Use cases: tags↔articles, permissions↔roles
Python
# Create M:N relation (with relation properties)
client.schema.create_relation_type({
    "apiName": "Student_enrolledIn_Course",
    "sourceObjectType": "Student",
    "targetObjectType": "Course",
    "cardinality": "MANY_TO_MANY",
    "properties": {
        "enrollDate": {"type": "DATE", "required": True},
        "grade": {"type": "DOUBLE"},
        "semester": {"type": "STRING"},
    },
})

# Query all courses a student is enrolled in
courses = client.objects.get_related(
    object_type="Student",
    object_id="stu-A",
    relation="Student_enrolledIn_Course",
)
for course in courses:
    print(f"{course.name} - Grade: {course.relation_props.grade}")

#4. Cascade Policies and Referential Integrity

#4.1 Four Cascade Policies

Code
┌─────────────┬──────────────────────────────────────────┐
│   Policy     │  Behavior when source/target is deleted  │
├─────────────┼──────────────────────────────────────────┤
│ NO_ACTION   │ Do nothing (may leave dangling refs)      │
│ CASCADE     │ Cascade-delete related objects             │
│ SET_NULL    │ Set relation field to null (unbind)        │
│ RESTRICT    │ Reject deletion (error if related)         │
└─────────────┴──────────────────────────────────────────┘

#4.2 Cascade Policies in Practice

Python
# Scenario: Factory → ProductionLine → Equipment (three-level cascade)

# Factory 1:N ProductionLine (delete factory cascades to lines)
client.schema.create_relation_type({
    "apiName": "Factory_hasLine_ProductionLine",
    "sourceObjectType": "Factory",
    "targetObjectType": "ProductionLine",
    "cardinality": "ONE_TO_MANY",
    "onDeleteSource": "CASCADE",
    "onDeleteTarget": "NO_ACTION",
})

# ProductionLine 1:N Equipment (delete line cascades to equipment)
client.schema.create_relation_type({
    "apiName": "ProductionLine_hasEquipment_Equipment",
    "sourceObjectType": "ProductionLine",
    "targetObjectType": "Equipment",
    "cardinality": "ONE_TO_MANY",
    "onDeleteSource": "CASCADE",
    "onDeleteTarget": "NO_ACTION",
})

# Deleting a factory triggers chained cascades:
# Factory → CASCADE → ProductionLine → CASCADE → Equipment
result = client.objects.delete("Factory", "factory-001", dry_run=True)
print(f"Will cascade-delete: {result.cascade_count} objects")
# Output: Will cascade-delete: 15 objects (3 lines + 12 equipment)

#4.3 RESTRICT Protection

Python
# Scenario: Customers with orders cannot be deleted
client.schema.create_relation_type({
    "apiName": "Order_placedBy_Customer",
    "sourceObjectType": "Order",
    "targetObjectType": "Customer",
    "cardinality": "MANY_TO_ONE",
    "onDeleteTarget": "RESTRICT",  # Can't delete referenced customers
})

try:
    client.objects.delete("Customer", "cust-001")
except ReferentialIntegrityError as e:
    print(f"Cannot delete: {e}")
    # "Cannot delete Customer cust-001:
    #  referenced by 23 Order objects via Order_placedBy_Customer"
    print(f"Referencing objects: {e.referencing_objects[:5]}")

#5. Graph Traversal: 1-5 Hop Queries

#5.1 Traversal API Design

Code
Single-hop traversal:
  GET /objects/{type}/{id}/relations/{relationName}

Multi-hop traversal:
  POST /objects/{type}/{id}/traverse
  Body: {
    "path": ["rel1", "rel2", "rel3"],
    "maxDepth": 3,
    "filters": {...}
  }
Python
# 1 hop: Find the factory where equipment is installed
factory = client.objects.get_related_one(
    object_type="Equipment",
    object_id="equip-001",
    relation="Equipment_installedAt_Factory",
)
print(f"Equipment installed at: {factory.name}")

# 2 hops: Find all employees at the same factory as the equipment
employees = client.objects.traverse(
    object_type="Equipment",
    object_id="equip-001",
    path=[
        "Equipment_installedAt_Factory",
        "Factory_employs_Employee",
    ],
)
print(f"Employees at same factory: {len(employees)}")

# 3 hops: Equipment → Factory → Supplier → Supplier's other customers
other_customers = client.objects.traverse(
    object_type="Equipment",
    object_id="equip-001",
    path=[
        "Equipment_suppliedBy_Supplier",
        "Supplier_suppliesTo_Factory",
        "Factory_ownedBy_Company",
    ],
    filters={
        "Company": {"industry": {"eq": "Manufacturing"}},
    },
    max_results=100,
)

# 5 hops: Full supply chain trace
supply_chain = client.objects.traverse(
    object_type="Product",
    object_id="prod-001",
    path=[
        "Product_containsPart_Component",
        "Component_madeBy_Supplier",
        "Supplier_locatedIn_Region",
        "Region_governedBy_Authority",
        "Authority_regulates_Standard",
    ],
    max_depth=5,
)

#5.2 Traversal Performance Optimization

Code
Performance comparison (100K objects, avg 5 relations/object):

Hops  │  Traditional SQL JOIN  │  coomia-dip Graph Traversal  │  Speedup
──────┼───────────────────────┼───────────────────────────────┼────────
1 hop │     12 ms             │      3 ms                     │   4x
2 hop │     89 ms             │      8 ms                     │  11x
3 hop │    650 ms             │     22 ms                     │  30x
4 hop │   4800 ms             │     65 ms                     │  74x
5 hop │  35000 ms             │    180 ms                     │ 194x

Optimization strategies:
├── Relation indexes: auto-create B+ tree indexes on sourceId/targetId
├── Adjacency cache: hot objects' relation lists cached in Redis
├── Depth pruning: auto-stop beyond maxDepth
└── Result limiting: maxResults prevents result set explosion

#6. Four Graph Layout Visualizations

#6.1 Hierarchical Layout

Code
Use case: Org charts, file directories, taxonomy trees

                    ┌───────┐
                    │  CEO  │
                    └───┬───┘
              ┌─────────┼─────────┐
          ┌───┴───┐ ┌───┴───┐ ┌───┴───┐
          │ VP-Eng│ │VP-Mkt │ │VP-Sales│
          └───┬───┘ └───┬───┘ └───┬───┘
        ┌─────┤         │         │
    ┌───┴───┐ ┌┴──┐  ┌──┴──┐  ┌──┴──┐
    │Team-A │ │T-B│  │ T-C │  │ T-D │
    └───────┘ └───┘  └─────┘  └─────┘

Configuration:
  layout: "hierarchical"
  direction: "top-down"  // or "left-right"
  levelSeparation: 100
  nodeSeparation: 60
Python
# Get hierarchical layout data
tree = client.graph.layout(
    root_type="Organization",
    root_id="org-001",
    relation="Organization_hasChild_Organization",
    layout="hierarchical",
    direction="top-down",
    max_depth=5,
)

for node in tree.nodes:
    indent = "  " * node.depth
    print(f"{indent}├── {node.display_name} ({node.object_type})")

#6.2 Force-Directed Layout

Code
Use case: Social networks, knowledge graphs, entity correlation analysis

    ○ Alice                    ○ Product-X
     ╲   ╱                    ╱    │
      ○ Bob ──── ○ Charlie ──○     │
     ╱        ╲       ╲      ╲    │
    ○ Dave     ○ Eve   ○ Frank ○ Vendor-A

Characteristics:
├── Node repulsion + edge spring = automatic layout
├── Dense relationship areas auto-cluster
├── Great for discovering community structure
└── Performance: smooth under 1000 nodes, >5000 needs downsampling

#6.3 Circular Layout

Code
Use case: Process cycles, dependency cycle detection, peer relationships

         ┌────┐
        ╱│Step1│╲
       ╱ └────┘  ╲
   ┌────┐       ┌────┐
   │Step4│       │Step2│
   └────┘       └────┘
       ╲ ┌────┐ ╱
        ╲│Step3│╱
         └────┘

Characteristics:
├── All nodes evenly distributed on the circumference
├── Naturally suited for displaying cyclical processes
├── Quick cycle detection in dependencies
└── Best when node count < 50

#6.4 Geographic Layout

Code
Use case: Supply chain networks, logistics routes, regional management

    ┌──────────────────────────────────┐
    │                    ○ Beijing Mfg  │
    │  ○ Urumqi WH    ╱    │           │
    │       │        ╱     │           │
    │       │      ╱       ▼           │
    │       ▼    ╱    ○ Shanghai HQ    │
    │    ○ Chengdu ╱       │           │
    │      DC  ───────────►│           │
    │                ○ Guangzhou Port   │
    └──────────────────────────────────┘

Characteristics:
├── Nodes positioned by latitude/longitude
├── Requires objects to have geo properties (lat/lng)
├── Edge length reflects actual geographic distance
└── Ideal for logistics, supply chains, infrastructure management
Python
# Get geographic layout data
geo_graph = client.graph.layout(
    root_type="DistributionCenter",
    root_id="dc-shanghai",
    relation="DistributionCenter_suppliesTo_Warehouse",
    layout="geographic",
    geo_property="location",  # Property containing lat/lng
    max_depth=2,
)

for edge in geo_graph.edges:
    print(f"{edge.source.name}{edge.target.name}: {edge.distance_km:.0f} km")

#7. Relation Query Patterns

#7.1 Neighbor Queries

Python
# Query all relations for an object (any type)
all_relations = client.objects.get_all_relations(
    object_type="Equipment",
    object_id="equip-001",
)

for rel in all_relations:
    print(f"[{rel.relation_type}] → {rel.target_type}/{rel.target_id}")
# [Equipment_installedAt_Factory] → Factory/factory-001
# [Equipment_maintainedBy_Technician] → Technician/tech-003
# [Equipment_produces_Product] → Product/prod-001
# [Equipment_produces_Product] → Product/prod-002

#7.2 Path Queries

Python
# Find the shortest path between two objects
path = client.graph.shortest_path(
    source_type="Employee",
    source_id="emp-001",
    target_type="Employee",
    target_id="emp-099",
    max_depth=6,
)

print(f"Shortest path ({path.hops} hops):")
for step in path.steps:
    print(f"  {step.from_name} --[{step.relation}]--> {step.to_name}")
# emp-001 --[Employee_reportsTo_Employee]--> emp-010
# emp-010 --[Employee_reportsTo_Employee]--> emp-050
# emp-050 --[Employee_manages_Employee]--> emp-099

#7.3 Subgraph Queries

Python
# Get a subgraph centered on a specific object
subgraph = client.graph.subgraph(
    center_type="Customer",
    center_id="cust-001",
    max_depth=2,
    relation_types=[
        "Customer_placedOrder_Order",
        "Order_contains_LineItem",
        "LineItem_isProduct_Product",
    ],
    max_nodes=200,
)

print(f"Subgraph contains: {len(subgraph.nodes)} nodes, {len(subgraph.edges)} edges")

#8. Relation Modeling Best Practices

#8.1 Naming Conventions

Code
Relation naming format: {SourceType}_{verb}_{TargetType}

Good names:
├── Employee_reportsTo_Employee       ✅ Clear verb
├── Order_contains_LineItem           ✅ Business semantics clear
├── Factory_locatedIn_Region          ✅ Direction clear
└── Equipment_maintainedBy_Technician ✅ Passive voice is fine

Bad names:
├── Employee_Employee_rel             ❌ No semantics
├── order_item                        ❌ Direction unknown
├── has_data                          ❌ Too vague
└── r1                                ❌ Completely meaningless

#8.2 Relation vs Property: When to Use Which

Code
When to use a Relation (RelationType):
├── Target is an independent business entity (has its own lifecycle)
├── Need reverse queries ("who references me?")
├── Many-to-many relationships
├── The relationship itself has properties (e.g., "when was it established")
└── Need graph traversal

When to use a Property:
├── Value is a simple scalar (string, number, date)
├── No reverse queries needed
├── Value has no independent lifecycle
├── Enum values (status, type)
└── Nested structures (use StructType)

#8.3 Avoiding Over-Modeling

Code
Anti-pattern: Modeling every possible relation (relation explosion)

Employee ──reportsTo──► Employee
Employee ──sameTeamAs──► Employee      ← Derivable from reportsTo
Employee ──sameDeptAs──► Employee      ← Derivable via 2-hop reportsTo
Employee ──sameFloorAs──► Employee     ← Derivable via locatedIn

Principles:
├── Only model "atomic relations" — those not derivable from others
├── Derivable relations use "Derived Relations" for auto-computation
├── Keep relation count to 2-3x the number of ObjectTypes
└── Regularly audit and clean up unused relations

#9. Relation Lifecycle Management

Python
# Relation types follow the same DRAFT → ACTIVE → DEPRECATED → ARCHIVED lifecycle

# 1. Create (DRAFT state)
rel = client.schema.create_relation_type({
    "apiName": "Equipment_locatedIn_Zone",
    "sourceObjectType": "Equipment",
    "targetObjectType": "Zone",
    "cardinality": "MANY_TO_ONE",
})
assert rel.lifecycle == "DRAFT"

# 2. Publish (transition to ACTIVE)
client.schema.publish_relation_type("Equipment_locatedIn_Zone")

# 3. Deprecate (give consumers migration time)
client.schema.deprecate_relation_type(
    "Equipment_locatedIn_Zone",
    reason="Replaced by Equipment_installedAt_Factory",
    sunset_date="2026-06-01",
)

# 4. Archive (cease usage)
client.schema.archive_relation_type("Equipment_locatedIn_Zone")

#10. Relation Indexes and Performance Tuning

Code
Indexing strategy:

┌─────────────────────────────────────────────┐
│          Relation Storage Structure          │
├─────────────────────────────────────────────┤
│  Forward index: source_type + source_id → targets │
│  Reverse index: target_type + target_id → sources │
│  Property index: relation_type + prop → instances  │
│  Full-text index: relation description → search    │
└─────────────────────────────────────────────┘

Performance tuning parameters:
├── Relation cache TTL (default: 5 minutes)
├── Traversal timeout (default: 30 seconds)
├── Max results per traversal (default: 10000)
├── Batch relation creation concurrency (default: 100)
└── Graph traversal width limit (max 1000 nodes expanded per level)
Python
# Batch create relations (high performance)
relations_batch = [
    {"sourceId": f"equip-{i:03d}", "targetId": "factory-001"}
    for i in range(1, 101)
]

result = client.objects.batch_create_relations(
    relation_type="Equipment_installedAt_Factory",
    relations=relations_batch,
    batch_size=50,  # 50 per batch
)
print(f"Created: {result.success_count}, Failed: {result.failure_count}")

#11. Real-World Case: Manufacturing Supply Chain Knowledge Graph

Python
# Complete supply chain relation model

# 1. Supplier → Raw Materials
client.schema.create_relation_type({
    "apiName": "Supplier_provides_RawMaterial",
    "sourceObjectType": "Supplier",
    "targetObjectType": "RawMaterial",
    "cardinality": "MANY_TO_MANY",
    "properties": {
        "unitPrice": {"type": "DOUBLE"},
        "leadTimeDays": {"type": "INTEGER"},
        "qualityRating": {"type": "DOUBLE"},
    },
})

# 2. Raw Materials → Products (BOM relation)
client.schema.create_relation_type({
    "apiName": "RawMaterial_usedIn_Product",
    "sourceObjectType": "RawMaterial",
    "targetObjectType": "Product",
    "cardinality": "MANY_TO_MANY",
    "properties": {
        "quantity": {"type": "DOUBLE"},
        "unit": {"type": "STRING"},
    },
})

# 3. Products → Customers (sales relation)
client.schema.create_relation_type({
    "apiName": "Product_soldTo_Customer",
    "sourceObjectType": "Product",
    "targetObjectType": "Customer",
    "cardinality": "MANY_TO_MANY",
    "properties": {
        "contractDate": {"type": "DATE"},
        "annualVolume": {"type": "INTEGER"},
    },
})

# 4. Supply chain risk analysis: which customers are affected if a supplier fails?
impact = client.graph.traverse(
    object_type="Supplier",
    object_id="supplier-CN-001",
    path=[
        "Supplier_provides_RawMaterial",
        "RawMaterial_usedIn_Product",
        "Product_soldTo_Customer",
    ],
)

print(f"Supplier disruption impact analysis:")
print(f"  Affected raw materials: {len(impact.layer(0))}")
print(f"  Affected products: {len(impact.layer(1))}")
print(f"  Affected customers: {len(impact.layer(2))}")

for customer in impact.layer(2):
    products = impact.paths_to(customer)
    print(f"  Customer {customer.name}: involves {len(products)} products")

#Key Takeaways

  1. RelationType is the cornerstone of knowledge graphs — it connects discrete ObjectTypes into a semantically rich relationship network, with every relation having a name, cardinality, cascade policy, and optional properties.
  2. Four cardinality constraints (1:1, 1:N, N:1, M:N) cover all business scenarios, with the system automatically enforcing constraints at runtime and raising errors on violations.
  3. Cascade policies ensure referential integrity — CASCADE auto-cleans, SET_NULL unbinds, RESTRICT blocks deletion, eliminating dangling references at the Schema level.
  4. 1-5 hop graph traversal is 4-194x faster than traditional SQL JOINs, representing the core advantage of knowledge graph queries.
  5. Four graph layouts (Hierarchical / Force-Directed / Circular / Geographic) cover visualization needs from org charts to supply chain networks.

#Next Article

The next article, S4-05 ActionType Explained, will cover how to model business operations (approval, assignment, computation) as platform-native capabilities, with 10 executor types, parameter validation, and idempotency guarantees behind every button click.

#ontology #relation-type #knowledge-graph #graph-traversal #cardinality #cascade #visualization