Model relationships between documents

Fauna supports relationships between documents in different collections, enabling complex data modeling without duplicating data.

This approach combines the ease of use of document databases with the data modeling capabilities of a traditional relational database.

Create a document relationship

You can use document references to create relationships between documents. You can then use projection to dynamically resolve document references on read.

Define a document relationship

You can define and enforce document relationships using typed field definitions in a collection schema:

collection Product {
  ...
  // The `category` field accepts `Category` collection documents.
  category: Ref<Category>
  ...
}

Instantiate the relationship

To instantiate the relationship, include the document as a field value:

// Get a `Category` collection document.
let produce = Category.byName("produce").first()

// Create a `Product` document that references
// the `Category` document.
Product.create({
 name: "key lime",
 description: "Organic, 1 ct",
 price: 79,
 // The `category` field includes
 // the `Category` document as a field value.
 category: produce,
 stock: 2000
})

Fauna stores the field value as a document reference. The reference acts as a pointer to the document. The reference contains the document’s collection and document ID.

If the field is not projected, the reference is returned on read:

// An example `Product` collection document.
{
  id: "<DOCUMENT_ID>",
  coll: Product,
  ...
  name: "key lime",
  description: "Organic, 1 ct",
  ...
  // A `Category` document reference.
  category: Category("<CATEGORY_DOCUMENT_ID>"),
  ...
}

Traverse the relationship

Project the field to automatically traverse the document relationship on read:

// Get a `Product` document and project the
// `name`, `description`, and `category` fields.
Product.byName("key lime").first() {
  name,
  description,
  category {
    id,
    name,
    description
  }
}
{
  name: "key lime",
  description: "Organic, 1 ct",
  // The projection resolves the `Category` document
  // reference in the `category` field.
  category: {
    id: "<CATEGORY_DOCUMENT_ID>",
    name: "produce",
    description: "Fresh Produce"
  }
}

You can use projection to traverse multiple, deeply nested relationships in a single query:

// Get a `Customer` document.
let customer = Customer.byId("<CUSTOMER_DOCUMENT_ID>")

// Use the `Order` collection's `byCustomer()` index to get
// `Order` documents based on their `customer` value. The
// previous `Customer` document is passed to the index call.
Order.byCustomer(customer) {
  // The `customer` field references the `Customer` document.
  customer {
    name,
    email
  },
  // The `items` field references a set of `OrderItem` documents.
  items {
    // Each `OrderItem` document references a nested `Product` document.
    product {
      name,
      description,
      price
    },
    quantity
  },
  total,
  status
}
{
  data: [
    {
      // Traverses the `Customer` collection document in
      // the `customer` field.
      customer: {
        name: "John Doe",
        email: "jdoe@example.com"
      },
      // Traverses the set of `OrderItem` collection documents in
      // the `items` field.
      items: {
        data: [
          {
            // Traverses nested `Product` documents in
            //  `OrderItem` documents.
            product: {
              name: "cups",
              description: "Translucent 9 Oz, 100 ct",
              price: 698
            },
            quantity: 8
          },
          {
            product: {
              name: "donkey pinata",
              description: "Original Classic Donkey Pinata",
              price: 2499
            },
            quantity: 10
          }
        ]
      },
      total: 30574,
      status: "cart"
    },
    ...
  ]
}

Model a complex relationship

You can use field definitions and computed fields to model complex document relationships.

Document arrays

A field definition can accept documents as part of an array:

collection Order {
  ...
  // The `items` field accepts an array
  // of `OrderItem` collection documents.
  items: Array<Ref<OrderItem>>
  ...
}

Nested objects

A field definition can accept documents as part of a nested object:

collection Customer {
  ...
  // Defines the `preferences` object field.
  preferences: {
    // Defines the nested `store` field.
    // The `store` field accepts `Store` collection documents.
    store: Ref<Store>,
    emailList: Boolean,
    ...
  }
  ...
}

Sets

You can use a computed field to create a relationship between a document and a set of documents:

