iT邦幫忙

2024 iThome 鐵人賽

DAY 26
0
自我挑戰組

從 Python 開發者的角度學習 Rust —— 從語法基礎到實戰應用系列 第 26

[Day 26] Rust 的 Web 應用(三):使用 Rocket 與 MongoDB 建立 RESTful API

  • 分享至 

  • xImage
  •  

在上一篇文章中,我們學習了如何使用 Rocket 建立一個簡單的 Web 應用,並使用 Tera 模板引擎進行 HTML 渲染。這次,我們將繼續探討如何使用 Rocket 建立一個 RESTful API 並與 MongoDB 進行資料庫互動。這種設計在開發 Web 服務中特別常見,特別是需要儲存數據和進行 CRUD 操作時(Create、Read、Update、Delete,分別代表創建、讀取、更新和刪除)。

一、什麼是 RESTful API?

簡單來說,RESTful API 是一種基於 HTTP 協定的 API 設計風格。它通過 HTTP 請求來對資源進行操作,這些操作包括:

  • GET:取得資源,例如 GET /users 來獲取使用者列表,GET /users/1 獲取 ID 為 1 的使用者。
  • POST:新增資源,例如 POST /users 來新增一名使用者。
  • PUT:更新資源,例如 PUT /users/1 修改使用者資料。
  • DELETE:刪除資源,例如 DELETE /users/1 刪除該使用者。

RESTful API 的無狀態特性要求每次請求都包含所有所需資訊,伺服器不會保留先前請求的上下文,這使得 API 具備高度可擴展性與靈活性。


二、建立 MongoDB 資料庫

註冊 MongoDB 帳號

首先我們要先在 MongoDB官網 建立一個雲端資料庫,實際操作步驟如下

https://ithelp.ithome.com.tw/upload/images/20241009/20121176ojRCyVpZPr.png
mongoDB官網點選免費試用,然後就可以用google帳號登入了

https://ithelp.ithome.com.tw/upload/images/20241009/20121176Cl8YNeDXro.png
接下來在此畫面點選建立一個新的組織,名字可以自定義

https://ithelp.ithome.com.tw/upload/images/20241009/20121176ZtOG26rGpj.png
建立組織之後會看到這個畫面,我們可以接著點選 New Project 以建立專案

https://ithelp.ithome.com.tw/upload/images/20241009/20121176XaycazDk8S.png
建立專案完畢後,在這邊點選 Create a cluster 當中的 +Create 按鈕,進入建立資料庫環節

https://ithelp.ithome.com.tw/upload/images/20241009/20121176wXcGdvFbnz.png
這邊應該就很好選了,免費的 M0 方案有提供512MB的免費容量空間,做為輕量型開發綽綽有餘

https://ithelp.ithome.com.tw/upload/images/20241009/20121176QqzCY5UmjU.png
下面這邊我們選擇 Google Cloud ,然後國家選擇台灣

https://ithelp.ithome.com.tw/upload/images/20241009/20121176q4CebySxC5.jpg
接下來會得到一組 UsernamePassword ,可以直接修改密碼,先記得複製記錄下來

https://ithelp.ithome.com.tw/upload/images/20241009/20121176OPvm0kB31K.png
使用者建立完畢之後,按下 Choose a connection method

https://ithelp.ithome.com.tw/upload/images/20241009/20121176DFc2Zg6gRo.png
連接方法點選第一個 Drivers

https://ithelp.ithome.com.tw/upload/images/20241009/20121176gpw28LI5tM.png
在語言選擇方面,選擇 Rust2.1

https://ithelp.ithome.com.tw/upload/images/20241009/20121176BW43zTd4Z6.png
這邊請選擇 Show Password 並且複製網址後備用,然後按下 Done

https://ithelp.ithome.com.tw/upload/images/20241009/201211761tIlkwEXEx.png
接下來在後台就可以看得到剛剛完成的Dataset了

https://ithelp.ithome.com.tw/upload/images/20241009/20121176yfLC5NXDo3.png
如果有資料操作可以在這邊看得到資料內容,剛開始可能會看到載入預設資料集,可以刪除也可保留

接下來我們就可以嘗試透過 Rust 跟 mongoDB互動了


