技术栈
- 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_poolscrate 提供的 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::ErrortraitDisplay实现(正好就是#[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())
}