Axum + SQLx 实战:把内存服务变成真正的持久化后端

举报
yd_232564628 发表于 2026/10/22 11:30:45 2026/10/22
【摘要】 上一篇我们用 Axum 写了一个简单的 HTTP 服务,但数据还放在内存里。只要服务重启,所有数据都会消失。真实后端不能这样,所以这一篇我们把数据落进数据库,用 Axum + SQLx 搭一个可持久化的 Rust 后端骨架。 为什么选 SQLx?SQLx 是 Rust 生态里非常流行的异步 SQL 工具库,支持 PostgreSQL、MySQL、SQLite 等数据库。 它最大的特点是:可以...

上一篇我们用 Axum 写了一个简单的 HTTP 服务,但数据还放在内存里。只要服务重启,所有数据都会消失。真实后端不能这样,所以这一篇我们把数据落进数据库,用 Axum + SQLx 搭一个可持久化的 Rust 后端骨架。

为什么选 SQLx?

SQLx 是 Rust 生态里非常流行的异步 SQL 工具库,支持 PostgreSQL、MySQL、SQLite 等数据库。 它最大的特点是:可以写原生 SQL,又能获得编译期检查。

比如你写:

sqlx::query_as!(
    User,
    "SELECT id, title, completed FROM todos WHERE id = $1",
    id
)
.fetch_one(&pool)
.await?;

SQLx 会在编译阶段检查这条 SQL 是否合法、返回列是否匹配 User 结构体。如果表结构改了,或者字段名写错了,很多错误会在编译期暴露,而不是等到线上请求失败。

第一步:准备数据库

本地开发最方便的方式是用 Docker 启动一个 PostgreSQL:

docker run --name pg -e POSTGRES_PASSWORD=123456 -p 5432:5432 -d postgres

然后创建数据库:

createdb -h localhost -U postgres todo_db

在项目根目录创建 .env 文件:

DATABASE_URL=postgres://postgres:123456@localhost:5432/todo_db

第二步:初始化项目和迁移

创建项目:

cargo new todo_service
cd todo_service
cargo add axum tokio --features tokio/full
cargo add sqlx --features runtime-tokio-rustls,postgres,macros,migrate,chrono
cargo add serde serde_json dotenvy thiserror anyhow

安装 SQLx CLI:

cargo install sqlx-cli --no-default-features --features postgres

创建迁移文件:

sqlx migrate add create_todos_table

在生成的迁移文件里写表结构:

CREATE TABLE IF NOT EXISTS todos (
    id SERIAL PRIMARY KEY,
    title TEXT NOT NULL,
    completed BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

运行迁移:

sqlx migrate run

这样数据库表结构就被版本化管理起来了,后续改表结构也只需要新增迁移文件。

第三步:连接池与应用状态

真实服务不会每次请求都新建连接,而是使用连接池。SQLx 的 PgPool 已经内置了连接池能力。

use axum::Router;
use sqlx::PgPool;
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    db: Arc<PgPool>,
}

async fn init_pool(database_url: &str) -> PgPool {
    PgPool::connect(database_url)
        .await
        .expect("failed to connect to database")
}

启动时读取环境变量,创建连接池,再注入到 Axum 路由中:

#[tokio::main]
async fn main() {
    dotenvy::dotenv().ok();

    let database_url = std::env::var("DATABASE_URL")
        .expect("DATABASE_URL must be set");

    let pool = Arc::new(init_pool(&database_url).await);
    let state = AppState { db: pool };

    let app = Router::new()
        .route("/todos", axum::routing::get(list_todos))
        .route("/todos", axum::routing::post(create_todo))
        .with_state(state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();

    axum::serve(listener, app).await.unwrap();
}

第四步:定义数据模型

use serde::{Deserialize, Serialize};
use sqlx::FromRow;

#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Todo {
    pub id: i32,
    pub title: String,
    pub completed: bool,
}

#[derive(Debug, Deserialize)]
pub struct CreateTodo {
    pub title: String,
}

FromRow 可以让 SQLx 自动把查询结果映射到结构体。

第五步:写两个接口

查询所有待办:

use axum::{extract::State, Json};

async fn list_todos(
    State(state): State<AppState>,
) -> Result<Json<Vec<Todo>>, AppError> {
    let todos = sqlx::query_as!(
        Todo,
        "SELECT id, title, completed FROM todos ORDER BY id DESC"
    )
    .fetch_all(&*state.db)
    .await?;

    Ok(Json(todos))
}

创建待办:

async fn create_todo(
    State(state): State<AppState>,
    Json(payload): Json<CreateTodo>,
) -> Result<Json<Todo>, AppError> {
    let todo = sqlx::query_as!(
        Todo,
        "INSERT INTO todos (title) VALUES ($1) RETURNING id, title, completed",
        payload.title
    )
    .fetch_one(&*state.db)
    .await?;

    Ok(Json(todo))
}

这里用了 RETURNING,插入后直接返回新记录,避免再查一次。

第六步:统一错误处理

不要到处 unwrap()。可以定义一个简单的应用错误类型:

use axum::{http::StatusCode, response::IntoResponse};

#[derive(Debug)]
enum AppError {
    Db(sqlx::Error),
    BadRequest(String),
}

impl From<sqlx::Error> for AppError {
    fn from(e: sqlx::Error) -> Self {
        AppError::Db(e)
    }
}

impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        match self {
            AppError::Db(_) => {
                (StatusCode::INTERNAL_SERVER_ERROR, "database error").into_response()
            }
            AppError::BadRequest(msg) => {
                (StatusCode::BAD_REQUEST, msg).into_response()
            }
        }
    }
}

这样 handler 返回 Result<T, AppError> 后,Axum 会自动把错误转成合适的 HTTP 响应。

接下来可以继续扩展

到这里,一个最小可用的持久化服务已经跑通了。后续可以继续加:

  • 更新和删除接口
  • 分页查询
  • 用户表与关联查询
  • JWT 登录认证
  • Tracing 日志
  • CORS、限流、压缩等 Tower 中间件
  • 用 SeaORM 替代手写 SQL

Rust 后端开发最舒服的地方就在这里:数据库连接、错误类型、请求参数、返回结构都被类型系统串在一起。刚开始会觉得约束多,但越往后越能感受到它的稳定感。尤其是当你把服务部署到服务器上,重启几次后数据依然完好,那种“终于像个正经后端”的感觉会很实在。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。