collection Customer {
  ...
  // Computed field definition for the `orders` field.
  // `orders` contains a set of `Order` collection documents.
  // The value is computed using the `Order` collection's
  // `byCustomer()` index to get the customer's orders.
  compute orders: Set<Order> = ( customer => Order.byCustomer(customer))
  ...
}

Use computed fields to create query-based relationships

You can use a computed field to create a relationship between documents based on a read-only query:

collection Customer {
  ...
  // Computed field definition for the `cart` field.
  // `cart` contains an `Order` collection document.
  // The value is computed using the `Order` collection's
  // `byCustomerAndStatus()` index to get the first order
  // for the customer with a `cart` status.
  compute cart: Order? = (customer => Order.byCustomerAndStatus(customer, 'cart').first())
  ...
}

Relationship types

You can use document references to model the following relationship types:

Relationship type Definition Example

A document in one collection has only one associated document in another collection.

A book has one author.

A document in a collection is associated with one or more documents in another collection.

An author writes many books.

A document in one collection is associated with multiple other documents in another collection. A document in the other collection is associated with multiple documents in the first collection.

A reader has many books, and a book has many readers.

One-to-one relationship

A one-to-one relationship exists when a document in one collection has only one associated document in another collection.

// Products have a one-to-one relationships with
// its category. Each product has one category.

// Get a `Category` collection document
let produce = Category.byName("produce").first()

// Create a `Product` document
// that references the `Category` document.
Product.create({
 name: "key lime",
 description: "Organic, 1 ct",
 price: 79,
 category: produce,
 stock: 2000
}) { name, description, category }

One-to-many relationship

A one-to-many relationship exists when a document in a collection is associated with one or more documents in another collection.

// Categories have a one-to-many relationship with
// products. A category can be assigned to multiple
// products.

// Get a `Category` collection document
let produce = Category.byName("produce").first()

// Define an array that contains data for
// multiple `Product` documents. Each `Product`
// document references the previous `Category` document.
let products = [
  {
    name: "key limes",
    description: "Organic, 2 ct",
    price: 1_39,
    // `category` contains the `Category` document reference.
    category: produce,
    stock: 1000
  },
  {
    name: "lemons",
    description: "Organic, 3 ct",
    price: 1_39,
    category: produce,
    stock: 1000
  }
]

// Use `map()` to create `Product` documents
// from the previous array.
products.map(product => Product.create({
  product
    // Project `name`, `description`, and resolved
    // `category` fields of the `Product` documents.
})) { name, description, category }

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:

// The `OrderItem` collection creates many-to-many
// relationships between `Order` and `Product` documents.
// A product can be included with multiple orders.
// An order can contain multiple products.
collection OrderItem {
  order: Ref<Order>
  product: Ref<Product>
  quantity: Int

  unique [.order, .product]
}

The following query instantiates the many-to-many relationship:

// Defines data for `Order` collection documents.
let orderData = [
  {
    customer: Customer.byId('111'),
    status: "processing",
    createdAt: Time.now()
  },
  {
    customer: Customer.byId('222'),
    status: "processing",
    createdAt: Time.now()
  }
]

// Defines data for `Product` collection documents.
let productData = [
  {
    name: "kiwis",
    description: "Organic, 2 ct",
    price: 2_39,
    // `category` contains the `Category` document reference.
    category: Category.byName("produce").first(),
    stock: 1000
  },
  {
    name: "oranges",
    description: "Organic, 3 ct",
    price: 3_39,
    category: Category.byName("produce").first(),
    stock: 1000
  }
]

// Creates `Order` and `Product` documents using
// the previous data.
let orders = orderData.map(doc => Order.create(doc))
let products = productData.map(doc => Product.create(doc))

// Create `OrderItem` documents for
// each order and product.
orders.flatMap(order =>
  products.map(product =>
    OrderItem.create({
      order: order,
      product: product,
      quantity: 1
    })
  )
// Return the resolved `order`, `product`, and `quantity`
// fields for each `OrderItem` document.
) { order, product, quantity }

Index document relationships

When you index a field that contains a document, you index a document reference. The reference consists of the document’s collection and document ID:

collection Product {
  ...
  // The `category` field contains
  // a `Category` collection document.
  category: Ref<Category>
  ...
  // Indexes the `category` field as an index term.
  // The index stores `Category` document references.
  // Example reference: Category("<CATEGORY_DOCUMENT_ID>")
  index byCategory {
    terms [.category]
  }
}

An index can’t store a referenced document’s fields. An index also can’t store a computed field that references another document. See Patterns to avoid.

Query indexed document relationships

You can’t run a covered query on an indexed document reference. Projection traverses the document reference, which requires a read of the document. For example:

// An uncovered query.
// The `category` field contains a document, which can't be covered.
Product.byCategory(Category.byId("<CATEGORY_DOCUMENT_ID>")) {
  category
}

Using indexes and computed fields can make queries on document relationships more readable and convenient. See Patterns to use.

Patterns to use

You can’t use an index to run cover queries on document relationships.

However, you can use indexes and computed fields to make queries on document relationships more readable and convenient.

Run an exact match search on a document reference

Use a document as an index term to run exact match searches on a document reference. For example:

  1. Define an index as part of a collection schema:

    // Defines the `Product` collection.
    collection Product {
      ...
      // The `category` field contains
      // a `Category` collection document.
      category: Ref<Category>
      ...
    
      // Defines the `byCategory()` index.
      // Use the index to get `Product` collection
      // documents by `category` value. In this case,
      // `category` contains `Category` collection documents.
      index byCategory {
        terms [.category]
      }
    }
  2. Use the index in a projection to fetch and traverse document references:

    // Get a `Category` collection document.
    let produce = Category.byName("produce").first()
    
    produce {
      id,
      name,
      // Use the `byCategory()` index to get
      // all products for the category.
      products: Product.byCategory(produce) {
        id,
        name,
        description,
      }
    }
    {
      id: "<CATEGORY_DOCUMENT_ID>",
      name: "produce",
      products: {
        data: [
          {
            id: "<PRODUCT_DOCUMENT_ID>"",
            name: "avocados",
            description: "Conventional Hass, 4ct bag"
          },
          {
            id: "<PRODUCT_DOCUMENT_ID>"",
            name: "single lime",
            description: "Conventional, 1 ct"
          },
          ...
        ]
      }
    }

Simplify projections with computed fields

You can call an index in a computed field to simplify projections in queries that traverse document relationships.

The following extends the previous example:

  1. Define the collection schemas:

    // Defines the `Product` collection.
    collection Product {
      ...
      // The `category` field contains
      // a `Category` collection document.
      category: Ref<Category>
      ...
    
      // Defines the `byCategory()` index.
      // Use the index to get `Product` collection
      // documents by `category` value.
        terms [.category]
      }
    }
    
    // Defines the `Category` collection.
    collection Category {
      ...
    
      // Defines the `all_products` computed field.
      // The field calls the `Product` collection's
      // `byCategory()` index.
      compute products: Set<Product> = (
        category => Product.byCategory(category)
      )
    }
  2. Update the previous query’s projection to use the computed field:

    // Get a `Category` collection document.
    let produce = Category.byName("produce").first()
    
    produce {
      id,
      name,
      // Project the `products` computed field instead of
      // directly calling the `byCategory()` index.
      products {
        id,
        name,
        description,
      }
    }
    // The results are the same as the previous query.
    {
      id: "<CATEGORY_DOCUMENT_ID>",
      name: "produce",
      all_products: {
        data: [
          {
            id: "<PRODUCT_DOCUMENT_ID>",
            name: "avocados",
            description: "Conventional Hass, 4ct bag"
          },
          {
            id: "<PRODUCT_DOCUMENT_ID>",
            name: "single lime",
            description: "Conventional, 1 ct"
          },
          ...
        ]
      }
    }
See Computed field definitions

Index an array of document references

