Denormalization with foreign master (v0.4)
File: 13-foreign-master-denormalization.xdbml · Target: MongoDB document model
A read-optimized order document that keeps copies of values mastered in customers and products, with every copy declared as a foreign master relationship (spec §11.10). Eight of them: the customer name, the loyalty tier, three address fields, and three catalogue fields inside each line item. Both declaration forms appear, so the example shows they mean the same thing -- top-level Ref: statements for the customer copies, the inline [ref: ..., foreign_master] form for the line-item copies, one of which sits inside an array element. The example also draws the line that matters in practice: unitPrice is NOT a foreign master, because the price charged is a fact about the order rather than a copy of the catalogue price, and a value that may legitimately differ from its source is not replicated data. Contrast the foreign key at the end, which is generated and carries roles, verbs and a constraint type (§11.14, §11.15), with the eight foreign masters above it, which reach no generator at all.
Source
xdbml: 0.4
// ---------------------------------------------------------------------------
// Denormalization with foreign master relationships.
//
// A product catalogue and an order history for a storefront. Orders and their
// line items keep copies of values that are mastered elsewhere, so reading an
// order needs no join and an order stays a faithful record of what was true
// at the time of sale.
//
// Every copy is declared with a `foreign_master` relationship (spec 11.10),
// which records where the value is mastered. Nothing is generated from them:
// no foreign key, no validator, no index. They exist so that a change to a
// master can be traced to every place the model repeats it.
//
// Three things distinguish a foreign master from a foreign key (11.11):
// - it takes one attribute on each side, never a composite
// - a copied attribute has exactly one master
// - it never reaches a generator
//
// Both declaration forms appear below. `customers` uses top-level `Ref:`
// statements, `orders.lineItems` uses the inline form, and they mean the
// same thing.
// ---------------------------------------------------------------------------
Project storefront {
targets: [MongoDB]
Note: 'Read-optimized order documents. Copies are deliberate; each one is declared.'
}
// --- Masters --------------------------------------------------------------
Collection customers {
Note: 'Master of the customer name and the default shipping address.'
customerID objectId [pk]
name string [not null]
email string [unique, not null, tags: ['pii']]
loyaltyTier string [default: 'standard']
address object {
street string
city string
country string [default: 'BE']
}
}
Collection products {
Note: 'Master of the product name, brand and current list price.'
productID objectId [pk]
sku string [unique, not null]
name string [not null]
brand string
listPrice Decimal128
}
// --- Denormalized document ------------------------------------------------
Collection orders {
Note: 'One document per order. Copies below are frozen at the time of sale.'
orderID objectId [pk]
orderDate Date [not null]
// The key reference. This is the foreign key: it locates the master.
customerID objectId [not null]
// Copies of customer fields, declared as foreign masters further down.
customerName string
loyaltyTier string [note: 'Tier as it stood when the order was placed.']
shipTo object {
street string
city string
country string
}
lineItems array [
lineItem object {
// The key reference for the line item.
productID objectId [ref: > products.productID, not null]
// Copies of catalogue fields, declared inline. `unitPrice` is NOT a
// foreign master: it records the price charged, which is a fact about
// this order rather than a copy of the catalogue price. A value that
// can legitimately differ from its source is not a replicated value.
sku string [ref: > products.sku, foreign_master]
productName string [ref: > products.name, foreign_master]
brand string [ref: > products.brand, foreign_master]
unitPrice Decimal128 [note: 'Price charged. May differ from products.listPrice.']
qty int32 [minimum: 1]
}
]
}
// --- Foreign masters, top-level form --------------------------------------
//
// One relationship per copied attribute. The parent endpoint names where the
// value is mastered; the child endpoint names the copy.
//
// customers.name and every field of customers.address carry the `dm` marker
// once these are declared, and the matching fields in orders carry `fm`.
// Those markers are computed, never written (11.12).
Ref: orders.customerName > customers.name [foreign_master]
Ref: orders.loyaltyTier > customers.loyaltyTier [foreign_master]
Ref: orders.shipTo.street > customers.address.street [foreign_master]
Ref: orders.shipTo.city > customers.address.city [foreign_master]
Ref: orders.shipTo.country > customers.address.country [foreign_master]
// --- The foreign key it sits beside ---------------------------------------
//
// Declared last so the contrast is visible: this one IS generated, and it
// carries the documentation a logical model wants (11.14, 11.15).
Ref placed_by: orders.customerID > customers.customerID [
source_role: 'order history', source_verb: 'is placed by',
target_role: 'buyer', target_verb: 'places',
source: '0..*', target: '1..1',
constraint_type: non_identifying
]