Fauna for MongoDB users

This guide outlines major differences between MongoDB and Fauna. The guide also translates common MongoDB Query Language (MQL) queries to Fauna Query Language (FQL) and Fauna Schema Language (FSL).

Major differences

The following table outlines major differences between MongoDB and Fauna.

Difference MongoDB Fauna

Data model

Document database. Stores data as denormalized JSON-like documents in collections.

Document-relational database. Stores data as denormalized and normalized JSON-like documents in collections with relationship traversals.

Access control

Role-based access control (RBAC).

RBAC and attribute-based access control (ABAC). Roles and privileges can be assigned dynamically based on conditions at query time.

Administration

Uses managed clusters that require configuration and sizing for best performance and costs.

Fully managed infrastructure that scales automatically. No configuration or sizing needed.

Connection methods

Client drivers.

Client drivers and HTTP API.

Consistency

Strong consistency requires additional configuration and may impact performance. Distributed, consistent transactions across shards require specific configuration.

Strong consistency by default across multiple regions, continents, and clouds. No configuration needed. No performance impacts.

Distribution

Single region by default. Multi-region are configurations available at additional costs.

Natively multi-region. Multi-region setups require no additional costs or maintenance.

Supports multi-cloud setups with Virtual Private Fauna.

Indexes

Supports single, compound, geospatial, and text search indexes.

Managing indexes across multiple shards may require rolling index builds and changes to application logic.

Supports single field and compound indexes.

Indexes with terms are automatically sharded (partitioned) with no operational overhead. Sharding is transparent to the client applications.

Multitenancy

No native concept of multitenancy. Multitenancy is handled by the client application.

Natively multitenant. Uses a nested database-per-tenant model that can scale to hundreds of thousands of tenants.

Tenant databases are instantly allocated and logically isolated from peers.

Schema

Schemaless. Offers schema validation for document writes, but no native support for migrating existing documents.

Supports a progressive schema enforcement, letting you migrate from schemaless to strict enforcement of document types.

Zero-downtime migrations let you update existing documents to the latest schema. Supports constraints for custom data validation rules.

Sharding

Unsharded by default. Clusters can be configured as sharded, which results in reduced capabilities.

Natively sharded with no configuration needed. Sharding is transparent to client applications.

Streaming

Offers change streams to track real-time data changes.

You can subscribe to data changes on a single collection, a database, or deployment.

Offers event streams to track real-time data changes.

You can subscribe to changes for a collection, index, document, or specific fields.

Examples

The following examples compare basic operations in MongoDB and Fauna. MongoDB examples use MQL. Fauna examples use FQL and FSL.

Create and manage collections

Create a schemaless collection

In a schemaless collection, documents can contain any field of any type. Documents in the same collection aren’t required to have the same fields.

// Creates the `Product` collection.
db.createCollection("Product")

To create a collection, add an FSL collection schema in the Fauna Dashboard or upload the schema using the Fauna CLI:

// Defines the `Product` collection.
collection Product {
  // Wildcard constraint.
  // Allows arbitrary ad hoc fields of any type.
  *: Any
  // If a collection schema has no field definitions
  // and no wildcard constraint, it has an implicit
  // wildcard constraint of `*: Any`.
}

Create a collection with predefined fields

Predefined fields let you control what fields are accepted in a collection’s documents.

In MongoDB, you can control the structure of a collection’s documents using a schema validator:

// Creates the `Customer` collection with a schema validator.
db.createCollection("Customer", {
   validator: {
      $jsonSchema: {
         bsonType: "object",
         required: ["email"],
         properties: {
            email: {
               bsonType: "string"
            },
            firstName: {
               bsonType: "string"
            },
            lastName: {
               bsonType: "string"
            },
            status: {
              bsonType: "string",
              enum: ["silver", "gold", "platinum"]
            },
            address: {
               bsonType: "object",
               properties: {
                  street: {
                     bsonType: "string"
                  },
                  city: {
                     bsonType: "string"
                  },
                  state: {
                     bsonType: "string"
                  },
                  zipCode: {
                     bsonType: "string"
                  }
               }
            },
            telephone: {
               bsonType: "string"
            },
            creditCard: {
               bsonType: "object",
               required: ["network", "number"],
               properties: {
                  network: {
                     bsonType: "string",
                     enum: ["Visa", "MasterCard", "American Express", "Discover"]
                  },
                  number: {
                     bsonType: "string"
                  }
               }
            }
         }
      }
   }
})

In Fauna, you can control the structure of a collection’s document type using its collection schema:

// Defines the `Customer` collection.
collection Customer {
  email: String
  firstName: String?
  lastName: String?
  status: String?
  address: {
    street: String?,
    city: String?,
    state: String?,
    zipCode: String?,
  }?
  telephone: String?
  creditCard: {
    network: "Visa" | "MasterCard" | "American Express"?,
    number: String?,
  }?

  *: Any
}

