Rust + Rocket + Diesel构建的RESTful API示例(CRUD)

技术栈

  • Web框架: Rocket 0.5.0
  • ORM: Diesel 2.1.0
  • 数据库: MySQL/MariaDB
  • 序列化/反序列化: Serde
  • 时间处理: Chrono
  • 错误处理: thiserror
  • 日志: log/env_logger

项目结构

bash 复制代码
rocket-demo/
├── src/
│   ├── main.rs              # 项目入口点
│   ├── controllers/         # API控制器
│   │   └── user_controller.rs
│   ├── models/              # 数据模型
│   │   └── user.rs
│   ├── services/            # 业务逻辑层
│   │   └── user_service.rs
│   ├── db/                  # 数据库连接
│   │   └── connection.rs
│   ├── utils/               # 工具函数
│   │   └── error.rs
│   └── schema.rs            # 数据库模式
├── migrations/              # 数据库迁移
│   └── 2025-12-18-124009-0000_create_users/
│       ├── up.sql
│       └── down.sql
├── Cargo.toml               # 项目配置
├── Rocket.toml              # Rocket配置
├── diesel.toml              # Diesel配置
└── .env                     # 环境变量

Cargo.toml

bash 复制代码
[package]
name = "rocket-demo"
version = "0.1.0"
edition = "2021"

[dependencies]
# Rocket Web框架 - 提供HTTP服务器、路由、请求处理等核心功能
# features = ["json"] 启用JSON支持,用于处理JSON格式的请求和响应
rocket = { version = "0.5.0", features = ["json"] }

# Rocket数据库连接池 - 提供数据库连接管理功能
# features = ["diesel_mysql_pool"] 启用Diesel ORM的MySQL连接池支持
rocket_sync_db_pools = { version = "0.1.0", features = ["diesel_mysql_pool"] }

# Diesel ORM框架 - Rust的ORM工具,提供类型安全的数据库操作
# features = ["mysql", "chrono"] 启用MySQL数据库支持和时间戳功能
diesel = { version = "2.1.0", features = ["mysql", "chrono"] }

# Diesel迁移工具 - 管理数据库结构变更(创建表、添加字段等)
diesel_migrations = "2.1.0"

# Serde序列化库 - Rust最流行的序列化/反序列化框架
# features = ["derive"] 启用派生宏,可以通过#[derive(Serialize, Deserialize)]自动生成代码
serde = { version = "1.0", features = ["derive"] }

# Serde JSON支持 - 提供JSON格式的序列化和反序列化功能
serde_json = "1.0"

# Dotenv环境变量管理 - 从.env文件加载环境变量到程序中
# 常用于存储数据库连接字符串、密钥等敏感信息
dotenv = "0.15.0"

# Chrono时间日期库 - 处理日期和时间
# features = ["serde"] 启用与Serde的集成,支持时间的序列化/反序列化
chrono = { version = "0.4", features = ["serde"] }

# ThisError错误处理库 - 简化自定义错误类型的创建
# 可以通过#[derive(Error)]宏快速定义错误类型
thiserror = "2.0.17"

# Log日志接口 - Rust标准日志接口,提供统一的日志API
# 需要配合具体的日志实现(如env_logger)使用
log = "0.4"

# Env Logger日志实现 - 基于环境变量的日志输出实现
# 可以通过RUST_LOG环境变量控制日志级别和输出格式
env_logger = "0.11.8"

依赖关系说明:

1、Web框架层:rocket 提供Web服务基础

2、数据库层:diesel + rocket_sync_db_pools 提供ORM和连接池

3、数据处理层:serde + serde_json 处理数据序列化

4、工具层:dotenv 管理配置,chrono 处理时间,thiserror 处理错误

5、日志层:log + env_logger 提供日志功能

.env

rust 复制代码
DATABASE_URL=mysql://rust:rust@mariadb:3306/rust_demo
ROCKET_DATABASES={mysql_db={url="mysql://rust:rust@mariadb:3306/rust_demo"}}

Rocket.toml

rust 复制代码
[default]
address = "0.0.0.0"
port = 8000
log_level = "debug"

[default.databases]
mysql_db = { url = "mysql://rust:rust@mariadb:3306/rust_demo" }

diesel.toml

rust 复制代码
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli

[print_schema]
file = "src/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]

[migrations_directory]
dir = "migrations"

这段内容是 Diesel CLI 的配置文件 diesel.toml,它告诉 diesel 命令行工具如何为你生成/管理数据库相关代码。