三、建立 RESTful API 的準備工作

在這個範例中,我們會使用 Rocket 建立一個 RESTful API,並結合 MongoDB 資料庫來進行數據操作。我們將創建、查詢、更新和刪除用戶資料。

首先建立一個專案 rust-restful-api

cargo new rust-restful-api
cd rust-restful-api

1. 修改 Cargo.toml

接著,我們需要更新 Cargo.toml 文件以引入必要的套件:

[package]
name = "rust-restful-api"
version = "0.1.0"
edition = "2021"

[dependencies]
rocket = { version = "0.5.0-rc.1", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
mongodb = "2.7"
tokio = { version = "1", features = ["full"] }
futures = "0.3"

套件的作用

  • rocket:Rust 中一個流行的 Web 框架,提供簡單和強大的 API 來構建 Web 應用。我們使用 rocket 來建立 HTTP 路由,處理 GET、POST、PUT 和 DELETE 請求。這裡啟用了 json 特性,允許我們處理 JSON 格式的數據,這對 RESTful API 非常重要。

  • serdeserde_json:這兩個庫用來將 Rust 的數據結構與 JSON 進行互相轉換。serde 是一個高效的序列化/反序列化框架,能夠自動將 Rust 結構體轉換為 JSON,或者將 JSON 數據轉換回 Rust 結構。serde_json 是專門用來處理 JSON 的 serde 擴展。

  • mongodb:MongoDB 的官方 Rust 驅動,讓我們可以與 MongoDB 資料庫互動。這個驅動提供了所有 CRUD 操作(增、查、改、刪)的接口,並且支援 BSON(Binary JSON)來處理 MongoDB 中常用的數據格式,例如 ObjectId

  • tokiotokio 是 Rust 中最流行的異步運行時,用來支援並發和非阻塞操作。這是我們構建異步應用的核心。MongoDB 驅動和 Rocket 框架都是異步的,而 tokio 提供了異步執行所需的核心基礎設施。

  • futuresfutures 庫是 Rust 中標準的異步操作工具包。它提供了多種異步操作的接口和工具,讓我們能夠在異步流和任務間進行轉換。我們使用 futuresTryStreamExt 來處理 MongoDB 查詢返回的異步流。


2. 建立 RESTful API

main.rs 中,我們將設置 MongoDB 的連接並創建一個 User 集合來儲存用戶數據。以下是完整範例程式碼,你可以在 main.rs 中使用這個範例來測試整個 RESTful API 的功能,並與 MongoDB 進行互動,但記得其中 mongoDB串接網址 要改成自己的,其中 需要包含密碼

use futures::stream::TryStreamExt; // 導入 TryStreamExt,提供 `try_collect` 方法以簡化處理 MongoDB 查詢結果
use mongodb::{bson::doc, bson::oid::ObjectId, options::ClientOptions, Client}; // 導入 MongoDB 客戶端、選項及 BSON 文檔的處理工具
use rocket::{delete, fs::FileServer, get, post, put, routes, serde::json::Json, State}; // 導入 Rocket 框架的路由、HTTP 請求方法及狀態管理
use serde::{Deserialize, Serialize}; // 導入 Serde 序列化及反序列化,讓結構體能夠轉換成 JSON

// 定義一個用戶結構,支援 JSON 的序列化和反序列化,並實現 Debug 和 Clone 特徵
#[derive(Debug, Deserialize, Serialize, Clone)]
struct User {
    #[serde(skip_serializing_if = "Option::is_none")] // 如果 id 欄位是 None,序列化時會跳過該欄位
    id: Option<String>, // 用戶的 ID,可能不存在(可選)
    name: String, // 用戶的名字,為必填欄位
}

// Rocket 主程式入口,使用 async 函數以支援異步操作
#[rocket::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 設置 MongoDB 連接選項,使用 MongoDB Atlas 提供的 URL 進行連接
    let client_options = ClientOptions::parse("mongodb+srv://masoufo0310:2ioxaagu@database.541vj.mongodb.net/?retryWrites=true&w=majority&appName=Database").await?;
    let client = Client::with_options(client_options)?; // 建立 MongoDB 客戶端連接

    let _db = client.database("testdb"); // 取得指定的資料庫 testdb

    // 構建 Rocket 應用,並管理 MongoDB 客戶端狀態
    rocket::build()
        .manage(client) // 傳遞 MongoDB 客戶端到 Rocket 管理狀態
        .mount(
            // 註冊路由,將所有 CRUD 操作綁定到應用上
            "/",
            routes![create_user, get_users, update_user, delete_user], // 綁定路由,處理用戶的新增、查詢、更新、刪除
        )
        .mount("/", FileServer::from("../frontend/build")) // 讓 Rocket 提供 React build 目錄中的靜態資源
        .ignite() // 啟動 Rocket 應用,初始化伺服器
        .await?
        .launch() // 啟動伺服器,並開始監聽請求
        .await?;

    Ok(()) // 正常退出時返回 OK
}