Edit a collection

The following examples add an age field definition to the previous Customer collection schema.

In MongoDB, you can edit a collection’s schema validator:

db.runCommand({
   collMod: "Customer",
   validator: {
      $jsonSchema: {
         bsonType: "object",
         required: ["email"],
         properties: {
            email: {
               bsonType: "string"
            },
            firstName: {
               bsonType: "string"
            },
            lastName: {
               bsonType: "string"
            },
            status: {
              bsonType: "string",
              enum: ["silver", "gold", "platinum"]
            },
            // Adds the `age` field. Accepts `int` or `null` values.
            // Accepting `null` means the field is not required.
            age: {
               bsonType: "int"
            },
            address: {
               bsonType: "object",
               properties: {
                  street: {
                     bsonType: "string"
                  },
                  city: {
                     bsonType: "string"
                  },
                  state: {
                     bsonType: "string"
                  },
                  zipCode: {
                     bsonType: "string"
                  }
               }
            },
            telephone: {
               bsonType: "string"
            },
            creditCard: {
               bsonType: "object",
               required: ["network", "number"],
               properties: {
                  network: {
                     bsonType: "string",
                     enum: ["Visa", "MasterCard", "American Express", "Discover"]
                  },
                  number: {
                     bsonType: "string"
                  }
               }
            }
         }
      }
   }
})

Schema validator changes apply only to new writes, not existing documents.

Existing documents may retain invalid field values until updated. For instance, Customer documents with age as a string before the validator updates would keep this invalid format until updated.

In Fauna, you can update a collection’s document type using a zero-downtime schema migration:

collection Customer {
  email: String
  firstName: String?
  lastName: String?
  status: String?
  address: {
    street: String?,
    city: String?,
    state: String?,
    zipCode: String?,
  }?
  telephone: String?
  creditCard: {
    network: "Visa" | "MasterCard" | "American Express"?,
    number: String?,
  }?
  // Adds the `age` field. Accepts `int` or `null` values.
  // Accepting `null` means the field is not required.
  age: Int?
  // Adds the `typeConflicts` field as a catch-all field for
  // existing `age` values that aren't `Int` or `null`.
  typeConflicts: { *: Any }?

  *: Any

  migrations {
    // Adds the `typeConflicts` field.
    add .typeConflicts
    // Adds the `age` field.
    add .age
    // Nests non-conforming `age` and `typeConflicts`
    // field values in the `typeConflicts` catch-all field.
    move_conflicts .typeConflicts
  }
}

In Fauna, schema migrations can affect existing documents. After the migration, the age field must be an Int value or not present (null).

If a document a non-conforming age value, such as a String, before the migration, the migration nests the value in the typeConflicts field.

View collections

db.getCollectionNames()
Collection.all()

Delete a collection

db.Customer.drop()

To delete a collection, delete its schema using the Dashboard or Fauna CLI. Deleting a collection deletes its documents and indexes.

Create and manage documents

Create a document

db.Customer.insertOne({
  email: "alice.appleseed@example.com",
  firstName: "Alice",
  lastName: "Appleseed",
  age: 42
})
Customer.create({
  email: "alice.appleseed@example.com",
  firstName: "Alice",
  lastName: "Appleseed",
  age: 42
})

Edit a document

db.Customer.updateOne(
  {_id: ObjectId("<DOCUMENT_ID>")},
  {$set: {email: "alice.appleseed2@example.com"}}
)
Customer.byId("<DOCUMENT_ID>")?.update({
  email: "alice.appleseed2@example.com"
})

Delete a document

db.Customer.deleteOne({ _id: ObjectId("<DOCUMENT_ID>") })
Customer.byId("<DOCUMENT_ID>")?.delete()

Perform bulk writes

The following examples set the status field to gold for existing Customer collection documents.

db.Customer.updateMany(
  {},
  { $set: { status: "gold" } }
)
// Uses `forEach()` to iterate through each
// document in the `Customer` collection.
Customer.all().forEach(doc => doc.update({
 status: "gold"
})) // The query doesn't return a value.

View all collection documents

db.Customer.find()
Customer.all()

Indexes and read queries

Create an index

The following examples create an index that covers the email, firstName, and lastName fields of the Customer collection.

db.Customer.createIndex({
  "email": 1,
  "firstName": 1,
  "lastName": 1
})

In Fauna, you define indexes in FSL as part of a collection schema:

