Chapter Five: The n-ary relation
The trade flow: one relation, seven roles
Schema so far
If you are starting here, run these queries in order in an empty database.
#!test[schema]
define
attribute name, value string;
attribute country_name, sub name;
attribute province_name, sub name;
attribute kabupaten_name, sub name;
entity place @abstract;
entity country,
sub place,
owns country_name @key;
entity province,
sub place,
owns province_name @key;
entity kabupaten,
sub place,
owns kabupaten_name @key;
#!test[schema]
define
relation containment,
relates parent @card(1),
relates child @card(1);
entity country,
plays containment:parent;
entity province,
plays containment:child @card(0..1), # at most one parent
plays containment:parent;
entity kabupaten,
plays containment:child @card(0..1); # at most one parent
#!test[schema]
define
attribute facility_name, sub name;
attribute mill_name, sub facility_name;
attribute refinery_name, sub facility_name;
attribute company_name, sub name;
entity legal_entity @abstract;
entity facility @abstract,
sub legal_entity,
plays ownership:owned @card(0..1); # at most one owner
entity mill,
sub facility,
owns mill_name;
entity refinery,
sub facility,
owns refinery_name;
entity company,
sub legal_entity,
owns company_name @key,
plays ownership:owner;
relation ownership,
relates owner @card(1),
relates owned @card(1);
#!test[schema]
define
entity company,
plays ownership:owned @card(0..1); # a company has at most one parent company
#!test[schema]
define
attribute commodity_name,
sub name,
value string @values("PALM OIL", "REFINED PALM OIL");
entity commodity,
owns commodity_name @key;
The flow
Each row of the data is one trade flow. Stated as constraints:
-
Each trade flow has exactly one district of origin, one mill, one exporter, one importer, one destination country and one commodity.
-
Each trade flow has at most one refinery. Crude palm oil is not refined.
-
Each trade flow has exactly one volume, one deforestation exposure and one value.
-
A company can be the exporter or the importer of a trade flow.
One relationship of degree seven.
The dashed line marks the optional role, and company participates twice, in two different roles.
#!test[schema]
define
attribute volume_tonnes, value double;
attribute deforestation_ha, value double;
attribute usd, value decimal;
relation trade_flow,
relates origin @card(1),
relates mill @card(1),
relates refiner @card(0..1), # optional
relates exporter @card(1),
relates importer @card(1),
relates destination @card(1),
relates commodity @card(1),
owns volume_tonnes @card(1),
owns deforestation_ha @card(1),
owns usd @card(1);
entity kabupaten, plays trade_flow:origin;
entity mill, plays trade_flow:mill;
entity refinery, plays trade_flow:refiner;
entity company,
plays trade_flow:exporter,
plays trade_flow:importer;
entity country, plays trade_flow:destination;
entity commodity, plays trade_flow:commodity;
-
relation trade_flowdeclares seven roles. Six are required (@card(1)) and the refiner is optional (@card(0..1)). -
The relation owns three attributes of its own, each required.
doubleanddecimalare numeric value types;decimalis exact, which suits money. -
Each
playsline says which type can fill which role.companyplays two.
The model is now complete. Open the Schema page in Studio to compare it with the diagrams so far.
Reading cardinalities
ER and UML notations differ on where a cardinality is written and what it counts, especially for n-ary relationships. In TypeQL, each cardinality is attached to one of two declarations, and each counts one thing:
| Declaration | Counts | Example |
|---|---|---|
|
How many role players fill this role in one relation |
|
|
How many relations of this type one instance plays this role in |
|
plays cardinality is (min,max) participation, counted from the participating instance.
relates cardinality has no direct equivalent in a notation where a relationship is identified by its participants, because it counts the participants of one relationship.
It is available because a TypeDB relation is an object.
If you leave a cardinality out, the defaults are:
-
relates:@card(0..1). Each role is optional and single-valued. -
plays:@card(0..). An instance can play a role in any number of relations, including none. -
owns:@card(0..1). Each attribute is optional and single-valued.
So mandatory participation, such as "every mill supplies at least one trade flow", is written on the plays side: entity mill, plays trade_flow:mill @card(1..);.
Comparison with tables and graphs
Here is the same relationship in tables and in a property graph.
As a table
A relational design makes the flow a table, with one foreign key per role:
CREATE TABLE trade_flow (
id INTEGER PRIMARY KEY,
origin_id INTEGER NOT NULL REFERENCES kabupaten,
mill_id INTEGER NOT NULL REFERENCES mill,
refinery_id INTEGER REFERENCES refinery, -- nullable: optional role
exporter_id INTEGER NOT NULL REFERENCES company,
importer_id INTEGER NOT NULL REFERENCES company,
destination_id INTEGER NOT NULL REFERENCES country,
commodity_id INTEGER NOT NULL REFERENCES commodity,
volume_tonnes DOUBLE PRECISION NOT NULL,
deforestation_ha DOUBLE PRECISION NOT NULL,
usd DECIMAL NOT NULL
);
This is the usual mapping of an n-ary relationship, but the roles survive only as column names, the optional role becomes a NULL, and the model’s generalizations have to be mapped separately.
If mills and refineries share a facility table, mill_id can no longer be constrained to reference only mills without extra machinery.
Participation constraints on the other side, such as every mill supplying at least one flow, need triggers or application code: the SQL standard’s CREATE ASSERTION would express them, but mainstream database systems don’t implement it.
As a graph
In a property graph, edges connect exactly two nodes. There are two ways to store a seven-way fact.
Chain binary edges, such as mill → refinery → exporter → importer. This adds facts that are not true. Each edge is true, but a path through them need not be: in this dataset, following the edges for the 71 refined flows connects 102 mill–importer pairs, but only 63 of them appear together in an actual flow. The other 39 are not in the data.
Reify the flow as a node, with one edge per role. This keeps the facts, but the schema-less graph does not know that a flow node needs exactly one mill edge, or that a mill edge may only point at a mill, or that the flow node represents a relationship rather than an entity.
TypeDB stores the flow as one relation with seven typed roles.
Challenge
Trase also records the port each flow was exported from.
Add a port entity identified by name, and make it an optional role of trade_flow.
Answer
Don’t run this if you are following along, as later chapters assume the original model:
#!test[schema, rollback]
define
attribute port_name, sub name;
entity port, owns port_name @key, plays trade_flow:port_of_export;
relation trade_flow, relates port_of_export @card(0..1);
@card(0..1) is also the default for relates, so it could be left out; writing it down documents the decision.