In this lesson, you will learn how to transform foreign key relationships from the relational schema into graph relationships. This transformation is where the graph model begins to diverge structurally from the relational model.
From Foreign Keys to Relationships
In relational databases, relationships between tables are implicit - defined through foreign keys. In a graph database, relationships are explicit and first-class citizens.
The transformation process involves:
-
Identifying foreign key constraints
-
Determining relationship direction
-
Naming relationships meaningfully
-
Deciding which properties belong on relationships
Common Misconceptions About Relationships
Common misconceptions that cause problems during migration:
Not every foreign key becomes a relationship
Assuming that every foreign key column in the relational schema should be modeled as a relationship in the graph.
Lookup tables may become properties
Some foreign keys reference lookup tables that might become properties instead of relationships.
If you have a status_id foreign key pointing to a statuses table with values like "Pending", "Shipped", "Delivered", you might choose to store status as a property on the Order node rather than creating a separate Status node and relationship. In Northwind, ship_via references shippers; shippers are distinct business entities, so a SHIPPED_BY relationship makes sense. But a region lookup with only four values might stay as a region property on Customer.
Use meaningful verbs, not column names
Using the foreign key column name (e.g. customer_id) as the relationship type instead of a verb that describes the business relationship.
Use verbs, not column names
The foreign key column (like customer_id) does NOT become the relationship type. Instead, use a meaningful verb that describes the business relationship.
Wrong: (Order)-[:CUSTOMER_ID]→(Customer)
Correct: (Customer)-[:PLACED]→(Order)
Northwind’s orders.ship_via references shippers.shipper_id. The relationship type should be SHIPPED_BY, not SHIP_VIA. Similarly, products.supplier_id becomes SUPPLIES from Supplier to Product.
Direction follows business semantics, not foreign key direction
Inferring relationship direction from the foreign key column location (e.g. Order points to Customer because orders.customer_id references customers) instead of from business meaning.
Direction follows business semantics
In SQL, orders.customer_id references customers.customer_id, but this does not mean the relationship should point from Order to Customer.
Choose direction based on business semantics: A customer places an order, so (Customer)-[:PLACED]→(Order) reads better than (Order)-[:PLACED_BY]→(Customer).
In Northwind, orders.employee_id references employees. The relationship should be (Employee)-[:PROCESSED]→(Order) because an employee processes an order, not (Order)-[:EMPLOYEE_ID]→(Employee). The foreign key lives on Order, but the natural direction is Employee to Order.
Junction table rows create one relationship each
Misinterpreting junction table rows as creating multiple relationships or as nodes, when each row typically represents a single relationship between two entities.
One row, one relationship
True for regular foreign keys, but be careful with junction tables. In order_details, each row represents ONE relationship between an Order and a Product - not two separate relationships.
One order_details row with order_id=10248, product_id=11, quantity=12, unit_price=14 creates a single (Order)-[:CONTAINS {quantity:12, unitPrice:14}]→(Product) relationship. It does not create an OrderDetail node, nor does it create two relationships (Order to Product and Product to Order).
Bookmark these misconceptions
Bookmark these four misconceptions. They represent the most frequent errors made when transforming foreign keys into graph relationships. Review them before starting any migration project.
Foreign Key Analysis
Recall the foreign keys in the Northwind database:
| Source Table |
Foreign Key |
Target Table |
Target Key |
orders
|
customer_id
|
customers
|
customer_id
|
orders
|
employee_id
|
employees
|
employee_id
|
orders
|
ship_via
|
shippers
|
shipper_id
|
order_details
|
order_id
|
orders
|
order_id
|
order_details
|
product_id
|
products
|
product_id
|
products
|
category_id
|
categories
|
category_id
|
products
|
supplier_id
|
suppliers
|
supplier_id
|
employees
|
reports_to
|
employees
|
employee_id
|
Designing Graph Relationships
For each foreign key, design a meaningful graph relationship:
Customer Orders
Foreign Key: orders.customer_id references customers.customer_id
Graph Relationship:
The customer places an order. The relationship direction reflects the business action.
Employee Processes Order
Foreign Key: orders.employee_id references employees.employee_id
Graph Relationship:
Alternatively, use (Order)-[:PROCESSED_BY]→(Employee) if you mostly query "which employee processed this order?" instead of "which orders did this employee process?"
Order Shipped By
Foreign Key: orders.ship_via references shippers.shipper_id
Graph Relationship:
Order Contains Products
Foreign Key: order_details.order_id references orders.order_id and order_details.product_id references products.product_id
This junction table becomes a relationship with properties:
Graph Relationship:
The order_details table data becomes properties on the CONTAINS relationship.
Product Categorization
Foreign Key: products.category_id references categories.category_id
Graph Relationship:
Product Supplier
Foreign Key: products.supplier_id references suppliers.supplier_id
Graph Relationship:
Or alternatively: (Product)-[:SUPPLIED_BY]→(Supplier)
Employee Hierarchy
Foreign Key: employees.reports_to references employees.employee_id
Graph Relationship:
This self-referencing relationship creates the management hierarchy.
Complete Relationship Model
The full Northwind graph model includes these relationships:
| Relationship Type |
From Node |
To Node |
PLACED
|
Customer
|
Order
|
PROCESSED
|
Employee
|
Order
|
SHIPPED_BY
|
Order
|
Shipper
|
CONTAINS
|
Order
|
Product
|
IN_CATEGORY
|
Product
|
Category
|
SUPPLIES
|
Supplier
|
Product
|
REPORTS_TO
|
Employee
|
Employee
|
Relationship Direction Guidelines
When deciding relationship direction, consider:
-
Business semantics - Which direction makes logical sense? (Customer places Order, not Order places Customer)
-
Query patterns - Which direction will you traverse most often?
-
Readability - The relationship should read naturally in a sentence
Traversal works in both directions
In Neo4j, you can traverse relationships in either direction regardless of how they are stored. The direction you choose affects query readability and can have minor performance implications for very large graphs.
Relationship Naming Conventions
Follow these conventions for relationship types:
| Convention |
Example |
UPPER_SNAKE_CASE |
PLACED, REPORTS_TO, IN_CATEGORY
|
Active voice verbs |
SUPPLIES not SUPPLIED
|
Descriptive names |
SHIPPED_BY not SHIP
|
Past tense for completed actions |
PLACED for orders that were placed
|
Handling Many-to-Many Relationships
The order_details table represents a many-to-many relationship between orders and products. In the graph model:
-
Each row in order_details becomes a CONTAINS relationship
-
The quantity, unit_price, and discount columns become relationship properties
-
No separate node is needed for order details
This is an advantage of graph databases: relationship properties eliminate the need for junction tables. In relational databases, storing attributes on a many-to-many relationship requires an intermediary table; in graph databases, these attributes become properties on the relationship itself.
What Does NOT Become a Relationship
Not every foreign key should become a relationship. Here are scenarios where you might choose differently:
Scenario 1: Foreign Keys to Small Lookup Tables
When a foreign key references a small lookup table with static values, consider using a property instead.
Example: A priority_id foreign key to a priorities table:
Option A - As a relationship (more complex):
Option B - As a property (simpler):
Choose Option B when:
-
The lookup table has few values (< 10)
-
You do not need to traverse or query the lookup values independently
-
The values are unlikely to change
Scenario 2: Redundant Relationships
Sometimes a relationship can be inferred from other relationships and does not need to be stored.
Example: If you have:
You might be tempted to also create:
But this is redundant - you can always find what products a customer purchased by traversing through orders. Only create direct relationships if:
Scenario 3: Self-Referencing Foreign Keys That Represent Hierarchy Levels
Sometimes self-referencing tables encode fixed hierarchy levels rather than flexible relationships.
Example: An employees table with level_1_manager_id, level_2_manager_id, level_3_manager_id columns.
Instead of creating three different relationship types, consider:
Foreign keys that track who created or modified records might not need relationships.
Example: created_by_user_id and modified_by_user_id columns:
Consider:
Relationship Cardinality Considerations
Understanding cardinality helps you design better relationships:
| SQL Cardinality |
Graph Representation |
Example |
One-to-One |
Single relationship (consider merging nodes) |
(Person)-[:HAS_PASSPORT]→(Passport) - Could also be properties on Person
|
One-to-Many |
Multiple relationships from one node |
(Customer)-[:PLACED]→(Order) - One customer, many orders
|
Many-to-Many |
Junction table becomes relationship with properties |
(Order)-[:CONTAINS {quantity}]→(Product)
|
Consider merging one-to-one relationships
One-to-One relationships are rare in graph databases. If two entities always exist together and are always queried together, consider merging them into a single node with combined properties.
Check Your Understanding
Direction for Customer-Order
When designing a relationship between Customer and Order nodes, which direction best represents the business semantics?
-
✓ (Customer)-[:PLACED]→(Order) — Customer placed the Order
-
❏ (Order)-[:PLACED]→(Customer) — Order placed the Customer
-
❏ Direction does not matter in Neo4j
-
❏ Both directions should be created
Hint
The relationship direction should reflect who does the action: the customer performs the action of placing an order, so the arrow goes from Customer to Order.
Solution
(Customer)-[:PLACED]→(Order) is the correct direction because it reflects the business semantics - a customer places an order, not the other way around.
While Neo4j allows traversing relationships in either direction, choosing a meaningful direction makes queries more readable and aligns with business semantics.
Choosing Relationship Types
A relational table orders has a foreign key column customer_id that references customers.customer_id. What should the graph relationship type be named?
-
❏ CUSTOMER_ID - using the column name
-
❏ ORDERS_CUSTOMERS - using the table names
-
✓ PLACED - using a meaningful verb describing the business action
-
❏ FK_CUSTOMER - using the foreign key prefix
Hint
Relationship types should describe the business meaning of the connection, not the technical implementation.
Solution
The relationship should be named PLACED (or similar verb like MADE, SUBMITTED).
Why not the other options?
-
CUSTOMER_ID - This is the column name, not a meaningful relationship description
-
ORDERS_CUSTOMERS - This describes the tables, not the business relationship
-
FK_CUSTOMER - This is technical jargon from the relational model
Good relationship names:
* Use verbs that describe the action or connection
* Read naturally in a sentence: "Customer PLACED Order"
* Follow UPPER_SNAKE_CASE convention
Foreign Keys That May Not Become Relationships
A table has a status_id foreign key pointing to a statuses table containing 5 values: "New", "Processing", "Shipped", "Delivered", "Cancelled". What is often the best approach?
-
❏ Create a Status node for each value and a HAS_STATUS relationship
-
✓ Store status as a property on the node instead of creating a relationship
-
❏ Create 5 different relationship types, one for each status
-
❏ Ignore the status data entirely
Hint
Consider whether you need to traverse to the status or just filter/display it.
Solution
For small lookup tables with static values, storing as a property is often the best approach:
Why not create Status nodes?
-
Only 5 values - not worth the overhead of separate nodes
-
You typically filter by status (WHERE o.status = "Shipped") rather than traverse to it
-
Status values are unlikely to have their own properties
When WOULD you create Status nodes?
-
If statuses have additional properties (description, color, sort order)
-
If you need to query "all possible statuses" independently
-
If the status list is dynamic and managed separately
Summary
In this lesson, you learned:
-
How to transform foreign keys into graph relationships
-
Guidelines for choosing relationship direction
-
Naming conventions for relationship types
-
How to handle junction tables as relationships with properties
In the next lesson, you will design the properties for nodes and relationships.