今天继续使用 Rust 构建 API,第二部分。
这部分我将解释如何将 API 连接到 MongoDB。我将创建两个端点,一个用于在数据库中添加数据,另一个用于从数据库中检索数据。
我将把所有代码放在同一个文件 (src/main.rs
) 中。当然,在实际情况中,你应该将代码拆分为多个文件/目录,并将控制器和服务之间的逻辑分离。
为了能够跟着本文实际动手,你需要安装 Rust 和 MongoDB,同时启动 MongoDB。如果你有使用 Docker,以下方式可能是最简单的:
docker run -d -p 27017:27017 --name mongo mongo
我们需要创建一个 .env
文件,安全地存储 MongoDB 连接字符串。如果你使用 Git,请确保添加 .env
到 .gitignore
中。永远不要把 .env
文件提交了。
打开本系列第 1 部分的项目并创建 .env
文件,并添加如下内容:
MONGODB_URI=mongodb://localhost:27017
根据运行 MongoDB 的方式,确保将连接字符串替换为正确的连接字符串。
最好的做法是有一个 .env.example
文件(这个文件应该被提交到代码仓库) ,这样当某人第一次拉取项目时,他们就有一个关于如何设置 .env
文件的模板。这也是目前的最佳实践!
创建一个 .env.example
文件,添加如下内容:
MONGODB_URI= ** MongoDB URI (e.g. mongodb://localhost:27017)
我们需要添加 2 个新的依赖项:dotenv
和 mongodb
。
dotenv
是一个 crate,允许我们使用 .env
文件,它将文件中的变量添加到环境变量中。
mongodb
是官方的 MongoDB Rust 驱动程序。它允许我们与数据库交互。
更新 Cargo.toml
文件:
[dependencies]
tide = "0.16"
async-std = { version = "1", features = ["attributes"] }
serde = { version = "1", features = ["derive"] }
dotenv = "0.15"
mongodb = { version = "1", features = ["async-std-runtime"], default-features = false }
因为我们使用了 async-std
运行时,所以我们需要禁用 mongodb
crate 的缺省特性。因为默认情况下它使用 tokio
。然后我们需要添加 async-std-runtime
特性来使用 async-std
运行时。
我们需要对代码进行一些修改:
use async_std::stream::StreamExt;
use dotenv::dotenv;
use mongodb::bson::doc;
use serde::{Deserialize, Serialize};
use std::env;
use tide::{Body, Request, Response, StatusCode};
这些都是我们将使用的所有模块项。
还记得上次我们设置了一个空的 Tide State
吗?这次我们将使用 State
将数据库连接传递给控制器。我们需要像这样更新 State
结构
#[derive(Clone)]
struct State {
db: mongodb::Database,
}
现在我们需要更新 main
函数。必须创建数据库连接,并在创建 Tide 应用程序之前添加它的状态。
下面是更新后的 main
函数:
#[async_std::main]
async fn main() -> tide::Result<()> {
// Use the dotenv crate to read the .env file and add the environment variables
dotenv().ok(); // Create the MongoDB client options with the connection string from the environment variables
let mongodb_client_options =
mongodb::options::ClientOptions::parse(&env::var("MONGODB_URI").unwrap()).await?;
// Instantiate the MongoDB client
let mongodb_client = mongodb::Client::with_options(mongodb_client_options)?;
// Get a handle to the "rust-api-example" database
let db = mongodb_client.database("rust-api-example");
// Create the Tide state with the database connection
let state = State { db };
let mut app = tide::with_state(state);
app.at("/hello").get(hello);
app.listen("127.0.0.1:8080").await?;
return Ok(());
}
我在代码中添加了注释,以帮助你理解发生了什么。
首先调用了 dotenv().ok()
方法从 .env
文件读取并添加到环境变量。然后创建了一个到数据库的连接。最后,我把这个连接传递给了 Tide State
,这样我们的控制器就可以访问它了。
现在我们将创建两个控制器,一个用于在数据库中创建文档,另一个用于检索这些文档。
下面是第一个控制器的代码:
#[derive(Debug, Serialize, Deserialize)]
// The request's body structure
pub struct Item {
pub name: String,
pub price: f32,
}async fn post_item(mut req: Request<State>) -> tide::Result {
// Read the request's body and transform it into a struct
let item = req.body_json::<Item>().await?;
// Recover the database connection handle from the Tide state
let db = &req.state().db;
// Get a handle to the "items" collection
let items_collection = db.collection_with_type::<Item>("items");
// Insert a new Item in the "items" collection using values
// from the request's body
items_collection
.insert_one(
Item {
name: item.name,
price: item.price,
},
None,
)
.await?;
// Return 200 if everything went fine
return Ok(Response::new(StatusCode::Ok));
}
首先,我定义了一个 Item
结构体,它表示请求的主体。主体应该是一个 JSON 对象,其属性 name
的类型为 string
,属性 price
的类型为 number
。
在函数内部,我首先尝试读取请求体。我没有做任何验证。实际项目中,不要像我一样,应该总是在使用请求体之前验证它。
我从 Tide State
恢复数据库连接。然后我得到一个 items
集合的句柄。
最后,我尝试使用请求体的值在集合中插入一个新文档(Mongo 的 Document)。
如果一切顺利的话,会返回 HTTP 状态码 200 的响应。
现在,让我们创建第二个控制器来从 items
集合中检索文档,下面是代码:
async fn get_items(req: Request<State>) -> tide::Result<tide::Body> {
// Recover the database connection handle from the Tide state
let db = &req.state().db; // Get a handle to the "items" collection
let items_collection = db.collection_with_type::<Item>("items");
// Find all the documents from the "items" collection
let mut cursor = items_collection.find(None, None).await?;
// Create a new empty Vector of Item
let mut data = Vec::<Item>::new();
// Loop through the results of the find query
while let Some(result) = cursor.next().await {
// If the result is ok, add the Item in the Vector
if let Ok(item) = result {
data.push(item);
}
}
// Send the response with the list of items
return Body::from_json(&data);
}
在函数内部,我从 Tide State
检索到数据库连接的句柄,然后获得 items
集合的句柄。
我使用 find
函数检索 items
集合中的所有文档。然后我创建一个新的空的 Vector Item。
我循环 find
查询结果,并在 Vector 中插入每个 item
。
最后,我将响应与主体中的 items 列表一起发送。
控制器已经准备好了,但是我们仍然需要将它们添加到 main
函数中。在 main
函数的 hello
路由之后添加以下内容:
app.at("/items").get(get_items);
app.at("/items").post(post_item);
好了,让我们测试一下,用下面的命令启动服务器:
cargo run
你现在应该能够发送一个 POST
请求给 /items
(http://localhost:8080/items
)。下面是 Body 的一个示例(确保将 Content-Type
头设置为 application/json
)。
{
"name": "coffee",
"price": 2.5
}
你应该得到一个状态码为 200 的空响应。
然后尝试检索你刚刚创建的文档,通过发送一个 GET
请求给 /items
( http://localhost:8080/items
) 获得。
你应该接收到一个数组,其中一个条目就是我们刚刚创建的项。
[
{
"name": "coffee",
"price": 2.5
}
]
这就是该系列的第二部分,希望它对你有所帮助。
我是 polarisxu,北大硕士毕业,曾在 360 等知名互联网公司工作,10多年技术研发与架构经验!2012 年接触 Go 语言并创建了 Go 语言中文网!著有《Go语言编程之旅》、开源图书《Go语言标准库》等。
坚持输出技术(包括 Go、Rust 等技术)、职场心得和创业感悟!欢迎关注「polarisxu」一起成长!也欢迎加我微信好友交流:gopherstudio