Getting Started with MongoDB using Rust
Introduction
MongoDB
is a NoSQL database that stores data in documents made up of field-value pairs. These are stored using BSON
, a binary representation of JSON
documents. MongoDB documents are equivalent to rows in a relational database table, and the document fields (key-value pairs) are similar to columns. A MongoDB document is part of a collection
, and one or more collections are part of database
. MongoDB has an API that supports regular CRUD
operations (create, read, update, delete) along with aggregation, geospatial queries, and text search. MongoDB also provides high availability and replication. One can distribute data across multiple MongoDB servers (called Replica sets) to provide redundancy and sharding.
There are multiple client drivers that MongoDB officially supports. These include Java, Python, Node.js, PHP, Ruby, Swift, C, C++, C#, Go, and Rust.
In this article, you will learn how to use the Rust driver for MongoDB by building a simple command-line application.
MongoDB Rust driver
MongoDB Rust driver is used to interface with MongoDB from Rust applications. Although the application in this article uses the synchronous API, the Rust driver also provides an asynchronous API.
It supports both asynchronous runtime crates - tokio
and async-std
. tokio
is the default runtime, but you can override this by choosing a runtime using the feature flags in Cargo.toml
.
In addition to the asynchronous API, some of the important feature flags supported by the MongoDB Rust driver include:
openssl-tls
: TLS connection handling is done usingopenssl
.bson-uuid-1
- Support forv1.x
of theuuid
crate in the public API of the re-exportedbson
crate.bson-serde_with
: Support for theserde_with
crate in the public API of the re-exportedbson
crate.
Prerequisites
Before following the steps in this guide, you need to:
- Deploy a new Ubuntu 22.04 LTS Vultr cloud server.
- Create a non-root sudo user.
- Install Docker.
- Install a recent version of Rust.
Create a new Rust project
SSH as the non-root user to the Ubuntu server you deployed in the Prerequisites section.
Create a new Rust project and change into the directory:
$ cargo new mongodb-rust $ cd mongodb-rust
This will create a new
Cargo
package including aCargo.toml
manifest and asrc/main.rs
file.Replace the contents of
Cargo.toml
file with below:[package] name = "user-app" version = "0.1.0" edition = "2018" [dependencies.mongodb] version = "1.1.1" default-features = false features = ["sync"] [dependencies.serde] version = "1.0.118" [dependencies.bson] version = "1.1.0"
Start MongoDB container
On the Ubuntu server, start the MongoDB container using Docker.
$ docker run -it --rm -p 27017:27017 mongo
Once the container has started, MongoDB will be accessible over port 27017
.
Connect to MongoDB with Rust driver
Replace the contents of
src/main.rs
file with the below code:use mongodb::sync::Client; use mongodb::sync::Collection; use mongodb::bson::{doc, Bson}; use serde::{Deserialize, Serialize}; struct UsersManager { coll: Collection } fn main() { let conn_string = std::env::var_os("MONGODB_URL").expect("missing environment variable MONGODB_URL").to_str().expect("missing MONGODB_URL").to_owned(); let users_db = std::env::var_os("MONGODB_DATABASE").expect("missing environment variable MONGODB_DATABASE").to_str().expect("missing MONGODB_DATABASE").to_owned(); let users_collection = std::env::var_os("MONGODB_COLLECTION").expect("missing environment variable MONGODB_COLLECTION").to_str().expect("missing MONGODB_COLLECTION").to_owned(); let um = UsersManager::new(conn_string,users_db.as_str(), users_collection.as_str()); } impl UsersManager{ fn new(conn_string: String, db_name: &str, coll_name: &str) -> Self{ let mongo_client = Client::with_uri_str(&*conn_string).expect("failed to create client"); println!("successfully connected to mongodb"); let users_coll = mongo_client.database(db_name).collection(coll_name); UsersManager{coll: users_coll} } }
Build the program:
$ cargo build
Once the program is compiled and built, you should see an output similar to this:
Finished dev [unoptimized + debuginfo] target(s) in 10.10s
Run the program:
$ export MONGODB_URL=mongodb://localhost:27017 $ export MONGODB_DATABASE=users-db $ export MONGODB_COLLECTION=users $ cargo run
If connected, you should see the following output:
successfully connected to mongodb
Add the User
struct
Add the code below to src/main.rs
file:
#[derive(Serialize, Deserialize)]
struct User {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
user_id: Option<bson::oid::ObjectId>,
#[serde(rename = "description")]
email: String,
status: String,
}
Add a method to create a user
Add the code below to src/main.rs
file (under impl UsersManager
after the new
method):
fn add_user(self, email: &str) {
let new_user = User {
user_id: None,
email: String::from(email),
status: String::from("enabled"),
};
let user_doc = mongodb::bson::to_bson(&new_user).expect("conversion failed").as_document().expect("conversion failed").to_owned();
let insert_result = self.coll.insert_one(user_doc, None).expect("failed to add user");
println!("inserted user with id = {}", insert_result.inserted_id);
}
Add a method to list all the users
Add the code below to src/main.rs
file (under impl UsersManager
after the add_user
method):
fn list_users(self, status_filter: &str) {
let mut filter = doc!{};
if status_filter == "enabled" || status_filter == "disabled"{
println!("listing '{}' users",status_filter);
filter = doc!{"status": status_filter}
} else if status_filter != "all" {
panic!("invalid user status")
}
let mut users = self.coll.find(filter, None).expect("failed to find users");
while let Some(result) = users.next() {
let user_doc = result.expect("user not present");
let user: User = bson::from_bson(Bson::Document(user_doc)).expect("conversion failed");
println!("user_id: {} \nemail: {} \nstatus: {}\n=========", user.user_id.expect("user id missing"), user.email, user.status);
}
}
Add method to update user status
Add the code below to src/main.rs
file (under impl UsersManager
after the list_users
method):
fn update_user(self, user_id: &str, status: &str) {
if status != "enabled" && status != "disabled" {
panic!("invalid user status")
}
println!("updating user {} status to {}", user_id, status);
let id_filter = doc! {"_id": bson::oid::ObjectId::with_string(user_id).expect("user_id is not valid ObjectID")};
let r = self.coll.update_one(id_filter, doc! {"$set": { "status": status }}, None).expect("user update failed");
if r.modified_count == 1 {
println!("updated status for user id {}",user_id);
} else if r.matched_count == 0 {
println!("could not update. check user id {}",user_id);
}
}
Add a method to delete a user
Add the code below to src/main.rs
file (under impl UsersManager
after the update_user
method):
fn delete_user(self, user_id: &str) {
let id_filter = doc! {"_id": bson::oid::ObjectId::with_string(user_id).expect("user_id is not valid ObjectID")};
self.coll.delete_one(id_filter, None).expect("delete failed").deleted_count;
println!("deleted user {}", user_id);
}
Add command-line operations
Add the code below to the main
function in src/main.rs
file:
let ops: Vec<String> = std::env::args().collect();
let operation = ops[1].as_str();
match operation {
"create" => um.add_user(ops[2].as_str()),
"list" => um.list_users(ops[2].as_str()),
"update" => um.update_user(ops[2].as_str(), ops[3].as_str()),
"delete" => um.delete_user(ops[2].as_str()),
_ => panic!("invalid user operation specified")
}
Test the application
Re-build the program:
$ cargo build --release
You should see output similar to this (output has been redacted for brevity):
..... Compiling bson v1.2.4 Compiling mongodb v1.2.5 Compiling user-app v0.1.0 (/Users/demo/mongodb-rust) Finished release [optimized] target(s) in 37.84s
Change to the directory where the application binary is present:
$ cd target/release
You can now test the CRUD
(Create, Read, Update, Delete) operations supported by the command-line application.
Create users
$ ./user-app create "user1@foo.com"
$ ./user-app create "user2@foo.com"
$ ./user-app create "user3@foo.com"
If successful, for each user that was created, you should see an output with the MongoDB _id
of the newly created user:
inserted user with id = ObjectId("63b5648cbd0aa2dad409a3d7")
Note that the object ID might be different in your case.
List all the users
$ ./user-app list all
You should see the users that you added in the previous step.
user_id: 63b5648cbd0aa2dad409a3d7
email: user1@foo.com
status: enabled
=========
user_id: 63b564c5f921fd0a9d0ea7ff
email: user2@foo.com
status: enabled
=========
user_id: 63b564c67f335133190fd1e2
email: user3@foo.com
status: enabled
=========
Note that the user (object) IDs might be different in your case.
Update the user status
Specify the user ID of the user whose status you want to update, along with the status (enabled
or disabled
):
$ ./user-app update 63b5648cbd0aa2dad409a3d7 disabled
Note that the user (object) ID might be different in your case. Use the one in your database.
You should see an output similar to this:
updating user 63b5648cbd0aa2dad409a3d7 status to disabled
updated status for user id 63b5648cbd0aa2dad409a3d7
List users based on their status
To fetch enabled
users:
$ ./user-app list enabled
You should see an output similar to this:
listing 'enabled' users
user_id: 63b564c5f921fd0a9d0ea7ff
email: user2@foo.com
status: enabled
=========
user_id: 63b564c67f335133190fd1e2
email: user3@foo.com
status: enabled
=========
Note that the user (object) IDs might be different in your case.
To fetch disabled
users:
$ ./user-app list disabled
You should see an output similar to this:
listing 'disabled' users
user_id: 63b5648cbd0aa2dad409a3d7
email: user1@foo.com
status: disabled
=========
Note that the user (object) ID might be different in your case.
Delete a user
Specify the user ID of the user who you want to delete.
$ ./user-app delete 63b5648cbd0aa2dad409a3d7
Note that the user (object) ID might be different in your case. Use the one in your database.
You should see an output similar to this:
deleted user 63b5648cbd0aa2dad409a3d7
Note that the user (object) ID might be different in your case.
List all the users to confirm
$ ./user-app list all
You should see an output similar to this. The user you deleted before will not be present.
user_id: 63b564c5f921fd0a9d0ea7ff
email: user2@foo.com
status: enabled
=========
user_id: 63b564c67f335133190fd1e2
email: user3@foo.com
status: enabled
=========
Note that the user (object) ID might be different in your case.
Conclusion
In this article, you started a MongoDB instance using Docker and interacted with it using a command-line application written in Rust.
You can also learn more in the following documentation: