Introduction
This is the manual for the officially supported MongoDB Rust driver, a client side library that can be used to interact with MongoDB deployments in Rust applications. It uses the bson
crate for BSON support. The driver contains a fully async API that supports either tokio
(default) or async-std
, depending on the feature flags set. The driver also has a sync API that may be enabled via feature flag.
Warning about timeouts / cancellation
In async Rust, it is common to implement cancellation and timeouts by dropping a future after a certain period of time instead of polling it to completion. This is how tokio::time::timeout
works, for example. However, doing this with futures returned by the driver can leave the driver's internals in an inconsistent state, which may lead to unpredictable or incorrect behavior (see RUST-937 for more details). As such, it is highly recommended to poll all futures returned from the driver to completion. In order to still use timeout mechanisms like tokio::time::timeout
with the driver, one option is to spawn tasks and time out on their JoinHandle
futures instead of on the driver's futures directly. This will ensure the driver's futures will always be completely polled while also allowing the application to continue in the event of a timeout.
e.g.
#![allow(unused)] fn main() { extern crate mongodb; extern crate tokio; use std::time::Duration; use mongodb::{ Client, bson::doc, }; async fn foo() -> std::result::Result<(), Box<dyn std::error::Error>> { let client = Client::with_uri_str("mongodb://example.com").await?; let collection = client.database("foo").collection("bar"); let handle = tokio::task::spawn(async move { collection.insert_one(doc! { "x": 1 }, None).await }); tokio::time::timeout(Duration::from_secs(5), handle).await???; Ok(()) } }
Minimum supported Rust version (MSRV)
The MSRV for this crate is currently 1.53.0. This will be rarely be increased, and if it ever is, it will only happen in a minor or major version release.
Installation and Features
Importing
The driver is available on crates.io. To use the driver in your application, simply add it to your project's Cargo.toml
.
[dependencies]
mongodb = "2.1.0"
Configuring the async runtime
The driver supports both of the most popular async runtime crates, namely tokio
and async-std
. By default, the driver will use tokio
, but you can explicitly choose a runtime by specifying one of "tokio-runtime"
or "async-std-runtime"
feature flags in your Cargo.toml
.
For example, to instruct the driver to work with async-std
, add the following to your Cargo.toml
:
[dependencies.mongodb]
version = "2.1.0"
default-features = false
features = ["async-std-runtime"]
Enabling the sync API
The driver also provides a blocking sync API. To enable this, add the "sync"
or "tokio-sync"
feature to your Cargo.toml
:
[dependencies.mongodb]
version = "2.1.0"
default-features = false
features = ["sync"]
Note: The sync-specific types can be imported from mongodb::sync
(e.g. mongodb::sync::Client
).
All Feature Flags
Feature | Description | Extra dependencies | Default |
---|---|---|---|
tokio-runtime | Enable support for the tokio async runtime | tokio 1.0 with the full feature | yes |
async-std-runtime | Enable support for the async-std runtime | async-std 1.0 | no |
sync | Expose the synchronous API (mongodb::sync ), using an async-std backend. Cannot be used with the tokio-runtime feature flag. | async-std 1.0 | no |
tokio-sync | Expose the synchronous API (mongodb::sync ), using a tokio backend. Cannot be used with the async-std-runtime feature flag. | tokio 1.0 with the full feature | no |
aws-auth | Enable support for the MONGODB-AWS authentication mechanism. | reqwest 0.11 | no |
bson-uuid-0_8 | Enable support for v0.8 of the uuid crate in the public API of the re-exported bson crate. | n/a | no |
bson-chrono-0_4 | Enable support for v0.4 of the chrono crate in the public API of the re-exported bson crate. | n/a | no |
bson-serde_with | Enable support for the serde_with crate in the public API of the re-exported bson crate. | serde_with 1.0 | no |
zlib-compression | Enable support for compressing messages with zlib | flate2 1.0 | no |
zstd-compression | Enable support for compressing messages with zstd . This flag requires Rust version 1.54. | zstd 0.9.0 | no |
snappy-compression | Enable support for compressing messages with snappy | snap 1.0.5 | no |
openssl-tls | Switch TLS connection handling to use 'openssl'. | openssl 0.10.38 | no |
Connecting to the Database
Connection String
Connecting to a MongoDB database requires using a connection string, a URI of the form:
mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]]
At its simplest this can just specify the host and port, e.g.
mongodb://mongodb0.example.com:27017
For the full range of options supported by the Rust driver, see the documentation for the ClientOptions::parse
method. That method will return a ClientOptions
struct, allowing for directly querying or setting any of the options supported by the Rust driver:
#![allow(unused)] fn main() { extern crate mongodb; use mongodb::options::ClientOptions; async fn run() -> mongodb::error::Result<()> { let mut options = ClientOptions::parse("mongodb://mongodb0.example.com:27017").await?; options.app_name = Some("My App".to_string()); Ok(()) } }
Creating a Client
The Client
struct is the main entry point for the driver. You can create one from a ClientOptions
struct:
#![allow(unused)] fn main() { extern crate mongodb; use mongodb::{Client, options::ClientOptions}; async fn run() -> mongodb::error::Result<()> { let options = ClientOptions::parse("mongodb://mongodb0.example.com:27017").await?; let client = Client::with_options(options)?; Ok(()) } }
As a convenience, if you don't need to modify the ClientOptions
before creating the Client
, you can directly create one from the connection string:
#![allow(unused)] fn main() { extern crate mongodb; use mongodb::Client; async fn run() -> mongodb::error::Result<()> { let client = Client::with_uri_str("mongodb://mongodb0.example.com:27017").await?; Ok(()) } }
Client
uses std::sync::Arc
internally, so it can safely be shared across threads or async tasks. For example:
#![allow(unused)] fn main() { extern crate mongodb; extern crate tokio; use mongodb::{bson::Document, Client, error::Result}; use tokio::task; async fn start_workers() -> Result<()> { let client = Client::with_uri_str("mongodb://example.com").await?; for i in 0..5 { let client_ref = client.clone(); task::spawn(async move { let collection = client_ref.database("items").collection::<Document>(&format!("coll{}", i)); // Do something with the collection }); } Ok(()) } }
Client Performance
While cloning a Client
is very lightweight, creating a new one is an expensive operation. For most use cases, it is highly recommended to create a single Client
and persist it for the lifetime of your application. For more information, see the Performance chapter.
Reading From the Database
Database and Collection Handles
Once you have a Client
, you can call Client::database
to create a handle to a particular database on the server, and Database::collection
to create a handle to a particular collection in that database. Database
and Collection
handles are lightweight - creating them requires no IO, clone
ing them is cheap, and they can be safely shared across threads or async tasks. For example:
#![allow(unused)] fn main() { extern crate mongodb; extern crate tokio; use mongodb::{bson::Document, Client, error::Result}; use tokio::task; async fn start_workers() -> Result<()> { let client = Client::with_uri_str("mongodb://example.com").await?; let db = client.database("items"); for i in 0..5 { let db_ref = db.clone(); task::spawn(async move { let collection = db_ref.collection::<Document>(&format!("coll{}", i)); // Do something with the collection }); } Ok(()) } }
A Collection
can be parameterized with a type for the documents in the collection; this includes but is not limited to just Document
. The various methods that accept instances of the documents (e.g. Collection::insert_one
) require that it implement the Serialize
trait from the serde
crate. Similarly, the methods that return instances (e.g. Collection::find_one
) require that it implement Deserialize
.
Document
implements both and can always be used as the type parameter. However, it is recommended to define types that model your data which you can parameterize your Collection
s with instead, since doing so eliminates a lot of boilerplate deserialization code and is often more performant.
#![allow(unused)] fn main() { extern crate mongodb; extern crate tokio; extern crate serde; use mongodb::{ bson::doc, error::Result, }; use tokio::task; async fn start_workers() -> Result<()> { use mongodb::Client; let client = Client::with_uri_str("mongodb://example.com").await?; use serde::{Deserialize, Serialize}; // Define a type that models our data. #[derive(Clone, Debug, Deserialize, Serialize)] struct Item { id: u32, } // Parameterize our collection with the model. let coll = client.database("items").collection::<Item>("in_stock"); for i in 0..5 { // Perform operations that work with directly our model. coll.insert_one(Item { id: i }, None).await; } Ok(()) } }
For more information, see the Serde Integration section.
Cursors
Results from queries are generally returned via Cursor
, a struct which streams the results back from the server as requested. The Cursor
type implements the Stream
trait from the futures
crate, and in order to access its streaming functionality you need to import at least one of the StreamExt
or TryStreamExt
traits.
# In Cargo.toml, add the following dependency.
futures = "0.3"
#![allow(unused)] fn main() { extern crate mongodb; extern crate serde; extern crate futures; use serde::Deserialize; #[derive(Deserialize)] struct Book { title: String } async fn foo() -> mongodb::error::Result<()> { let typed_collection = mongodb::Client::with_uri_str("").await?.database("").collection::<Book>(""); // This trait is required to use `try_next()` on the cursor use futures::stream::TryStreamExt; use mongodb::{bson::doc, options::FindOptions}; // Query the books in the collection with a filter and an option. let filter = doc! { "author": "George Orwell" }; let find_options = FindOptions::builder().sort(doc! { "title": 1 }).build(); let mut cursor = typed_collection.find(filter, find_options).await?; // Iterate over the results of the cursor. while let Some(book) = cursor.try_next().await? { println!("title: {}", book.title); } Ok(()) } }
Performance
Client
Best Practices
The Client
handles many aspects of database connection behind the scenes that can require manual management for other database drivers; it discovers server topology, monitors it for any changes, and maintains an internal connection pool. This has implications for how a Client
should be used for best performance.
Lifetime
A Client
should be as long-lived as possible. Establishing a new Client
is relatively slow and resource-intensive, so ideally that should only be done once at application startup. Because Client
is implemented using an internal Arc
, it can safely be shared across threads or tasks, and clone
ing it to pass to new contexts is extremely cheap.
#![allow(unused)] fn main() { extern crate mongodb; use mongodb::Client; use std::error::Error; // This will be very slow because it's constructing and tearing down a `Client` // with every request. async fn handle_request_bad() -> Result<(), Box<dyn Error>> { let client = Client::with_uri_str("mongodb://example.com").await?; // Do something with the client Ok(()) } // This will be much faster. async fn handle_request_good(client: &Client) -> Result<(), Box<dyn Error>> { // Do something with the client Ok(()) } }
This is especially noticeable when using a framework that provides connection pooling; because Client
does its own pooling internally, attempting to maintain a pool of Client
s will (somewhat counter-intuitively) result in worse performance than using a single one.
Runtime
A Client
is implicitly bound to the instance of the tokio
or async-std
runtime in which it was created. Attempting to execute operations on a different runtime instance will cause incorrect behavior and unpredictable failures. This is easy to accidentally invoke when testing, as the tokio::test
or async_std::test
helper macros create a new runtime for each test.
#![allow(unused)] fn main() { extern crate mongodb; extern crate once_cell; extern crate tokio; use mongodb::Client; use std::error::Error; use tokio::runtime::Runtime; use once_cell::sync::Lazy; static CLIENT: Lazy<Client> = Lazy::new(|| { let rt = Runtime::new().unwrap(); rt.block_on(async { Client::with_uri_str("mongodb://example.com").await.unwrap() }) }); // This will inconsistently fail. #[tokio::test] async fn test_list_dbs() -> Result<(), Box<dyn Error>> { CLIENT.list_database_names(None, None).await?; Ok(()) } }
To work around this issue, either create a new Client
for every async test, or bundle the Runtime
along with the client and don't use the test helper macros.
#![allow(unused)] fn main() { extern crate mongodb; extern crate once_cell; extern crate tokio; use mongodb::Client; use std::error::Error; use tokio::runtime::Runtime; use once_cell::sync::Lazy; static CLIENT_RUNTIME: Lazy<(Client, Runtime)> = Lazy::new(|| { let rt = Runtime::new().unwrap(); let client = rt.block_on(async { Client::with_uri_str("mongodb://example.com").await.unwrap() }); (client, rt) }); #[test] fn test_list_dbs() -> Result<(), Box<dyn Error>> { let (client, rt) = &*CLIENT_RUNTIME; rt.block_on(async { client.list_database_names(None, None).await })?; Ok(()) } }
or
#![allow(unused)] fn main() { extern crate mongodb; extern crate tokio; use mongodb::Client; use std::error::Error; #[tokio::test] async fn test_list_dbs() -> Result<(), Box<dyn Error>> { let client = Client::with_uri_str("mongodb://example.com").await?; CLIENT.list_database_names(None, None).await?; Ok(()) } }
Parallelism
Where data operations are naturally parallelizable, spawning many asynchronous tasks that use the driver concurrently is often the best way to achieve maximum performance, as the driver is designed to work well in such situations.
#![allow(unused)] fn main() { extern crate mongodb; extern crate tokio; use mongodb::{bson::Document, Client, error::Result}; use tokio::task; async fn start_workers() -> Result<()> { let client = Client::with_uri_str("mongodb://example.com").await?; for i in 0..5 { let client_ref = client.clone(); task::spawn(async move { let collection = client_ref.database("items").collection::<Document>(&format!("coll{}", i)); // Do something with the collection }); } Ok(()) } }