// POST 請求,用來新增用戶
#[post("/users", data = "<user>")]
async fn create_user(client: &State<Client>, user: Json<User>) -> Json<User> {
    let collection = client.database("testdb").collection::<User>("users"); // 連接到 MongoDB 的 testdb 資料庫,並選擇 users 集合
    let new_user = User {
        id: None,                // 新增用戶時,ID 設為 None,由 MongoDB 自動生成
        name: user.name.clone(), // 複製請求中的用戶名稱
    };

    collection.insert_one(new_user.clone(), None).await.unwrap(); // 將新用戶資料插入資料庫,忽略錯誤處理
    Json(new_user) // 回傳新增的用戶資料作為 JSON
}

// GET 請求,用來獲取所有用戶資料
#[get("/users")]
async fn get_users(client: &State<Client>) -> Json<Vec<User>> {
    let collection = client
        .database("testdb")
        .collection::<mongodb::bson::Document>("users"); // 查詢集合並指定返回 Document 類型

    let mut cursor = collection.find(None, None).await.unwrap(); // 從集合中查詢所有用戶,獲取 MongoDB 游標
    let mut users: Vec<User> = Vec::new(); // 定義一個空的 Vec 來存放用戶

    while let Some(doc) = cursor.try_next().await.unwrap() {
        let id = doc.get_object_id("_id").unwrap().to_hex(); // 從 BSON 文檔中提取 ObjectId 並轉換為字串
        let name = doc.get_str("name").unwrap().to_string(); // 從 BSON 文檔中提取用戶的名字

        let user = User { id: Some(id), name }; // 構建用戶對象
        users.push(user); // 將用戶加入到用戶列表
    }

    Json(users) // 回傳所有用戶的 JSON 數據
}

// PUT 請求,用來更新指定 ID 的用戶資料
#[put("/users/<id>", data = "<user>")]
async fn update_user(client: &State<Client>, id: String, user: Json<User>) -> Json<User> {
    let collection = client.database("testdb").collection::<User>("users");

    // 將 id 轉換為 ObjectId
    let object_id = match ObjectId::parse_str(&id) {
        Ok(oid) => oid,
        Err(_) => {
            return Json(User {
                id: Some(id),
                name: "Invalid ID format".to_string(),
            })
        }
    };

    let filter = doc! { "_id": object_id }; // 根據 ObjectId 進行過濾
    let update = doc! { "$set": { "name": &user.name } }; // 設置更新條件

    collection.update_one(filter, update, None).await.unwrap();
    Json(User {
        id: Some(id),            // 更新後的用戶 ID 設為指定的 ID
        name: user.name.clone(), // 返回更新後的用戶名字
    })
}

// DELETE 請求,用來刪除指定 ID 的用戶
#[delete("/users/<id>")]
async fn delete_user(client: &State<Client>, id: String) -> String {
    let collection = client.database("testdb").collection::<User>("users"); // 連接到 MongoDB 的 testdb 資料庫,並選擇 users 集合

    // 將傳入的 id 轉換為 ObjectId,如果失敗則回傳錯誤訊息
    let object_id = match ObjectId::parse_str(&id) {
        Ok(oid) => oid,
        Err(_) => return format!("Invalid ID format: {}", id),
    };

    let filter = doc! { "_id": object_id }; // 使用 BSON 格式,根據 ObjectId 進行過濾

    // 執行刪除操作,忽略錯誤處理
    match collection.delete_one(filter, None).await {
        Ok(result) => {
            if result.deleted_count == 1 {
                format!("User with id {} deleted", id) // 回傳刪除成功訊息
            } else {
                format!("No user found with id {}", id) // 如果沒有找到用戶,回傳未找到訊息
            }
        }
        Err(_) => format!("Failed to delete user with id {}", id), // 刪除失敗時回傳錯誤訊息
    }
}

