Mapping Relationships

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.

Northwind graph model showing nodes and relationships

The transformation process involves:

  1. Identifying foreign key constraints

  2. Determining relationship direction

  3. Naming relationships meaningfully

  4. 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:

(Customer)-[:PLACED]->(Order)

The customer places an order. The relationship direction reflects the business action.

Customer PLACED Order relationship mapping

Employee Processes Order

Foreign Key: orders.employee_id references employees.employee_id

Graph Relationship:

(Employee)-[:PROCESSED]->(Order)

Alternatively, use (Order)-[:PROCESSED_BY]→(Employee) if you mostly query "which employee processed this order?" instead of "which orders did this employee process?"

Employee PROCESSED Order relationship mapping

Order Shipped By

Foreign Key: orders.ship_via references shippers.shipper_id

Graph Relationship:

(Order)-[:SHIPPED_BY]->(Shipper)
Order SHIPPED_BY Shipper relationship mapping

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:

(Order)-[:CONTAINS {quantity, unitPrice, discount}]->(Product)

The order_details table data becomes properties on the CONTAINS relationship.

Order CONTAINS Product - Junction table becomes relationship with properties

Product Categorization

Foreign Key: products.category_id references categories.category_id

Graph Relationship:

(Product)-[:IN_CATEGORY]->(Category)
Product IN_CATEGORY Category relationship mapping

Product Supplier

Foreign Key: products.supplier_id references suppliers.supplier_id

Graph Relationship:

(Supplier)-[:SUPPLIES]->(Product)

Or alternatively: (Product)-[:SUPPLIED_BY]→(Supplier)

Supplier SUPPLIES Product relationship mapping

Employee Hierarchy

Foreign Key: employees.reports_to references employees.employee_id

Graph Relationship:

(Employee)-[:REPORTS_TO]->(Employee)

This self-referencing relationship creates the management hierarchy.

Employee REPORTS_TO Employee - Self-referencing relationship for 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

Complete Northwind Graph Model showing all nodes and relationships

Relationship Direction Guidelines

When deciding relationship direction, consider:

  1. Business semantics - Which direction makes logical sense? (Customer places Order, not Order places Customer)

  2. Query patterns - Which direction will you traverse most often?

  3. 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:

sql
-- Relational model
SELECT o.order_id, p.priority_name
FROM orders o
JOIN priorities p ON o.priority_id = p.priority_id;

Option A - As a relationship (more complex):

(Order)-[:HAS_PRIORITY]->(Priority {name: "High"})

Option B - As a property (simpler):

(Order {priority: "High"})

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:

(Customer)-[:PLACED]->(Order)-[:CONTAINS]->(Product)

You might be tempted to also create:

(Customer)-[:PURCHASED]->(Product)

But this is redundant - you can always find what products a customer purchased by traversing through orders. Only create direct relationships if:

  • The traversal path is very long (performance concern)

  • The direct relationship has unique properties not derivable from the path

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:

  • Using a single REPORTS_TO relationship and traversing the hierarchy

  • Or storing the level as a property on the relationship

Scenario 4: Foreign Keys for Audit/Metadata

Foreign keys that track who created or modified records might not need relationships.

Example: created_by_user_id and modified_by_user_id columns:

sql
CREATE TABLE orders (
    order_id INTEGER PRIMARY KEY,
    created_by_user_id INTEGER,
    modified_by_user_id INTEGER,
    -- ... other columns
);

Consider:

  • As relationships - If you need to query "what orders did user X create?"

  • As properties - If this is just audit data you rarely query: (Order {createdBy: "user123"})

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:

(Order {status: "Shipped"})

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.

Chatbot

How can I help you today?