文件位置与作用

  • 文件名:diesel.toml(放在项目根目录)
  • 谁用它:diesel migration generate/run/revert、diesel print-schema 等子命令。
  • 目的:把数据库 schema 同步到 Rust 代码,并统一迁移脚本存放路径。

src/schema.rs

rust 复制代码
// @generated automatically by Diesel CLI.

diesel::table! {
    users (id) {
        id -> Integer,
        #[max_length = 255]
        username -> Varchar,
        #[max_length = 255]
        email -> Varchar,
        #[max_length = 255]
        full_name -> Nullable<Varchar>,
        age -> Nullable<Integer>,
        created_at -> Datetime,
        updated_at -> Datetime,
    }
}

这段代码是 Diesel CLI 自动生成的"表结构说明书",位于 src/schema.rs。

它把数据库里真实的 users 表"翻译"成 Rust 的类型系统,让编译器能在编译期就检查出 SQL 拼写错误、类型不匹配、空值处理等问题。

src/db/connection.rs

rust 复制代码
use rocket_sync_db_pools::diesel;

#[database("mysql_db")]
pub struct DbConn(diesel::MysqlConnection);

🔧 代码逐行解释

1️⃣ use rocket_sync_db_pools::diesel;

  • 引入 rocket_sync_db_pools crate 提供的 Diesel 异步连接池 支持。
  • rocket_sync_db_pools 是 Rocket 官方维护的库,用于将同步的 Diesel 数据库操作包装成 异步、线程安全 的接口。

2️⃣ #[database("mysql_db")]

  • Rocket 的属性宏,告诉 Rocket:
    1、从 Rocket.toml 或环境变量中读取名为 "mysql_db" 的数据库配置。
    2、自动生成连接池、连接获取、连接回收等样板代码。
    3、为 DbConn 实现 FromRequest,使其能直接作为处理器函数参数使用(如 fn foo(conn: DbConn))。

3️⃣ pub struct DbConn(diesel::MysqlConnection);

  • 新类型模式(Newtype Pattern):
    1、封装 diesel::MysqlConnection,形成你自己的连接类型 DbConn。
    2、避免与其他库的连接类型冲突,同时利用 Rust 类型系统做区分。
  • 虽然看起来是"裸"连接,但 宏会自动将其包装成连接池,实际运行时:
    1、每次请求从池中取一个连接。
    2、请求结束后自动归还。

src/db/mod.rs

rust 复制代码
pub mod connection;

声明一个名为 connection 的公共子模块。

src/utils/error.rs

1. 依赖与工具

rust 复制代码
use rocket::http::Status;
use rocket::response::{self, Responder, Response};
use rocket::serde::json::Json;
use serde::Serialize;
use thiserror::Error;
作用
Status Rocket 内置的 HTTP 状态码枚举。
Responder Rocket 的"可响应" trait;实现后就能从处理器直接返回。
Json<T> Rocket 的 JSON 封装,自动序列化并设置 Content-Type: application/json
thiserror::Error 宏,一键生成 Error + Display + From 等样板代码。

2. 业务错误枚举

rust 复制代码
#[derive(Error, Debug)]
pub enum ApiError {
    #[error("Database error: {0}")]
    DatabaseError(String),
    
    #[error("Not found")]
    NotFound,
    
    #[error("Bad request: {0}")]
    BadRequest(String),
    
    #[error("Internal server error")]
    InternalServerError,
}

1️⃣ #[derive(Error)]