3. CRUD 操作的 RESTful API 路由

我們定義了四組 API 路由來處理用戶數據,包括新增、查詢、更新和刪除用戶。

1. 新增用戶(POST 請求)

#[post("/users", data = "<user>")]
async fn create_user(client: &State<Client>, user: Json<User>) -> Json<User> {
    let collection = client.database("testdb").collection::<User>("users");
    let new_user = User { id: None, name: user.name.clone() };
    
    collection.insert_one(new_user.clone(), None).await.unwrap();
    Json(new_user)
}

這個部分處理新增用戶的操作。當接收到一個 POST 請求時,API 會將請求中的 JSON 數據解析為 User 結構,然後將該用戶的數據插入 MongoDB 中的 users 集合。插入完成後,返回該用戶的數據給前端。

2. 取得所有用戶(GET 請求)

查詢所有用戶的 API 路由:

#[get("/users")]
async fn get_users(client: &State<Client>) -> Json<Vec<User>> {
    let collection = client
        .database("testdb")
        .collection::<mongodb::bson::Document>("users"); // 查詢集合並指定返回 Document 類型

    let mut cursor = collection.find(None, None).await.unwrap(); // 從集合中查詢所有用戶,獲取 MongoDB 游標
    let mut users: Vec<User> = Vec::new(); // 定義一個空的 Vec 來存放用戶

    while let Some(doc) = cursor.try_next().await.unwrap() {
        let id = doc.get_object_id("_id").unwrap().to_hex(); // 從 BSON 文檔中提取 ObjectId 並轉換為字串
        let name = doc.get_str("name").unwrap().to_string(); // 從 BSON 文檔中提取用戶的名字

        let user = User { id: Some(id), name }; // 構建用戶對象
        users.push(user); // 將用戶加入到用戶列表
    }

    Json(users) // 回傳所有用戶的 JSON 數據
}

這段代碼處理取得所有用戶的操作。當發送 GET 請求時,API 從 users 集合中查詢所有用戶,將其轉換為 Vec,然後以 JSON 格式返回用戶數據。

3. 更新用戶(PUT 請求)

更新指定用戶的 API 路由:

#[put("/users/<id>", data = "<user>")]
async fn update_user(client: &State<Client>, id: String, user: Json<User>) -> Json<User> {
    let collection = client.database("testdb").collection::<User>("users");
    let filter = doc! { "_id": id.clone() };
    let update = doc! { "$set": { "name": &user.name } };

    collection.update_one(filter, update, None).await.unwrap();
    Json(User { id: Some(id), name: user.name.clone() })
}

這個部分處理用戶更新。PUT 請求包含了用戶的 ID 及要更新的資料。API 根據提供的用戶 ID 在資料庫中找到該用戶,然後更新他的 name 欄位,並返回更新後的資料。

4. 刪除用戶(DELETE 請求)

刪除指定用戶的 API 路由:

#[delete("/users/<id>")]
async fn delete_user(client: &State<Client>, id: String) -> String {
    let collection = client.database("testdb").collection::<User>("users");
    let filter = doc! { "_id": id.clone() };

    collection.delete_one(filter, None).await.unwrap();
    format!("User with id {} deleted", id)
}

這段程式碼處理刪除用戶。當接收到一個 DELETE 請求時,API 根據請求中的用戶 ID,將相應的用戶從 MongoDB 中刪除,並返回刪除成功的訊息。


五、測試 API

可以使用 Invoke-RestMethodcurl 或 Postman 來測試 API。

1. 測試 POST request:

Windows系統(Powershell)

Invoke-RestMethod -Uri http://localhost:8000/users -Method POST -Body '{"name": "Alice"}' -ContentType 'application/json'

Linux系統

