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
- 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.
- 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.
- Cascade policies ensure referential integrity — CASCADE auto-cleans, SET_NULL unbinds, RESTRICT blocks deletion, eliminating dangling references at the Schema level.
- 1-5 hop graph traversal is 4-194x faster than traditional SQL JOINs, representing the core advantage of knowledge graph queries.
- 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