返回文章列表

使用 Actix-web、Diesel 與 PostgreSQL 建構 REST API

本文為一份完整的後端開發教學,指導如何使用 Actix-web 框架、Diesel ORM 與 PostgreSQL 資料庫建構一個 REST API。內容涵蓋使用 Docker 部署資料庫、透過 Diesel CLI 進行資料庫遷移,以及編寫從資料庫讀取資料並以 JSON 格式回傳的 API 端點。

Web 開發 Rust

要將一個網站從靜態內容展示提升為具備完整互動能力的 Web 應用,建構一個後端 REST API 並整合資料庫是關鍵的一步。本文將引導您完成一個完整的後端開發流程,涵蓋了從使用 Docker 部署 PostgreSQL 資料庫,到利用 Diesel ORM 進行資料庫遷移與互動,最終透過 Actix-web 框架建立一個能提供 JSON 資料的 REST API。

專案架構概覽

在開始之前,讓我們先了解整個應用程式的架構。前端(一個簡單的 HTML 頁面)將透過 HTTP 請求與我們的 Actix-web 後端 API 溝通。後端則使用 Diesel 作為 ORM (物件關聯對映) 來操作儲存在 PostgreSQL 資料庫中的資料。

圖表解說:應用程式架構圖

此圖清晰地展示了從前端到資料庫的完整技術堆疊及其互動關係。

@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 16
title Catdex 應用程式架構

package "前端 (Browser)" {
  [HTML/JS]
}

package "後端 (Actix-web Server)" {
  [API Endpoint]
  [Diesel ORM]
}

package "資料庫 (Docker Container)" {
  [PostgreSQL]
}

[HTML/JS] ..> [API Endpoint] : HTTP Request (fetch)
[API Endpoint] -> [Diesel ORM] : Rust 函式呼叫
[Diesel ORM] ..> [PostgreSQL] : SQL 查詢

@enduml

步驟一:環境準備 - Docker 與 PostgreSQL

在開發環境中直接安裝資料庫可能會帶來版本與設定的困擾。使用 Docker 容器化技術,我們可以快速啟動一個乾淨、隔離的 PostgreSQL 環境。

首先,請確保您已安裝 Docker。接著,執行以下指令來啟動一個 PostgreSQL 容器:

docker run --name catdex-db \
    -e POSTGRES_PASSWORD=mypassword \
    -p 5432:5432 \
    -d \
    postgres:12.3-alpine

指令解說

  • --name catdex-db: 為容器命名,方便管理。
  • -e POSTGRES_PASSWORD=mypassword: 設定資料庫的 postgres 使用者密碼。
  • -p 5432:5432: 將本機的 5432 埠映射到容器的 5432 埠。
  • -d: 以分離模式在背景執行容器。
  • postgres:12.3-alpine: 指定使用的 Docker 映像檔,alpine 版本體積較小。

執行 docker ps 可以確認容器是否正在運行。

步驟二:資料庫初始化 - Diesel 遷移

Diesel 是 Rust 生態中最受歡迎的 ORM 之一。我們將使用其命令列工具 (CLI) 來管理資料庫結構的變更,這個過程稱為「遷移」(Migration)。

  1. 安裝 Diesel CLI:

    cargo install diesel_cli --no-default-features --features postgres
    

    注意:安裝時可能需要 PostgreSQL 的開發函式庫 (如 libpq-dev)。

  2. 設定資料庫連線 URL: 在專案根目錄建立 .env 檔案,並寫入以下內容。Diesel CLI 會自動讀取此檔案。

    DATABASE_URL=postgres://postgres:mypassword@localhost/postgres
    
  3. 初始化 Diesel:

    diesel setup
    

    此指令會根據 .env 的設定連接資料庫,並建立 migrations 資料夾。

  4. 建立遷移檔案:

    diesel migration generate create_cats
    

    這會產生 migrations/<timestamp>_create_cats/ 資料夾,內含 up.sqldown.sql

  5. 編寫 SQL:

    • up.sql 中寫入建立資料表的 SQL:
      CREATE TABLE cats (
          id SERIAL PRIMARY KEY,
          name VARCHAR NOT NULL,
          image_path VARCHAR NOT NULL
      );
      
    • down.sql 中寫入撤銷操作的 SQL:
      DROP TABLE cats;
      
  6. 執行遷移:

    diesel migration run
    

    此指令會將 up.sql 的內容應用到資料庫。至此,我們的 cats 資料表已成功建立。

