要將一個網站從靜態內容展示提升為具備完整互動能力的 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)。
安裝 Diesel CLI:
cargo install diesel_cli --no-default-features --features postgres注意:安裝時可能需要 PostgreSQL 的開發函式庫 (如
libpq-dev)。設定資料庫連線 URL: 在專案根目錄建立
.env檔案,並寫入以下內容。Diesel CLI 會自動讀取此檔案。DATABASE_URL=postgres://postgres:mypassword@localhost/postgres初始化 Diesel:
diesel setup此指令會根據
.env的設定連接資料庫,並建立migrations資料夾。建立遷移檔案:
diesel migration generate create_cats這會產生
migrations/<timestamp>_create_cats/資料夾,內含up.sql和down.sql。編寫 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;
- 在
執行遷移:
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。這個架構不僅穩健,也為未來擴展更複雜的功能(如新增、修改、刪除資料)奠定了堅實的基礎。