collection Customer {
  ...

  index byEmail {
    // `terms` are document fields for exact match searches.
    // In this example, you get `Customer` collection documents
    // by their `email` field value.
    terms [.email]
    // `values` are document fields for sorting and range searches.
    // In this example, you sort or filter index results by their
    // descending `firstName` and `lastName` field values.
    values [.firstName, .lastName]
  }
}
// MongoDB automatically uses indexed terms, if applicable.
db.Customer.find({email: "alice.appleseed@example.com"})
// Runs an unindexed query.
Customer.where(.email == "alice.appleseed@example.com")

For better performance on large datasets, use an index with the email term to run an exact match search.

Define the index in the collection schema:

collection Customer {
  ...

  // Defines the `byEmail()` index for the `Customer`
  // collection.
  index byEmail {
    // Includes the `email` field as an index term.
    terms [.email]
    values [.firstName, .lastName]
  }
}

You call an index as a method on its collection:

// Uses the `Customer` collection's `byEmail()` index
// to run an exact match search on an `email` field value.
Customer.byEmail("alice.appleseed@example.com")

Sort collection documents

// Sorts `Customer` collection documents by:
// - `lastName` (ascending), then ...
// - `firstName` (ascending).
db.Customer.find().sort({lastName: 1, firstName: 1 })
// Runs an unindexed query.
// Sorts `Customer` collection documents by:
// - `lastName` (ascending), then ...
// - `firstName` (ascending).
Customer.all().order(.lastName, .firstName)

For better performance on large datasets, use an index with the lastName and firstName values to sort collection documents.

Define the index in the collection schema:

collection Customer {
  ...

  // Defines the `sortedByName()` index for the `Customer`
  // collection.
  index sortedByName {
    // Includes the `lastName` and `firstName` fields
    // as an index values.
    values [.lastName, .firstName]
  }
}

Call the index in a query:

// Uses the `Customer` collection's `sortedByName()` index
// to sort `Customer` collection documents by:
// - `lastName` (ascending), then ...
// - `firstName` (ascending).
Customer.sortedByName()
// Get `Customer` collection documents with an `age` between
// 18 (inclusive) and 100 (inclusive).
db.Customer.find({ age: { $gte: 18, $lte: 100 }})
// Runs an unindexed query.
// Get `Customer` collection documents with an `age` between
// 18 (inclusive) and 100 (inclusive).
Customer.all().where(.age >= 18 && .age <= 100)

For better performance on large datasets, use an index with the age value to run range searches.

Define the index in the collection schema:

collection Customer {
  ...

  // Defines the `sortedByAge()` index for the `Customer`
  // collection.
  index sortedByAge {
    // Includes the `age` field as an index value.
    values [.age]
  }
}

Call the index in a query:

// Use the `sortedByAge()` index to get `Customer`
// collection documents with an `age` between
// 18 (inclusive) and 100 (inclusive).
// Also sorts results by ascending `age` value.
Customer.sortedByAge({ from: 18, to: 100 })

Document relationships

Create a relationship between documents

The following examples create:

  • A one-to-one relationship between a Order collection document and a Customer document.

  • A one-to-many relationship between the Order document and several Product documents.

In MongoDB, you model relationships between documents using embedded documents or key-based document references. MongoDB doesn’t directly support document relationships.

Embedded documents

To embed a document, you duplicate and nest a document’s data inside another document:

db.Order.insertOne({
  // Embeds a `Customer` document's data in the `Order` document's
  // `customer` field.
  customer: {
    customerId: "<CUSTOMER_DOCUMENT_ID>",
    firstName: "Carol",
    lastName: "Clark",
    email: "carol.clark@example.com",
    ...
  },
  cart: [
    {
      // Embeds a `Product` document's data in the `product` field.
      product: {
        productID: "<PRODUCT_DOCUMENT_ID>",
        name: "limes",
        description: "Conventional, 16 oz bag",
        ...
      },
      quantity: 8,
      price: 2.99
    },
    {
      product: {
        productID: "<PRODUCT_DOCUMENT_ID>",
        name: "pizza",
        description: "Frozen Cheese",
        ...
      },
      quantity: 8,
      price: 4.99
    },
  ],
  status: "processing"
})

Embedding documents increases storage costs and can impact performance. Embedding documents can also make it difficult to keep data synced and up to date.

Key-based document references

To use a key-based document reference, you include a document’s ID as a field value. Key-based references are untyped and indistinguishable from other fields.

db.Order.insertOne({
  // Adds a `Customer` document's ID as the `Order` document's
  // `customer` field value.
  customer: new ObjectId("<CUSTOMER_DOCUMENT_ID>"),
  cart: [
    {
      // Adds a `Product` document's ID as the `product` field value.
      product: new ObjectId("<PRODUCT_DOCUMENT_ID>"),
      quantity: 8,
      price: 2.99
    },
    {
      product: new ObjectId("<PRODUCT_DOCUMENT_ID>"),
      quantity: 8,
      price: 4.99
    }
  ],
  status: "processing"
})

