Nodes, relationships, properties — first-class relationships versus RDF reification and junction tables

A property graph models data as nodes plus directed, typed relationships that are first-class records carrying their own properties, which is why 'Alice KNOWS Bob since 2020' is one edge here but a junction-table-plus-attributes mess in relational and a multi-triple reification in RDF.

Previously

Scene 1's pointer walk beat the join, but we cheated — we never said how a relationship gets to be a thing you can follow. Here's the model that makes it real: relationships are first-class records with their own type and properties, not rows in a join table. Now the obvious question — what does 'first-class' buy you at the byte level that a join row doesn't?

Scene 02

Nodes, relationships, properties

  1. Watch
  2. Try it
  3. Predict
  4. Capture
Alice KNOWS Bob since 2020RELATIONALUSER1 Alice2 BobKNOWS(a,b)1 → 2KNOWS_ATTRS(1,2) since=2020RDF (triples)alice :knows _:k1 ._:k1 :object bob ._:k1 :since 2020 .PROPERTY GRAPH{since:2020}Alice{name:Alice}BobFRIENDSame fact, three models — the property graph carries the date ON the edge.
What to watch for

One fact: 'Alice KNOWS Bob since 2020.' Watch it drawn three ways, left to right. RELATIONAL needs a junction table for the link AND a separate attributes table just to hold the date. RDF has to reify — the edge becomes its own blank node with extra triples. The PROPERTY GRAPH stores it as a single typed edge, Alice—[:KNOWS]→Bob, with {since:2020} living right on the edge. Count the moving parts: the property graph has the fewest. That right pane is the model the rest of the course is built on.

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

PropertyGraph.record_fact
one edge carries the relationship's own properties
def record_fact(g, a, rel_type, b, props):
# nodes are first-class; so is the relationship
g.merge_node(a)
g.merge_node(b)
# the edge IS a record — properties live ON it
g.add_edge(a, b, type=rel_type, props=props)
# e.g. add_edge('Alice','Bob','KNOWS',{since:2020})
Relational.record_fact
a link needs a junction row + a separate attrs row
def record_fact(db, a, rel, b, attrs):
db.upsert('USER', a); db.upsert('USER', b)
# the link: just the two endpoints
db.insert(rel + '(a,b)', (a.id, b.id))
# attributes have nowhere to go but a side table
db.insert(rel + '_ATTRS', (a.id, b.id, attrs))
RDF.record_fact
reify: the edge becomes a blank node + extra triples
def record_fact(store, a, rel, b, attrs):
bnode = store.fresh_blank_node()
store.add(a, rel, bnode) # alice :knows _:k1
store.add(bnode, ':object', b) # _:k1 :object bob
for k, v in attrs.items():
store.add(bnode, k, v) # _:k1 :since 2020

Where this sits in Build a graph database (Neo4j / Dgraph-style)

Scene 02 of 16, in the Why graphs act — Friends-of-friends melts a join; edges go first-class.. The property-graph model is four things — nodes, typed relationships, labels, and properties on both — and putting key/values directly ON the edge is the move that relational join-tables and RDF triples can't make cleanly.

Up next. We've made the edge a first-class object on paper. But 'first-class' has to mean something physical: when I'm standing on Alice and I want Bob, how does the engine GET to the edge without looking it up in an index? The answer is the single most important idea in the whole course — and it's why a hop costs the same whether the graph has ten nodes or ten billion.

All 16 scenes in Build a graph database (Neo4j / Dgraph-style) · Every curriculum

Built with Arqly
Every scene in Build a graph database (Neo4j / Dgraph-style) builds on the one before it.All 16 Build a graph database (Neo4j / Dgraph-style) scenes