Use mva() (multi-value attribute) to index an array of document references. For example:

  1. Include the array field in an index definition. Wrap the field accessor in mva():

    // Defines the `Store` collection.
    collection Store {
      ...
      // The `product` field contains an array of
      // `Product` collection documents.
      products: Array<Ref<Product>>
      ...
    
      // Defines the `byProduct()` index.
      // Use the index to get `Store` collection
      // documents by `products` value.
      index byProduct {
        terms [mva(.products)]
      }
    }
  2. Use the index in a projection to query:

    // Gets a `Product` collection document.
    let product = Product.byName("avocados").first()
    
    // Uses projection to return `id`, `name`, and `orders` fields
    // for the `Product` collection document.
    product {
      id,
      name,
      // Uses the `byProduct()` index to get all stores
      // that contain the product in the `products` field.
      stores: Store.byProduct(product) {
        id,
        name,
        products {
          name
        }
      }
    }
    {
      id: "<PRODUCT_DOCUMENT_ID>",
      name: "avocados",
      stores: {
        data: [
          {
            id: "<STORE_DOCUMENT_ID>",
            name: "DC Fruits",
            products: [
              {
                name: "avocados"
              },
              {
                name: "single lime"
              }
            ]
          },
          ...
        ]
      }
    }

Patterns to avoid

An index can’t store a referenced document’s fields. Avoid index definitions that attempt to index these fields.

Don’t index fields of referenced documents

Don’t attempt to use an index to store fields of a referenced document:

// Defines the `Customer` collection.
collection Customer {
  ...
  // The `address` field contains an
  // `Address` collection document.
  address: Ref<Address>
  ....

  // INCORRECT:
  // Fauna can't index the previous `Address` document's
  // `city` field. The `city` field references another document.
  index byCity {
    terms [.address.city]
  }
}

// Defines the `Address` collection.
collection Address {
  street: String
  city: String
}

Don’t index computed fields that reference other documents

Don’t index a computed field that references another document:

// Defines the `Customer` collection.
collection Customer {
  ...
  // The `address` field contains an
  // `Address` collection document.
  address: Ref<Address>
  ....

  // The `city` computed field gets the previous
  // `Address` document's `city` field.
  compute city = ((customer) => customer.address.city)

  // INCORRECT:
  // Fauna can't index the computed `city` field.
  // The field references another document.
  index byCity {
    terms [.city]
  }
}

collection Address {
  street: String
  city: String
}

Don’t index the IDs of document references

When a field contains a document reference, index the field rather than the referenced document’s ID:

// Defines the `Product` collection.
collection Product {
  // The `category` field contains `Category` collection documents.
  category: Ref<Category>

  // CORRECT:
  index byCategory {
    terms [.category]
  }

  // INCORRECT:
  // Fauna can't index the previous `Category` document's `id` field.
  index byCategoryId {
    terms [.category.id]
  }
}

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 `category` field, which contains a
  // reference to a `Category` collection document.
  // Removing the `category` field does not delete
  // the `Category` document.
 category: null
})

Dangling references

Deleting a document does not remove its inbound document references. Documents may contain references to documents that don’t exist. These are called dangling references. For example:

// Gets a `Product` collection document.
// Use projection to return `name`, `description`, and `category` fields.
Product.byId("<PRODUCT_DOCUMENT_ID>") {
  name,
  description,
  // The `category` field contains a reference to a `Category` collection document.
  category
}
{
  name: "cups",
  description: "Translucent 9 Oz, 100 ct",
  price: 698,
  // If the referenced `Category` collection document doesn't exist,
  // the projection returns a NullDoc.
  category: Category("<CATEGORY_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 set.forEach() to iterate through a document’s relationships.

In the following example, you’ll delete a Category collection document and any Product documents that reference the category.

  1. Define an index as part of a collection schema:

    collection Product {
      ...
      category: Ref<Category>
      ...
    
      // Defines the `byCategory()` index.
      // Use the index to get `Product` collection
      // documents by `category` value. In this case,
      // `category` contains `Category` collection documents.
      index byCategory {
        terms [.category]
      }
    }
  2. Use the index and set.forEach() to delete the category and any related products:

    // Gets a `Category` collection document.
    let category = Category.byId("<CATEGORY_DOCUMENT_ID>")
    // Gets `Product` collection documents that
    // contain the `Category` document in the `category` field.
    let products = Product.byCategory(category)
    
    // Deletes the `Category` collection document.
    category?.delete()
    // Deletes `Product` collection documents that
    // contain the `Category` document in the `category` 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!