In MongoDB, document references are not dynamically resolved on read. See Traverse document relationships.

In Fauna, you can define and enforce document relationships using typed field definitions in the FSL collection schema. You can also index fields that contain documents.

collection Order {
  // Accepts `Customer` collection documents and `null`.
  customer: Ref<Customer>?,
  // `cart` contains an array of objects.
  cart: Array<{
    // Accepts `Product` collection documents and `null`.
    product: Ref<Product>?,
    quantity: Int?,
    price: Double?,
  }>?
  status: String?
  ...


  // Defines the `byCustomer()` index.
  // Use the index to get `Order` documents by `customer` value.
  // `customer`, if present, contains a `Customer` document.
  index byCustomer {
    terms [.customer]
  }
}

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

Order.create({
  // Adds a `Customer` collection document as a field value.
  customer: Customer("<CUSTOMER_DOCUMENT_ID>"),
  cart: [
    {
      // Adds a `Product` collection document as a field value.
      product: Product("<PRODUCT_DOCUMENT_ID>"),
      quantity: 8,
      price: 2.99
    },
    {
      product: Product("<PRODUCT_DOCUMENT_ID>"),
      quantity: 8,
      price: 4.99
    }
  ],
  status: "processing"
})

This approach avoids duplicating data. You can use projection to automatically traverse the document relationship. See Traverse document relationships.

Traverse document relationships

If using key-based document references in MongoDB, you must run multiple queries to resolve the references:

db.Order.aggregate([
  {
    $match: { customer: new ObjectId("<CUSTOMER_DOCUMENT_ID>") }
  },
  {
    $lookup: {
      from: "Customer",
      localField: "customer",
      foreignField: "_id",
      as: "customer"
    }
  },
  {
    $project: {
      customer: { $arrayElemAt: ["$customer", 0] },
      cart: 1,
      status: 1
    }
  }
])
[
  {
    _id: ObjectId('66846ba8c516f4578cd7f153'),
    // Resolves the `Customer` document in the `customer` field.
    customer: {
      _id: ObjectId('66846978c516f4578cd7f14f'),
      firstName: 'Carol',
      lastName: 'Clark',
      email: 'carol.clark@example.com',
      address: {
        street: '5 Troy Trail',
        city: 'Washington',
        state: 'DC',
        zipCode: '20220'
      },
      telephone: '907-949-4470',
      creditCard: { network: 'Visa', number: '4532636730015542' }
    },
    cart: [
      {
        // Doesn't resolve nested `Product` documents.
        product: ObjectId('668469d2c516f4578cd7f151'),
        quantity: 8,
        price: 2.99
      },
      {
        product: ObjectId('6684699fc516f4578cd7f150'),
        quantity: 8,
        price: 4.99
      }
    ],
    status: 'processing'
  },
  ...
]

These queries compound if a collection’s documents include multiple or deeply nested document references.

In FQL, you dynamically traverse document relationships by projecting fields that contain documents. You can traverse multiple, deeply nested relationships in a single query:

// Uses a variable to reference a `Customer` collection document.
let customer = Customer.byId("<CUSTOMER_DOCUMENT_ID>")

// Uses 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) {
  customer,
  cart {
    product {
        name,
        description,
        price,
        store
    },
    quantity,
    price
  },
  status
}

The projection fetches any related documents:

{
  data: [
    {
      // Traverses the `Customer` collection document in
      // the `customer` field.
      customer: {
        id: "402322135539777613",
        coll: Customer,
        ts: Time("2099-07-02T17:58:57.640Z"),
        firstName: "Carol",
        lastName: "Clark",
        email: "carol.clark@example.com",
        ...
      },
      cart: [
        {
          // Traverses nested `Product` documents.
          product: {
            name: "limes",
            description: "Conventional, 16 oz bag",
            price: 2.99,
            // Traverses nested `Store` documents in
            // nested `Product` documents.
            store: {
              id: "402322135518806093",
              coll: Store,
              ts: Time("2099-07-02T17:58:57.640Z"),
              name: "Foggy Bottom Market",
              address: {
                street: "4 Florida Ave",
                city: "Washington",
                state: "DC",
                zipCode: "20037"
              }
            }
          },
          },
          quantity: 8,
          price: 2.99
        },
        {
          product: {
            name: "pizza",
            description: "Frozen Cheese",
            price: 4.99,
            store: {
              id: "402322135518806093",
              coll: Store,
              ts: Time("2099-07-02T17:58:57.640Z"),
              name: "Foggy Bottom Market",
              address: {
                street: "4 Florida Ave",
                city: "Washington",
                state: "DC",
                zipCode: "20037"
              }
            }
          },
          },
          quantity: 8,
          price: 4.99
        }
      ],
      status: "processing"
    },
    ...
  ]
}

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!