用 Rust 構建 API 系列教程:第二部分

關注「Rust 編程指北」,一起學習 Rust,給未來投資

大家好,我是胖蟹哥。

今天繼續使用 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 個新的依賴項:dotenvmongodb

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
  }
]

結尾

這就是該系列的第二部分,希望它對你有所幫助。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/wWoxMFmy3bwYXEhiIYtVdQ