由 thiserror 提供,为每个变体自动生成:

  • std::error::Error trait
  • Display 实现(正好就是 #[error("...")] 里的模板)

2️⃣ 四个变体覆盖最常见的场景:

  • 数据库操作失败 → DatabaseError(msg)
  • 主键/资源找不到 → NotFound
  • 客户端传参不合规 → BadRequest(msg)
  • 意料之外的异常 → InternalServerError

3. 错误响应体

rust 复制代码
#[derive(Serialize)]
pub struct ErrorResponse {
    pub message: String,
    pub status: u16,
}

前端收到的 JSON 一定是这种形状:

rust 复制代码
{
  "message": "Bad request: email missing",
  "status": 400
}

4. 错误 → HTTP 状态码

rust 复制代码
impl ApiError {
    pub fn status_code(&self) -> Status {
        match self {
            ApiError::DatabaseError(_) => Status::InternalServerError, // 500
            ApiError::NotFound         => Status::NotFound,            // 404
            ApiError::BadRequest(_)    => Status::BadRequest,          // 400
            ApiError::InternalServerError => Status::InternalServerError,
        }
    }
}

集中维护"业务错误"与"HTTP 状态"的映射,后面 Responder 实现会直接调用。

5. 让错误本身成为"响应"

rust 复制代码
impl<'r> Responder<'r, 'static> for ApiError {
    fn respond_to(self, request: &'r rocket::Request) -> response::Result<'static> {
        // 1. 选状态码
        let status = self.status_code();

        // 2. 构造 JSON 体
        let error_response = ErrorResponse {
            message: self.to_string(),   // 利用 thiserror 生成的 Display
            status: status.code,         // u16
        };

        // 3. 把 JSON 体再包成 Rocket 响应
        Response::build_from(Json(error_response).respond_to(request)?)
            .status(status)              // 设置 HTTP 状态行
            .ok()                        // 成功则返回 Response
    }
}

关键点

  • 一旦处理器返回 Err(api_error),Rocket 会自动调用本 Responder。
  • 无需在每个路由里手动 map_err → 直接写 ? 即可,错误会"自动"变成合规的 JSON 响应。

6. 统一结果别名

rust 复制代码
pub type ApiResult<T, E = ApiError> = Result<T, E>;
  • 让路由签名更短:
    fn foo() -> ApiResult<Json<User>>
    等价于
    fn foo() -> Result<Json<User>, ApiError>
  • 如果未来需要其他错误类型,可 ApiResult<T, CustomError> 灵活切换。

7. 使用示例(对照)

路由里只需:

rust 复制代码
#[get("/<id>")]
pub async fn get_user_by_id(conn: DbConn, id: i32) -> ApiResult<Json<User>> {
    let user = UserService::get_user_by_id(&conn, id).await?; // 404 会自动透出
    Ok(Json(user))
}

找不到 → UserService 返回 Err(ApiError::NotFound) → 前端收到

rust 复制代码
HTTP/1.1 404 Not Found
{"message":"Not found","status":404}

任何其它 ApiError 同理,零额外代码。

8. 完整的error.rs

rust 复制代码
use rocket::http::Status;
use rocket::response::{self, Responder, Response};
use rocket::serde::json::Json;
use serde::Serialize;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ApiError {
    #[error("Database error: {0}")]
    DatabaseError(String),
    
    #[error("Not found")]
    NotFound,
    
    #[error("Bad request: {0}")]
    BadRequest(String),
    
    #[error("Internal server error")]
    InternalServerError,
}

#[derive(Serialize)]
pub struct ErrorResponse {
    pub message: String,
    pub status: u16,
}

impl ApiError {
    pub fn status_code(&self) -> Status {
        match self {
            ApiError::DatabaseError(_) => Status::InternalServerError,
            ApiError::NotFound => Status::NotFound,
            ApiError::BadRequest(_) => Status::BadRequest,
            ApiError::InternalServerError => Status::InternalServerError,
        }
    }
}

impl<'r> Responder<'r, 'static> for ApiError {
    fn respond_to(self, request: &'r rocket::Request) -> response::Result<'static> {
        let status = self.status_code();
        let error_response = ErrorResponse {
            message: self.to_string(),
            status: status.code,
        };
        
        Response::build_from(Json(error_response).respond_to(request)?) 
            .status(status)
            .ok()
    }
}

pub type ApiResult<T, E = ApiError> = Result<T, E>;

src/utils/mod.rs

rust 复制代码
pub mod error;

声明一个名为 error 的公共子模块。

src/models/user.rs

1. 引入依赖

rust 复制代码
use serde::{Deserialize, Serialize};
use chrono::NaiveDateTime;
use diesel::{Queryable, Insertable, AsChangeset};
use crate::schema::users;

作用说明:
serde: 用于数据的序列化和反序列化(JSON转换)
chrono::NaiveDateTime: 处理不带时区信息的日期时间
diesel: ORM框架,提供数据库操作能力
crate::schema::users: 引入Diesel自动生成的users表结构

2. User结构体 - 查询用

rust 复制代码
#[derive(Queryable, Serialize, Deserialize, Debug, Clone)]
pub struct User {
    pub id: i32,
    pub username: String,
    pub email: String,
    pub full_name: Option<String>,
    pub age: Option<i32>,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
}

详细解释:
#[derive(Queryable)]: Diesel的trait,表示这个结构体可以从数据库查询结果构建
#[derive(Serialize, Deserialize)]: Serde的trait,支持JSON序列化/反序列化
#[derive(Debug, Clone)]: 标准trait,支持调试打印和克隆
Option<T>: 表示字段可以为NULL,是Rust处理空值的安全方式
NaiveDateTime: 存储不带时区的日期时间(适合数据库)

用途: 从数据库查询用户数据时使用,包含所有字段。

3. NewUser结构体 - 插入用

rust 复制代码
#[derive(Insertable, Serialize, Deserialize, Debug)]
#[diesel(table_name = users)]
pub struct NewUser {
    pub username: String,
    pub email: String,
    pub full_name: Option<String>,
    pub age: Option<i32>,
}

详细解释:
#[derive(Insertable)]: Diesel的trait,表示这个结构体可以插入到数据库
#[diesel(table_name = users)]: 指定对应的数据库表名

注意: 没有id, created_at, updated_at字段,因为这些通常由数据库自动生成

用途: 创建新用户时使用,只包含用户可提供的字段。

4. UpdateUser结构体 - 更新用

rust 复制代码
#[derive(AsChangeset, Serialize, Deserialize, Debug, Default)]
#[diesel(table_name = users)]
pub struct UpdateUser {
    pub username: Option<String>,
    pub email: Option<String>,
    pub full_name: Option<String>,
    pub age: Option<i32>,
}

详细解释:
#[derive(AsChangeset)]: Diesel的trait,表示这个结构体可以用于更新操作
#[derive(Default)]: 提供默认值,方便构建部分更新

所有字段都是Option: 允许部分更新,只设置需要修改的字段

用途: 更新用户信息时使用,可以只更新部分字段。

5. NewUser的实现块

rust 复制代码
impl NewUser {
    pub fn new(username: String, email: String, full_name: Option<String>, age: Option<i32>) -> Self {
        Self {
            username,
            email,
            full_name,
            age,
        }
    }
}

作用: 提供一个便捷的构造函数,简化NewUser实例的创建。

6. 完整的user.rs

rust 复制代码
use serde::{Deserialize, Serialize};
use chrono::NaiveDateTime;
use diesel::{Queryable, Insertable, AsChangeset};
use crate::schema::users;

#[derive(Queryable, Serialize, Deserialize, Debug, Clone)]
pub struct User {
    pub id: i32,
    pub username: String,
    pub email: String,
    pub full_name: Option<String>,
    pub age: Option<i32>,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
}

#[derive(Insertable, Serialize, Deserialize, Debug)]
#[diesel(table_name = users)]
pub struct NewUser {
    pub username: String,
    pub email: String,
    pub full_name: Option<String>,
    pub age: Option<i32>,
}

#[derive(AsChangeset, Serialize, Deserialize, Debug, Default)]
#[diesel(table_name = users)]
pub struct UpdateUser {
    pub username: Option<String>,
    pub email: Option<String>,
    pub full_name: Option<String>,
    pub age: Option<i32>,
}

impl NewUser {
    pub fn new(username: String, email: String, full_name: Option<String>, age: Option<i32>) -> Self {
        Self {
            username,
            email,
            full_name,
            age,
        }
    }
}

src/models/mod.rs

rust 复制代码
pub mod user;

这行代码的意思是:声明一个名为 user 的公共子模块。

详细说明:

1、mod user - 声明模块

  • 告诉Rust编译器存在一个名为 user 的模块
  • 对应的代码应该在 user.rs 文件或 user/mod.rs 文件中

2、pub - 公共可见性

  • 让这个模块对外部代码可见
  • 其他模块可以通过 use crate::models::user; 来使用这个模块

src/services/user_service.rs

1. 引入依赖

rust 复制代码
use diesel::prelude::*;
use diesel::result::Error as DieselError;
use crate::db::connection::DbConn;
use crate::models::user::{User, NewUser, UpdateUser};
use crate::schema::users;
use crate::utils::error::{ApiError, ApiResult};
模块 作用
diesel::prelude::* 引入 Diesel 的常用类型和宏
DieselError Diesel 的数据库错误类型
DbConn 项目中封装的 Rocket 数据库连接池类型
User, NewUser, UpdateUser 用户模型(查询/插入/更新)
users Diesel 自动生成的数据库表结构
ApiError, ApiResult<T> 自定义的错误类型和统一返回类型

2. 定义服务结构体

rust 复制代码
pub struct UserService;

这是一个 零大小结构体(ZST),没有字段,只用于组织方法,类似 Java 的静态工具类。

3. 创建用户:create_user

rust 复制代码
pub async fn create_user(conn: &DbConn, new_user: NewUser) -> ApiResult<User> {
    conn.run(move |c| {
        diesel::insert_into(users::table)
            .values(&new_user)
            .execute(c)
            .map_err(|e| ApiError::DatabaseError(e.to_string()))?;

        users::table
            .order(users::id.desc())
            .first(c)
            .map_err(|e| ApiError::DatabaseError(e.to_string()))
    }).await
}
步骤 说明
insert_into(...).values(...) 插入新用户
.execute(c) 执行插入语句
.order(...).first(c) 获取刚插入的用户(通过最新ID)
map_err(...) 将 Diesel 错误转换为你自定义的 ApiError

4. 获取所有用户:get_all_users

rust 复制代码
pub async fn get_all_users(conn: &DbConn) -> ApiResult<Vec<User>> {
    conn.run(|c| {
        users::table
            .load::<User>(c)
            .map_err(|e| ApiError::DatabaseError(e.to_string()))
    }).await
}

load::<User>(c):将查询结果加载为 Vec<User>

5. 根据 ID 获取用户:get_user_by_id

rust 复制代码
pub async fn get_user_by_id(conn: &DbConn, user_id: i32) -> ApiResult<User> {
    conn.run(move |c| {
        users::table
            .find(user_id)
            .first(c)
            .map_err(|e| match e {
                DieselError::NotFound => ApiError::NotFound,
                _ => ApiError::DatabaseError(e.to_string()),
            })
    }).await
}
  • .find(user_id):主键查找。
  • 明确区分了"找不到"和"数据库错误"两种情况。

6. 更新用户:update_user

rust 复制代码
pub async fn update_user(conn: &DbConn, user_id: i32, update_data: UpdateUser) -> ApiResult<User> {
    conn.run(move |c| {
        if user_id == 999 {
            return Err(ApiError::InternalServerError);
        }

        diesel::update(users::table.find(user_id))
            .set(&update_data)
            .execute(c)
            .map_err(|e| match e {
                DieselError::NotFound => ApiError::NotFound,
                _ => ApiError::DatabaseError(e.to_string()),
            })?;

        users::table
            .find(user_id)
            .first(c)
            .map_err(|e| ApiError::DatabaseError(e.to_string()))
    }).await
}
亮点 说明
user_id == 999 模拟特殊错误场景,用于测试前端错误处理
.set(&update_data) 只更新非 None 的字段(UpdateUser 的设计)
返回更新后的完整用户数据 符合 RESTful 更新接口规范

7. 删除用户:delete_user

rust 复制代码
pub async fn delete_user(conn: &DbConn, user_id: i32) -> ApiResult<()> {
    conn.run(move |c| {
        diesel::delete(users::table.find(user_id))
            .execute(c)
            .map_err(|e| match e {
                DieselError::NotFound => ApiError::NotFound,
                _ => ApiError::DatabaseError(e.to_string()),
            })?;

        Ok(())
    }).await
}

返回 ApiResult<()> 表示"成功就是没错误,不需要返回数据"。

8. 根据用户名查找用户:get_user_by_username

rust 复制代码
pub async fn get_user_by_username(conn: &DbConn, username: &str) -> ApiResult<User> {
    let username = username.to_string();
    conn.run(move |c| {
        users::table
            .filter(users::username.eq(username))
            .first(c)
            .map_err(|e| match e {
                DieselError::NotFound => ApiError::NotFound,
                _ => ApiError::DatabaseError(e.to_string()),
            })
    }).await
}

.filter(...):条件查询。

注意:username.to_string() 是为了解决 &str 的生命周期问题,确保 move 闭包能拥有数据。

9. 总结:设计亮点

设计点 说明
异步支持 使用 conn.run(...).await 包装同步 Diesel 操作
统一错误处理 所有数据库错误都转换为 ApiError
清晰的职责分离 服务层只负责数据库操作,不涉及 HTTP 或路由
类型安全 利用 Diesel 的类型系统避免 SQL 注入和运行时错误
可测试性 每个方法都是纯函数式,便于单元测试

10. 完整的user_service.rs

rust 复制代码
use diesel::prelude::*;
use diesel::result::Error as DieselError;
use crate::db::connection::DbConn;
use crate::models::user::{User, NewUser, UpdateUser};
use crate::schema::users;
use crate::utils::error::{ApiError, ApiResult};

pub struct UserService;

impl UserService {
    pub async fn create_user(conn: &DbConn, new_user: NewUser) -> ApiResult<User> {
        conn.run(move |c| {
            diesel::insert_into(users::table)
                .values(&new_user)
                .execute(c)
                .map_err(|e| ApiError::DatabaseError(e.to_string()))?;
            
            users::table
                .order(users::id.desc())
                .first(c)
                .map_err(|e| ApiError::DatabaseError(e.to_string()))
        }).await
    }

    pub async fn get_all_users(conn: &DbConn) -> ApiResult<Vec<User>> {
        conn.run(|c| {
            users::table
                .load::<User>(c)
                .map_err(|e| ApiError::DatabaseError(e.to_string()))
        }).await
    }

    pub async fn get_user_by_id(conn: &DbConn, user_id: i32) -> ApiResult<User> {
        conn.run(move |c| {
            users::table
                .find(user_id)
                .first(c)
                .map_err(|e| match e {
                    DieselError::NotFound => ApiError::NotFound,
                    _ => ApiError::DatabaseError(e.to_string()),
                })
        }).await
    }

    pub async fn update_user(conn: &DbConn, user_id: i32, update_data: UpdateUser) -> ApiResult<User> {
        conn.run(move |c| {
            // 模拟特殊情况,当user_id为999时返回内部服务器错误
            if user_id == 999 {
                return Err(ApiError::InternalServerError);
            }
            
            diesel::update(users::table.find(user_id))
                .set(&update_data)
                .execute(c)
                .map_err(|e| match e {
                    DieselError::NotFound => ApiError::NotFound,
                    _ => ApiError::DatabaseError(e.to_string()),
                })?;
            
            users::table
                .find(user_id)
                .first(c)
                .map_err(|e| ApiError::DatabaseError(e.to_string()))
        }).await
    }

    pub async fn delete_user(conn: &DbConn, user_id: i32) -> ApiResult<()> {
        conn.run(move |c| {
            diesel::delete(users::table.find(user_id))
                .execute(c)
                .map_err(|e| match e {
                    DieselError::NotFound => ApiError::NotFound,
                    _ => ApiError::DatabaseError(e.to_string()),
                })?;
            
            Ok(())
        }).await
    }

    pub async fn get_user_by_username(conn: &DbConn, username: &str) -> ApiResult<User> {
        let username = username.to_string();
        conn.run(move |c| {
            users::table
                .filter(users::username.eq(username))
                .first(c)
                .map_err(|e| match e {
                    DieselError::NotFound => ApiError::NotFound,
                    _ => ApiError::DatabaseError(e.to_string()),
                })
        }).await
    }
}

src/services/mod.rs

rust 复制代码
pub mod user_service;

这行代码的意思是:声明一个名为 user_service 的公共子模块。

src/controllers/user_controller.rs

1. 引入依赖

rust 复制代码
use rocket::{get, post, put, delete};
use rocket::serde::json::Json;
use rocket::response::status;
use crate::db::connection::DbConn;
use crate::models::user::{User, NewUser, UpdateUser};
use crate::services::user_service::UserService;
use crate::utils::error::{ApiResult, ApiError};
模块 作用
get, post, put, delete Rocket 的宏,用于定义 HTTP 路由
Json<T> Rocket 的 JSON 封装类型,自动序列化/反序列化
status::NoContent HTTP 204 无内容响应
DbConn 数据库连接池封装
UserService 服务层,处理业务逻辑
ApiResult<T>, ApiError 统一的错误处理和返回类型

2. 创建用户:POST /

rust 复制代码
#[post("/", data = "<new_user>")]
pub async fn create_user(
    conn: DbConn,
    new_user: Json<NewUser>
) -> ApiResult<Json<User>> {
    let user_data = new_user.into_inner();

    if user_data.username.is_empty() {
        return Err(ApiError::BadRequest("Username cannot be empty".to_string()));
    }

    if user_data.email.is_empty() {
        return Err(ApiError::BadRequest("Email cannot be empty".to_string()));
    }

    let user = UserService::create_user(&conn, user_data).await?;
    Ok(Json(user))
}
要点 说明
#[post("/", data = "<new_user>")] 定义 POST 路由,路径为 /users(假设挂载在 /users
Json<NewUser> 自动将请求体 JSON 反序列化为 NewUser
into_inner() 取出内部的 NewUser 数据
输入校验 检查用户名和邮箱是否为空
? 自动将 Err 转换为 ApiResult 的错误类型

3. 获取所有用户:GET /

rust 复制代码
#[get("/")]
pub async fn get_all_users(conn: DbConn) -> ApiResult<Json<Vec<User>>> {
    let users = UserService::get_all_users(&conn).await?;
    Ok(Json(users))
}
  • 返回 Vec<User> 的 JSON 数组。
  • 路径为 /users(假设挂载在 /users)。

4. 根据 ID 获取用户:GET /<id>

rust 复制代码
#[get("/<id>")]
pub async fn get_user_by_id(conn: DbConn, id: i32) -> ApiResult<Json<User>> {
    let user = UserService::get_user_by_id(&conn, id).await?;
    Ok(Json(user))
}
  • 路径示例:/users/123
  • 自动将 URL 中的<id>解析为i32类型。

5. 更新用户:PUT /<id>

rust 复制代码
#[put("/<id>", data = "<update_data>")]
pub async fn update_user(
    conn: DbConn,
    id: i32,
    update_data: Json<UpdateUser>
) -> ApiResult<Json<User>> {
    let user = UserService::update_user(&conn, id, update_data.into_inner()).await?;
    Ok(Json(user))
}
  • 路径示例:/users/123
  • 请求体为 UpdateUser 的 JSON,允许部分更新。

6. 删除用户:DELETE /<id>

rust 复制代码
#[delete("/<id>")]
pub async fn delete_user(conn: DbConn, id: i32) -> ApiResult<status::NoContent> {
    UserService::delete_user(&conn, id).await?;
    Ok(status::NoContent)
}
  • 返回 204 No Content,表示删除成功但无返回体。
  • 符合 RESTful 规范。

7. 根据用户名查找用户:GET /username/<username>

rust 复制代码
#[get("/username/<username>")]
pub async fn get_user_by_username(conn: DbConn, username: String) -> ApiResult<Json<User>> {
    let user = UserService::get_user_by_username(&conn, &username).await?;
    Ok(Json(user))
}
  • 路径示例:/users/username/alice
  • 注意:username 是路径参数,不是查询参数。

8. 路由注册:routes()

rust 复制代码
pub fn routes() -> Vec<rocket::Route> {
    routes![
        create_user,
        get_all_users,
        get_user_by_id,
        get_user_by_username,
        update_user,
        delete_user
    ]
}
作用 说明
返回所有路由处理器 用于在 main.rs 中统一挂载
挂载示例 rocket::build().mount("/users", routes())

9. RESTful 接口总结

方法 路径 功能
POST /users 创建用户
GET /users 获取所有用户
GET /users/123 获取 ID 为 123 的用户
PUT /users/123 更新 ID 为 123 的用户
DELETE /users/123 删除 ID 为 123 的用户
GET /users/username/alice 获取用户名为 alice 的用户

10. 设计亮点

设计点 说明
类型安全 使用 Json<T> 自动验证请求体格式
统一错误处理 所有错误都通过 ApiResult<T> 返回
清晰的职责分离 控制器只处理 HTTP 层逻辑,业务逻辑交给服务层
RESTful 风格 符合标准的 HTTP 语义和状态码
可测试性 每个处理器都是纯函数,便于单元测试

11. 完整的user_controller.rs

rust 复制代码
use rocket::{get, post, put, delete};
use rocket::serde::json::Json;
use rocket::response::status;
use crate::db::connection::DbConn;
use crate::models::user::{User, NewUser, UpdateUser};
use crate::services::user_service::UserService;
use crate::utils::error::{ApiResult, ApiError};

#[post("/", data = "<new_user>")]
pub async fn create_user(
    conn: DbConn,
    new_user: Json<NewUser>
) -> ApiResult<Json<User>> {
    let user_data = new_user.into_inner();
    
    // Input validation
    if user_data.username.is_empty() {
        return Err(ApiError::BadRequest("Username cannot be empty".to_string()));
    }
    
    if user_data.email.is_empty() {
        return Err(ApiError::BadRequest("Email cannot be empty".to_string()));
    }
    
    let user = UserService::create_user(&conn, user_data).await?;
    Ok(Json(user))
}

#[get("/")]
pub async fn get_all_users(conn: DbConn) -> ApiResult<Json<Vec<User>>> {
    let users = UserService::get_all_users(&conn).await?;
    Ok(Json(users))
}

#[get("/<id>")]
pub async fn get_user_by_id(conn: DbConn, id: i32) -> ApiResult<Json<User>> {
    let user = UserService::get_user_by_id(&conn, id).await?;
    Ok(Json(user))
}

#[put("/<id>", data = "<update_data>")]
pub async fn update_user(
    conn: DbConn,
    id: i32,
    update_data: Json<UpdateUser>
) -> ApiResult<Json<User>> {
    let user = UserService::update_user(&conn, id, update_data.into_inner()).await?;
    Ok(Json(user))
}

#[delete("/<id>")]
pub async fn delete_user(conn: DbConn, id: i32) -> ApiResult<status::NoContent> {
    UserService::delete_user(&conn, id).await?;
    Ok(status::NoContent)
}

#[get("/username/<username>")]
pub async fn get_user_by_username(conn: DbConn, username: String) -> ApiResult<Json<User>> {
    let user = UserService::get_user_by_username(&conn, &username).await?;
    Ok(Json(user))
}

pub fn routes() -> Vec<rocket::Route> {
    routes![
        create_user,
        get_all_users,
        get_user_by_id,
        get_user_by_username,
        update_user,
        delete_user
    ]
}

src/main.rs

1. 前置宏导入

rust 复制代码
#[macro_use] extern crate rocket;
#[macro_use] extern crate rocket_sync_db_pools;

Rust 2018 版以后通常不再需要 extern crate,但 rocket 的宏(#[get]#[post] 等)和 rocket_sync_db_pools 的宏(database!)目前仍以"传统宏"方式提供,因此用 #[macro_use] 一次性全局导入,后面所有模块才能直接使用这些宏而不再 use。

2. 模块声明

rust 复制代码
mod db;
mod controllers;
mod models;
mod services;
mod utils;
mod schema;

把项目拆成 标准分层:

  • db → 数据库连接池定义
  • models → 实体 / Diesel 模型
  • services → 业务逻辑(UserService 等)
  • controllers → 路由 / 处理器函数
  • utils → 公共工具(错误处理、日志等)
  • schema → Diesel 自动生成的表结构
    声明后编译器会把同名文件夹或文件(db/mod.rs、controllers/mod.rs ...)编译进二进制。

3. 导入路由

rust 复制代码
use crate::controllers::user_controller::routes as user_routes;
  • 前面声明了 mod controllers;,其 mod.rs 里又 pub mod user_controller;
  • 这里把 user_controller::routes() 函数(返回 Vec<Route>)重命名为 user_routes,后面直接挂载。

4. 数据库连接池 fairing

rust 复制代码
use crate::db::connection::DbConn;
  • DbConn 就是之前 #[database("mysql_db")] 生成的连接池包装类型。
  • 通过 ·DbConn::fairing() ·把它注册为 Rocket fairing(生命周期钩子),完成:
    1、启动时读取配置 → 创建连接池
    2、请求时自动注入 DbConn 参数
    3、关机时优雅关闭连接

5. 火箭发射场

rust 复制代码
#[launch]
fn rocket() -> _ {
    rocket::build()
        .attach(DbConn::fairing())
        .mount("/api/users", user_routes())
}
说明
#[launch] Rocket 提供的属性宏,自动生成 fn main() 并调用 rocket().launch()省去手写 main
rocket::build() 构造 Rocket<Build> 实例,进入"配置阶段"。
.attach(DbConn::fairing()) 把数据库连接池挂到 Rocket 生命周期上,后续处理器可直接把 DbConn 当参数。
.mount("/api/users", user_routes()) user_routes() 返回的所有路由统一加前缀 /api/users,最终生成: - POST /api/users - GET /api/users - GET /api/users/123 - PUT /api/users/123 - DELETE /api/users/123 - GET /api/users/username/alice
返回 _ 让编译器推断最终类型 Rocket<Ignite>,无需手写冗长签名。

6. 启动后发生了什么

1、Rocket 读取 Rocket.toml(或环境变量)找到 mysql_db 配置 → 建立连接池。

2、路由表注册完成,等待请求。

3、第一个请求进来时,Rocket 从池中拿一个连接,以 DbConn 形式注入处理器;请求结束自动归还。

4、进程收到 SIGTERM 时 fairing 先关闭连接池,再退出。

7. 完整的main.rs

rust 复制代码
#[macro_use] extern crate rocket;
#[macro_use] extern crate rocket_sync_db_pools;

mod db;
mod controllers;
mod models;
mod services;
mod utils;
mod schema;

use crate::controllers::user_controller::routes as user_routes;
use crate::db::connection::DbConn;

#[launch]
fn rocket() -> _ {
    rocket::build()
        .attach(DbConn::fairing())
        .mount("/api/users", user_routes())
}
相关推荐
无敌最俊朗@2 小时前
STL-关联容器(面试复习4)
开发语言·c++
bybitq2 小时前
string,byte,rune,character?详解Golang编码-UTF-8
开发语言·后端·golang
wjs20242 小时前
HTML 框架:构建网页结构的基础
开发语言
无限进步_2 小时前
【C语言】栈(Stack)数据结构的实现与应用
c语言·开发语言·数据结构·c++·后端·visual studio
embrace992 小时前
【C语言学习】预处理详解
java·c语言·开发语言·数据结构·c++·学习·算法
浅尝辄止;2 小时前
C# 优雅实现 HttpClient 封装(可直接复用的工具类)
开发语言·c#
林太白2 小时前
Rust01-认识安装
开发语言·后端·rust
龙山云仓2 小时前
No095:沈括&AI:智能的科学研究与系统思维
开发语言·人工智能·python·机器学习·重构
IoT智慧学堂2 小时前
C语言循环结构综合应用篇(详细案例讲解)
c语言·开发语言