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.

For example, a customer can order several products. A product can also be ordered by several customers.

In Fauna, creating a many-to-many relationship requires a third collection to track the associations.

In the following example, the Order collection tracks relationships between Product and Customer documents.

// Define customer data
let customerData = [
  { firstName: "Dan", lastName: "Doe" },
  { firstName: "Evie", lastName: "Ewin" },
]

// Define product data
let productData = [
  { name: "key limes", description: "Conventional, 2 ct" },
  { name: "key limes", description: "Conventional, 3 ct" },
]

// Create customers and products
let customers = customerData.map(data => Customer.create(data))
let products = productData.map(data => Product.create(data))

// Create orders for each customer and product
customers.flatMap(customer =>
  products.map(product =>
    Order.create({
      customer: customer,
      cart: [{ product: product }]
    })
  )
// Return the resolved `customer` and `cart` for each order
) { customer, cart }

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.

You 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 reference Store documents in the store 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, you can 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(<field>) to index document relationships in an Array field:

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 reference Product documents in the cart 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.

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!