Document relationships
Relationships establish associations between documents.
Define document relationships
A collection schema can include a document-type-definitions, document type definition that controls what fields are accepted in a collection’s documents.
The document type can include field definition that accept a collection’s documents as a data type:
collection Product {
...
// `store` accepts `Store` collection documents.
store: Ref<Store>
...
}
A field can accept documents as part of an array:
collection Order {
...
// `cart` accepts an array of `Product` collection documents.
cart: Array<Ref<Product>>
...
}
Or as part of a nested object:
collection Customer {
...
preferences: {
// `store` accepts `Store` collection documents
// as part of the `preferences` object.
store: Ref<Store>,
emailList: Boolean,
...
}
...
}
Create a document relationship
You create relationships by including a reference to a document in another document. Fauna supports the following relationship types:
Relationship type | Example |
---|---|
A book has one author. |
|
An author writes many books. |
|
A reader has many books, and a book has many readers. |
Create a one-to-one relationship
A one-to-one relationship exists when a document in one collection has only one associated document in another collection.
In the following example, each product is associated with one store.
// Get a store
let store = Store.byName("DC Fruits").first()
// Create a product that references the store
Product.create({
name: "limes",
description: "Organic, 1 ct",
store: store
// Return the product `name`, `description`, and resolved `store`
}) { name, description, store }
Create a one-to-many relationship
A one-to-many relationship exists when a document in a collection is associated with one or more documents in the another collection.
For example, one store may be associated with multiple products.
// Get a store
let store = Store.byName("DC Fruits").first()
// Define multiple products that reference the store
let products = [
{ name: "limes", description: "Organic, 2 ct", store: store },
{ name: "limes", description: "Organic, 3 ct", store: store }
]
// Create each product
products.map(product => Product.create({
product
// Return the product `name`, `description`, and resolved `store`
})) { name, description, store }
Create many-to-many relationships
A many-to-many relationship exists when a document in one collection is associated with multiple other documents in another collection, and the reverse.
In Fauna, creating a many-to-many relationship requires a third collection to track the associations.
In the following example, the ManagerToStore
collection tracks relationships
between Manager
and Store
documents. A manager can be associated with
multiple stores. A store can be associated with multiple managers.
The ManagerToStore
collection schema:
collection ManagerToStore {
// The `manager` field accepts `Manager` collection documents.
manager: Ref<Manager>
// The `store` field accepts `Store` collection documents.
store: Ref<Store>
}
The following query creates several ManagerToStore
collection documents. The
documents create a relationship between other Manager
and Store
documents:
// Defines data for `Manager` collection documents.
let managerData = [
{ firstName: "Jane", lastName: "Doe", email: "jane.doe@example.com" },
{ firstName: "John", lastName: "Doe", email: "john.doe@example.com" },
]
// Define data for `Store` collection documents.
let storeData = [
{ name: "Acme Supplies" },
{ name: "Ace Fruits" },
]
// Creates `Manager` and `Store` collection documents using
// the previous data.
let managers = managerData.map(doc => Manager.create(doc))
let stores = storeData.map(doc => Store.create(doc))
// Create `ManagerToStore` collection documents for
// each manager and store.
managers.flatMap(manager =>
stores.map(store =>
ManagerToStore.create({
manager: manager,
store: store
})
)
// Return the resolved `manager` and `store` fields
// for each `ManagerToStore` collection document.
) { manager, store }
Traverse document relationships with projection
When you include a document in another document’s field, Fauna stores the value as a reference. By default, this reference is returned instead of the document itself.
For example, the following Product
document’s store
field contains a
another document:
{
id: "394062089570746432",
coll: Product,
...
name: "pizza",
description: "Frozen Cheese",
...
store: Store("394062089566552128"),
...
}
Use projection to traverse the
reference and return the store
document:
// Use projection to return `name`, `desciption`, and `store` fields
Product.byName("pizza").first() {
name,
description,
store {
id,
name,
address
}
}
Response with the resolved store
reference:
{
name: "pizza",
description: "Frozen Cheese",
store: {
id: "394062089566552128",
name: "Foggy Bottom Market",
address: {
street: "4 Florida Ave",
city: "Washington",
state: "DC",
zipCode: "20037"
}
}
}
Query by document relationship
You can use an index to query documents by their relationships. Using an index can significantly improve query performance and reduce costs, especially for large datasets. For example:
-
Define an index as part of a collection schema:
collection Product { ... store: Ref<Store> ... // Defines the `byStore()` index. // Use the index to get `Product` collection // documents by `store` value. In this case, // `store` contains `Store` collection documents. index byStore { terms [.store] } }
-
Use the index in a projection to query
Product
documents that referenceStore
documents in thestore
field:// Get a `Store` collection document. let store = Store.byName("DC Fruits").first() store { id, name, // Use the `byStore()` index to get all products for the store. products: Product.byStore(store) { id, name, description, } }
{ id: "401783355920613449", name: "DC Fruits", products: { data: [ { id: "401783355930050633", name: "avocados", description: "Conventional Hass, 4ct bag" }, { id: "401783355932147785", name: "limes", description: "Conventional, 1 ct" }, { id: "401783355934244937", name: "limes", description: "Organic, 16 oz bag" }, { id: "401783355937390665", name: "cilantro", description: "Organic, 1 bunch" } ] } }
See Indexes |
---|
Simplify projections with computed fields
You can call an index in a computed field to simplify projections in queries that traverse document relationships. Extending the previous example:
-
Add a computed field to the
Store
collection schema:collection Store { ... // Defines the `all_products` computed field. // The field calls the `Product` collection's // `byStore()` index. compute all_products = ((store) => Product.byStore(store)) }
-
Update the previous query’s projection to use the computed field in place of the
byStore()
index call:// Get a `Store` collection document. let store = Store.byName("DC Fruits").first() store { id, name, // Project the `all_product` computed field instead of // directly calling the `byStore()` index. all_products { id, name, description, } }
{ id: "401783355920613449", name: "DC Fruits", products: { data: [ { id: "401783355930050633", name: "avocados", description: "Conventional Hass, 4ct bag" }, { id: "401783355932147785", name: "limes", description: "Conventional, 1 ct" }, { id: "401783355934244937", name: "limes", description: "Organic, 16 oz bag" }, { id: "401783355937390665", name: "cilantro", description: "Organic, 1 bunch" } ] } }
See Computed field definitions |
---|
Query document relationships in an array
Use mva()
(multi-value attribute) to index document relationships in an
Array field. For example:
-
Include the Array field in an index definition. Wrap the field accessor in
mva()
:collection Order { ... cart: Array<Ref<Product>> ... // Defines the `byProduct()` index. // Use the index to get `Order` collection // documents by `cart` value. In this case, // `cart` contains an array of `Product` collection documents. index byProduct { terms [mva(.cart)] } }
-
Use the index in a projection to query
Order
documents that referenceProduct
documents in thecart
field:// Get a `Product` collection document. let product = Product.byName("avocados").first() // Use projection to return `id`, `name`, and `orders` fields // for the `Product` collection document. product { id, name, // Use the `byProduct()` index to get all orders // that contain the product in the `cart` field. orders: Order.byProduct(product) { id, status, cart { id, name, description } } }
{ id: "401783355930050633", name: "avocados", orders: { data: [ { id: "401783355955216457", status: "processing", cart: [ { id: "401783355930050633", name: "avocados", description: "Conventional Hass, 4ct bag" }, { id: "401783355937390665", name: "cilantro", description: "Organic, 1 bunch" }, { id: "401783355932147785", name: "limes", description: "Conventional, 1 ct" } ] } ] } }
Don’t index document IDs
When indexing a field that contains document, index the entire field, not the document ID:
collection Product {
...
// The `store` field accepts `Store` collection documents.
store: Ref<Store>
...
// Correct:
index byStore {
terms [.store]
}
// Incorrect:
// Indexing `store.id` fails.
// Instead, index by the entire `store`field.
index byStoreId {
terms [.store.id]
}
}
IDs aren’t persistable and can’t be indexed.
Delete document relationships
Removing a field that contains a document reference does not delete the referenced document. For example:
// Updates a `Product` collection document.
Product.byId("<PRODUCT_DOCUMENT_ID>")?.update({
// Removes the `store` field, which contains a
// reference to a `Store` collection document.
// Removing the `store` field does not delete
// the `Store` document.
store: null
})
Deleting a document does not remove its document references. Fauna returns a NullDoc when you traverse a reference to a document that doesn’t exist. For example:
// Gets a `Product` collection document.
// Use projection to return `name`, `desciption`, and `store` fields.
Product.byId("<PRODUCT_DOCUMENT_ID>") {
name,
description,
// The `store` field contains a `Store` collection document.
store
}
{
name: "cups",
description: "Translucent 9 Oz, 100 ct",
price: 6.98,
// If the referenced `Store` collection document doesn't exist,
// the projection returns a NullDoc.
store: Store("<STORE_DOCUMENT_ID>") /* not found */
}
Perform a cascading delete
A cascading delete is an operation where deleting a document in one collection automatically deletes related documents in other collections.
Fauna doesn’t provide automatic cascading deletes for user-defined
collections. Instead, you can use an index and
forEach()
to iterate through a
document’s relationships.
In the following example, you’ll delete a Store
collection document and
any Product
documents that reference the store.
-
Define an index as part of a collection schema:
collection Product { ... store: Ref<Store> ... // Defines the `byStore()` index. // Use the index to get `Product` collection // documents by `store` value. In this case, // `store` contains `Store` collection documents. index byStore { terms [.store] } }
-
Use the index and
forEach()
to delete the store and any related products:// Gets a `Store` collection document. let store = Store.byId("<STORE_DOCUMENT_ID>") // Gets `Product` collection documents that // contain the `Store` document in the `store` field. let products = Product.byStore(store) // Deletes the `Store` collection document. store?.delete() // Deletes `Product` collection documents that // contain the `Store` document in the `store` field. products.forEach(.delete()) // Returns `null`
Is this article helpful?
Tell Fauna how the article can be improved:
Visit Fauna's forums
or email docs@fauna.com
Thank you for your feedback!