步驟三:建立 Actix-web API

現在,資料庫已準備就緒,我們可以開始編寫後端 API。

1. 專案依賴

Cargo.toml 中加入必要的依賴:

[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
diesel = { version = "2.0", features = ["postgres", "r2d2"] }
dotenv = "0.15"

2. 資料模型與 Schema

Diesel CLI 在執行遷移後,會自動產生或更新 src/schema.rs 檔案,它描述了資料庫的結構。我們還需要手動建立一個 src/models.rs 來定義與資料表對應的 Rust 結構體。

// src/models.rs
use serde::Serialize;
use diesel::prelude::*;

#[derive(Queryable, Serialize)]
pub struct Cat {
    pub id: i32,
    pub name: String,
    pub image_path: String,
}

#[derive(Queryable)] 讓此結構體可以作為 Diesel 查詢的結果,#[derive(Serialize)] 則讓它可以被序列化為 JSON。

3. API 端點與主程式

src/main.rs 中,我們建立資料庫連線池,並定義一個讀取所有貓咪資料的 API 端點。

use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use diesel::prelude::*;
use diesel::r2d2::{self, ConnectionManager};

// ... 引入 models 和 schema ...

pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;

// GET /api/cats 的處理函式
async fn get_cats(pool: web::Data<DbPool>) -> impl Responder {
    let mut conn = pool.get().expect("無法從連線池取得連線");

    // 使用 web::block 避免在 Actix 的執行緒中執行阻塞的資料庫操作
    let cats = web::block(move || cats::table.load::<Cat>(&mut conn))
        .await
        .map_err(|e| {
            eprintln!("{}", e);
            HttpResponse::InternalServerError().finish()
        });
    
    match cats {
        Ok(Ok(cat_data)) => HttpResponse::Ok().json(cat_data),
        _ => HttpResponse::InternalServerError().finish(),
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenv::dotenv().ok();
    let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL 未設定");
    
    let manager = ConnectionManager::<PgConnection>::new(database_url);
    let pool = r2d2::Pool::builder()
        .build(manager)
        .expect("建立資料庫連線池失敗");

    println!("正在監聽 http://127.0.0.1:8080");
    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(pool.clone()))
            .service(
                web::scope("/api")
                    .route("/cats", web::get().to(get_cats))
            )
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

步驟四:前端整合與測試

為了驗證我們的 API,可以建立一個簡單的 static/index.html 頁面來呼叫它。

<!DOCTYPE html>
<html>
<head>
    <title>Catdex</title>
</head>
<body>
    <h1>Catdex</h1>
    <section id="cats"><p>正在載入貓咪資料...</p></section>
    <script>
      document.addEventListener("DOMContentLoaded", () => {
        fetch("/api/cats")
          .then((response) => response.json())
          .then((cats) => {
            const catsSection = document.getElementById("cats");
            catsSection.innerHTML = ""; // 清空載入提示
            for (const cat of cats) {
              const catElement = document.createElement("article");
              catElement.innerHTML = `<h3>${cat.name}</h3><img src="${cat.image_path}" width="200">`;
              catsSection.appendChild(catElement);
            }
          });
      });
    </script>
</body>
</html>

同時,也可以使用 curl 來直接測試 API 端點:

# 假設資料庫中已有資料
curl http://localhost:8080/api/cats

透過以上四個步驟,我們成功地建立了一個由資料庫驅動的 REST API。這個架構不僅穩健,也為未來擴展更複雜的功能(如新增、修改、刪除資料)奠定了堅實的基礎。