curl -X POST -H "Content-Type: application/json" -d '{"name": "Alice"}' http://localhost:8000/users

這個指令會向 API 發送一個 POST 請求,新增一名名為 Alice 的用戶,發送成功之後會得到以下結果:

https://ithelp.ithome.com.tw/upload/images/20241009/20121176JWIAm7uMj4.png

2. 測試 GET request:

建立完使用者之後,我們可以開啟瀏覽器輸入 http://localhost:8000/users 這個 URL 會向 API 發送一個 GET request,並列出所有的用戶數據。

https://ithelp.ithome.com.tw/upload/images/20241009/20121176NkmOKBBnpE.png

好的,以下是如何測試 PUT 和 DELETE 請求的部分。

3. 測試 PUT request:

我們使用 PUT 請求來更新指定用戶的資料。假設我們已經新增了一名名為 Alice 的用戶,現在想將她的名字更新為 Alice Doe。

Windows 系統 (Powershell):

Invoke-RestMethod -Uri http://localhost:8000/users/<user_id> -Method PUT -Body '{"name": "Alice Doe"}' -ContentType 'application/json'

Linux 系統:

curl -X PUT -H "Content-Type: application/json" -d '{"name": "Alice Doe"}' http://localhost:8000/users/<user_id>

請將 <user_id> 替換為你要更新的用戶的實際 ID。PUT 請求會將指定 ID 的用戶名字更新為 "Alice Doe"。成功後,你會收到更新後的用戶資料作為回應。

例如,如果 ID 為 614b2d39f1a3e8e4c1234567,可以這樣發送請求:

Windows 系統 (Powershell):

Invoke-RestMethod -Uri http://localhost:8000/users/614b2d39f1a3e8e4c1234567 -Method PUT -Body '{"name": "Alice Doe"}' -ContentType 'application/json'

Linux 系統:

curl -X PUT -H "Content-Type: application/json" -d '{"name": "Alice Doe"}' http://localhost:8000/users/614b2d39f1a3e8e4c1234567

執行成功後,將會得到如下回應:

https://ithelp.ithome.com.tw/upload/images/20241009/20121176LeoY4MeNwK.png

4. 測試 DELETE request:

DELETE 請求用於刪除指定的用戶,假設我們想刪除 ID 為 614b2d39f1a3e8e4c1234567 的用戶。

Windows 系統 (Powershell):

Invoke-RestMethod -Uri http://localhost:8000/users/<user_id> -Method DELETE

Linux 系統:

curl -X DELETE http://localhost:8000/users/<user_id>

同樣,將 <user_id> 替換為你要刪除的用戶的實際 ID。例如,刪除 ID 為 614b2d39f1a3e8e4c1234567 的用戶:

Windows 系統 (Powershell):

Invoke-RestMethod -Uri http://localhost:8000/users/614b2d39f1a3e8e4c1234567 -Method DELETE

Linux 系統:

curl -X DELETE http://localhost:8000/users/614b2d39f1a3e8e4c1234567

成功後,會返回一個確認刪除的訊息,例如:

https://ithelp.ithome.com.tw/upload/images/20241009/20121176B5JoPS4sX5.png

這樣就完成了 CRUD 操作的所有部分,通過這些指令,你可以使用 POSTGETPUTDELETE 操作來測試你建立的 RESTful API 與 MongoDB 的互動。在MongoDB後台,我們也可以看得到新增的資料。

https://ithelp.ithome.com.tw/upload/images/20241009/20121176bcZ0vDTsUU.png


總結

在這篇文章中,我們學習了如何使用 RocketMongoDB 建立 RESTful API,並與 MongoDB 進行互動。我們能夠輕鬆地處理資料庫中的數據,並提供一個 API 給外部應用使用。希望這篇文章對你在 Rust Web 開發過程中有所幫助,接下來,我們就要更進一步將 React 作為前端, Rust 作為後端的部分整合,完成一個 React + Rust 的 Web service 實作囉!


上一篇
[Day 25] Rust 的 Web 應用(二):探索 Rocket 框架
下一篇
[Day 27] Rust 的 Web 應用(四):Rust + React 全端開發
系列文
從 Python 開發者的角度學習 Rust —— 從語法基礎到實戰應用30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言