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.

Combines document flexibility with native relational capabilities. 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.

Multi-tenancy

No native concept of multi-tenancy. Multi-tenancy is handled by the client application.

Natively multi-tenant. 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.

Change data capture (CDC) and real-time events

Offers Change Streams to track real-time data changes.

You can subscribe to changes on a single collection, a database, or across a cluster.

Offers Event Streams for real-time data changes and Event Feeds for asynchronous event pulls.

You can track changes at the collection, index, document, or specific field level.

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, create a collection schema in the database.

An example FSL collection schema:

// 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`.
}

You can create and manage schema using any of the following:

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", "name", "address"],
         properties: {
            email: {
               bsonType: "string"
            },
            name: {
               bsonType: "string"
            },
            status: {
              bsonType: "string",
              enum: ["silver", "gold", "platinum"]
            },
            address: {
               bsonType: "object",
               required: ["street", "city", "state", "postalCode", "country"],
               properties: {
                  street: {
                     bsonType: "string"
                  },
                  city: {
                     bsonType: "string"
                  },
                  state: {
                     bsonType: "string"
                  },
                  postalCode: {
                     bsonType: "string"
                  },
                  country: {
                     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 {
  name: String
  email: String
  status: "silver" | "gold" | "platinum"?
  address: {
    street: String
    city: String
    state: String
    postalCode: String
    country: String
  }

  *: Any
}

Edit a collection

The following examples add an points 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", "name", "address"],
         properties: {
            email: {
               bsonType: "string"
            },
            name: {
               bsonType: "string"
            },
            status: {
              bsonType: "string",
              enum: ["silver", "gold", "platinum"]
            },
            // Adds the `points` field. Accepts `int` or `null` values.
            // Accepting `null` means the field is not required.
            points: {
               bsonType: "int"
            },
            address: {
               bsonType: "object",
               required: ["street", "city", "state", "postalCode", "country"],
               properties: {
                  street: {
                     bsonType: "string"
                  },
                  city: {
                     bsonType: "string"
                  },
                  state: {
                     bsonType: "string"
                  },
                  postalCode: {
                     bsonType: "string"
                  },
                  country: {
                     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 points 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 {
  name: String
  email: String
  status: "silver" | "gold" | "platinum"?
  address: {
    street: String
    city: String
    state: String
    postalCode: String
    country: String
  }
  // Adds the `points` field. Accepts `int` or `null` values.
  // Accepting `null` means the field is not required.
  points: Int?
  // Adds the `typeConflicts` field as a catch-all field for
  // existing `points` values that aren't `Int` or `null`.
  typeConflicts: { *: Any }?

  *: Any

  migrations {
    // Adds the `typeConflicts` field.
    add .typeConflicts
    // Adds the `points` field.
    add .points
    // Nests non-conforming `points` 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 points field must be an Int value or not present (null).

If a document a non-conforming points 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 any of the following:

Deleting a collection deletes its documents and indexes.

Create and manage documents

Create a document

db.Customer.insertOne({
  name: "John Doe",
  email: "jdoe@example.com",
  address: {
    street: "87856 Mendota Court",
    city: "Washington",
    state: "DC",
    postalCode: "20220",
    country: "US"
  },
  points: 42
})
Customer.create({
  name: "John Doe",
  email: "jdoe@example.com",
  address: {
    street: "87856 Mendota Court",
    city: "Washington",
    state: "DC",
    postalCode: "20220",
    country: "US"
  },
  points: 42
})

Edit a document

db.Customer.updateOne(
  {_id: ObjectId("111")},
  {$set: {email: "jdoe2@example.com"}}
)
Customer.byId("111")?.update({
  email: "jdoe2@example.com"
})

Delete a document

db.Customer.deleteOne({ _id: ObjectId("111") })
Customer.byId("111")?.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 and name fields of the Customer collection.

db.Customer.createIndex({
  "email": 1,
  "name": 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 `name` field value.
    values [.name]
  }
}
// MongoDB automatically uses indexed terms, if applicable.
db.Customer.find({email: "jdoe@example.com"})
// Runs an unindexed query.
Customer.where(.email == "jdoe@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 [.name]
  }
}

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("jdoe@example.com")

Sort collection documents

// Sorts `Product` collection documents by:
// - `price` (ascending), then ...
// - `name` (ascending), then ...
// - `description` (ascending), then ...
// - `stock` (ascending).
db.Product.find().sort({price: 1, name: 1, description: 1, stock: 1})
// Runs an unindexed query.
// Sorts `Product` collection documents by:
// - `price` (ascending), then ...
// - `name` (ascending), then ...
// - `description` (ascending), then ...
// - `stock` (ascending).
Product.all().order(.price, .name, .description, .stock)

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

Define the index in the collection schema:

collection Product {
  ...

  // Defines the `sortedByName()` index for the `Customer`
  // collection.
  index sortedByPriceLowToHigh {
    values [.price, .name, .description, .stock]
  }
}

Call the index in a query:

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

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

Define the index in the collection schema:

collection Customer {
  ...

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

Call the index in a query:

// Use the `sortedByPoints()` index to get `Customer`
// collection documents with a `points` between
// 18 (inclusive) and 100 (inclusive).
// Also sorts results by ascending `points` value.
Customer.sortedByPoints({ 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 OrderItem documents.

  • A one-to-one relationship between each OrderItem document and a Product document.

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: "12345",
    name: "John Doe",
    email: "jdoe@example.com",
    ...
  },
  status: "cart"
  // Embeds an array of `OrderItem` documents.
  items: [
    {
      orderItemID: "12345",
      // Embeds a `Product` document's data in the `product` field.
      product: {
        productID: "111",
        name: "cups",
        description: "Translucent 9 Oz, 100 ct",
        price: 698,
        ...
      },
      quantity: 8
    },
    {
      orderItemID: "67890",
      product: {
        productID: "222",
        name: "donkey pinata",
        description: "Original Classic Donkey Pinata",
        price: 2499,
        ...
      },
      quantity: 10
    }
  ]
})

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(333),
  status: "cart",
  items: [
    {
      // Adds a `OrderItem` document's ID as the `orderItem` field value.
      orderItem: new ObjectId(12345)
    },
    {
      orderItem: new ObjectId(67890)
    }
  ]
})

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 a reference to a `Customer` collection document.
  customer: Ref<Customer>
  status: "cart" | "processing" | "shipped" | "delivered"
  createdAt: Time

  // `items` contains a Set of `OrderItem` collection documents.
  compute items: Set<OrderItem> = (order => OrderItem.byOrder(order))
  compute total: Number = (order => order.items.fold(0, (sum, orderItem) => {
    let orderItem: Any = orderItem
    if (orderItem.product != null) {
      sum + orderItem.product.price * orderItem.quantity
    } else {
      sum
    }
  }))
  payment: { *: Any }

  // Defines the `byCustomer()` index.
  // Use the index to get `Order` documents by `customer` value.
  index byCustomer {
    terms [.customer]
    values [desc(.createdAt), .status]
  }
}

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

// Creates an `Order` collection document.
let cartOrder: Any = Order.create({
  status: "cart",
    // Adds a `Customer` collection document as a field value.
  customer: Customer.byId("333"),
  createdAt: Time.now(),
  payment: {}
})

// Creates an `OrderItem` collection document.
OrderItem.create({
  // Adds a `Product` collection document as a field value.
  product: Product.byId("111"),
  quantity: 8,
  // Adds the previous `Order` collection document as a field value.
  order: cartOrder
})

OrderItem.create({
  product: Product.byId("222"),
  quantity: 10,
  order: cartOrder
})

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("12345") }
  },
  {
    $lookup: {
      from: "Customer",
      localField: "customer",
      foreignField: "_id",
      as: "customer"
    }
  },
  {
    $project: {
      customer: { $arrayElemAt: ["$customer", 0] },
      status: 1
      items: 1,

    }
  }
])
[
  {
    _id: ObjectId('66846ba8c516f4578cd7f153'),
    // Resolves the `Customer` document in the `customer` field.
    customer: {
      _id: ObjectId('66846978c516f4578cd7f14f'),
      name: "John Doe",
      email: "jdoe@example.com",
      ...
    },
    status: "cart",
    items: [
      {
        // Doesn't resolve nested `OrderItem` documents.
        orderItem: ObjectId('668469d2c516f4578cd7f151')
      },
      {
        orderItem: ObjectId('6684699fc516f4578cd7f150')
      }
    ]
  },
  ...
]

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("12345")

// 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.
let order: Any = Order.byCustomer(customer)

order {
  customer {
    name,
    email
  },
  items {
    product {
      name,
      description,
      price
    },
    quantity
  },
  total,
  status
}

The projection fetches any related documents:

{
  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: 2
          },
          {
            product: {
              name: "donkey pinata",
              description: "Original Classic Donkey Pinata",
              price: 2499
            },
            quantity: 1
          },
          {
            product: {
              name: "pizza",
              description: "Frozen Cheese",
              price: 499
            },
            quantity: 3
          }
        ]
      },
      total: 5392,
      status: "cart"
    },
    ...
  ]
}

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!