Rust实战:使用Axum和SQLx构建高性能RESTful API

前言

Rust是一种现代系统编程语言,专注于性能、可靠性和生产力。它通过一套丰富的类型系统和所有权模型,在编译时保证内存安全和线程安全,让开发者能够自信地构建健壮的软件。Rust的设计哲学使其在需要高性能和高并发的场景中表现出色,例如Web后端服务、嵌入式系统和游戏开发。

在现代Web开发中,构建高性能、高并发的API服务是至关重要的。Rust凭借其内存安全、卓越性能和强大的并发能力,成为构建这类服务的理想选择。本文将通过一个实战项目,带您深入了解如何使用Axum框架和SQLx库构建一个功能完备、性能卓越的RESTful API。

Axum是一个基于Tokio的模块化Web框架,以其高性能和易用性著称。SQLx则是一个异步的、类型安全的SQL客户端,能够有效防止SQL注入等安全问题。通过结合这两者,我们将构建一个待办事项(Todo)管理API,涵盖CRUD(创建、读取、更新、删除)操作,并实现错误处理、数据验证和数据库交互等核心功能。

文章目录


第一部分:项目初始化与环境配置

1.1 创建新项目

首先,我们使用Cargo创建一个新的Rust项目:

bash 复制代码
cargo new todo_api
cd todo_api

1.2 添加依赖

接下来,在Cargo.toml文件中添加项目所需的依赖:

toml 复制代码
[package]
name = "todo_api"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite"] }
dotenv = "0.15"
thiserror = "1"
anyhow = "1"
validator = { version = "0.16", features = ["derive"] }

依赖项说明:

  • axum:核心Web框架。
  • tokio:异步运行时。
  • serdeserde_json:用于JSON序列化和反序列化。
  • sqlx:异步数据库客户端,我们使用SQLite作为示例数据库。
  • dotenv:用于管理环境变量。
  • thiserroranyhow:用于优雅地处理错误。
  • validator:用于数据验证。

1.3 配置环境变量

在项目根目录下创建一个.env文件,用于存放数据库连接信息:

复制代码
DATABASE_URL=sqlite:todos.db

同时,创建一个.sqlx目录,并在其中创建一个migrations文件夹,用于存放数据库迁移文件:

bash 复制代码
md -p .sqlx/migrations

1.4 数据库迁移

我们使用sqlx-cli工具来管理数据库迁移。首先,安装sqlx-cli

bash 复制代码
cargo install sqlx-cli

然后,创建一个新的迁移文件:

bash 复制代码
sqlx migrate add create_todos_table

这会在.sqlx/migrations目录下生成一个SQL文件。编辑该文件,定义todos表的结构:

sql 复制代码
-- .sqlx/migrations/{timestamp}_create_todos_table.sql
CREATE TABLE IF NOT EXISTS todos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title VARCHAR(255) NOT NULL,
    completed BOOLEAN NOT NULL DEFAULT FALSE
);

现在,运行迁移以创建数据库和表:

bash 复制代码
sqlx migrate run

第二部分:构建核心业务逻辑

2.1 定义数据模型

src/main.rs中,我们首先定义Todo模型和用于创建、更新Todo项的数据传输对象(DTO)。

rust 复制代码
use serde::{Deserialize, Serialize};
use validator::Validate;

#[derive(Serialize, sqlx::FromRow)]
struct Todo {
    id: i64,
    title: String,
    completed: bool,
}

#[derive(Deserialize, Validate)]
struct CreateTodo {
    #[validate(length(min = 1, message = "Title is required"))]
    title: String,
}

#[derive(Deserialize, Validate)]
struct UpdateTodo {
    #[validate(length(min = 1, message = "Title is required"))]
    title: Option<String>,
    completed: Option<bool>,
}

2.2 实现数据库操作

接下来,我们创建数据库连接池,并实现与数据库交互的函数。

rust 复制代码
use sqlx::{sqlite::SqlitePool, Error as SqlxError};
use std::env;

async fn create_todo_db(pool: &SqlitePool, title: &str) -> Result<Todo, SqlxError> {
    let mut conn = pool.acquire().await?;
    let id = sqlx::query!(
        "INSERT INTO todos (title) VALUES (?)",
        title
    )
    .execute(&mut conn)
    .await?
    .last_insert_rowid();

    let todo = sqlx::query_as!(Todo, "SELECT * FROM todos WHERE id = ?", id)
        .fetch_one(&mut conn)
        .await?;
    Ok(todo)
}

async fn get_all_todos_db(pool: &SqlitePool) -> Result<Vec<Todo>, SqlxError> {
    let todos = sqlx::query_as!(Todo, "SELECT * FROM todos")
        .fetch_all(pool)
        .await?;
    Ok(todos)
}

// 其他数据库操作函数(get_todo_by_id, update_todo_db, delete_todo_db)将在后续实现

2.3 错误处理

为了提供清晰的错误信息,我们定义一个自定义的错误类型。

rust 复制代码
use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("SQLx error: {0}")]
    Sqlx(#[from] SqlxError),
    #[error("Validation error: {0}")]
    Validation(#[from] validator::ValidationErrors),
    #[error("Item not found")]
    NotFound,
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, error_message) = match self {
            AppError::Sqlx(_) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Database error".to_string(),
            ),
            AppError::Validation(err) => (
                StatusCode::BAD_REQUEST,
                format!("Validation failed: {}", err),
            ),
            AppError::NotFound => (StatusCode::NOT_FOUND, "Item not found".to_string()),
        };

        let body = Json(serde_json::json!({ "error": error_message }));
        (status, body).into_response()
    }
}

第三部分:创建API路由和处理器

3.1 创建路由

main函数中,我们设置Axum路由,并将它们与相应的处理器函数关联起来。

rust 复制代码
use axum::{
    routing::{get, post},
    Router,
};

#[tokio::main]
async fn main() {
    dotenv::dotenv().ok();
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    let pool = SqlitePool::connect(&database_url)
        .await
        .expect("Failed to create pool.");

    let app = Router::new()
        .route("/todos", get(get_all_todos).post(create_todo))
        .route("/todos/:id", get(get_todo_by_id).patch(update_todo).delete(delete_todo))
        .with_state(pool);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("Listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app).await.unwrap();
}

3.2 实现处理器函数

现在,我们为每个路由实现处理器函数。

创建Todo
rust 复制代码
use axum::{extract::State, Json};

async fn create_todo(
    State(pool): State<SqlitePool>,
    Json(payload): Json<CreateTodo>,
) -> Result<Json<Todo>, AppError> {
    payload.validate()?;
    let todo = create_todo_db(&pool, &payload.title).await?;
    Ok(Json(todo))
}
获取所有Todo
rust 复制代码
async fn get_all_todos(
    State(pool): State<SqlitePool>,
) -> Result<Json<Vec<Todo>>, AppError> {
    let todos = get_all_todos_db(&pool).await?;
    Ok(Json(todos))
}
获取、更新和删除单个Todo

这些函数的实现留给读者作为练习。您需要:

  1. 从路径中提取id
  2. 实现get_todo_by_id_dbupdate_todo_dbdelete_todo_db函数。
  3. 在处理器函数中调用这些数据库函数,并处理可能出现的NotFound错误。

程序完整代码如下

rust 复制代码
use axum::{

    extract::{Path, State},

    http::StatusCode,

    response::{IntoResponse, Response},

    routing::get,

    Json, Router,

};

use serde::{Deserialize, Serialize};

use sqlx::{sqlite::SqlitePool, Error as SqlxError};

use std::env;

use thiserror::Error;

use validator::Validate;

  

#[derive(Serialize, sqlx::FromRow)]

struct Todo {

    id: i64,

    title: String,

    completed: bool,

}

  

#[derive(Deserialize, Validate)]

struct CreateTodo {

    #[validate(length(min = 1, message = "Title is required"))]

    title: String,

}

  

#[derive(Deserialize, Validate)]

struct UpdateTodo {

    #[validate(length(min = 1, message = "Title is required"))]

    title: Option<String>,

    completed: Option<bool>,

}

  

#[derive(Error, Debug)]

enum AppError {

    #[error("SQLx error: {0}")]

    Sqlx(#[from] SqlxError),

    #[error("Validation error: {0}")]

    Validation(#[from] validator::ValidationErrors),

    #[error("Item not found")]

    NotFound,

}

  

impl IntoResponse for AppError {

    fn into_response(self) -> Response {

        let (status, error_message) = match self {

            AppError::Sqlx(_) => (

                StatusCode::INTERNAL_SERVER_ERROR,

                "Database error".to_string(),

            ),

            AppError::Validation(err) => (

                StatusCode::BAD_REQUEST,

                format!("Validation failed: {}", err),

            ),

            AppError::NotFound => (StatusCode::NOT_FOUND, "Item not found".to_string()),

        };

  

        let body = Json(serde_json::json!({ "error": error_message }));

        (status, body).into_response()

    }

}

  

async fn create_todo_db(pool: &SqlitePool, title: &str) -> Result<Todo, SqlxError> {

    let mut conn = pool.acquire().await?;

    let id = sqlx::query!(

        "INSERT INTO todos (title) VALUES (?)",

        title

    )

    .execute(&mut *conn)

    .await?

    .last_insert_rowid();

  

    let todo = sqlx::query_as!(Todo, "SELECT * FROM todos WHERE id = ?", id)

        .fetch_one(&mut *conn)

        .await?;

    Ok(todo)

}

  

async fn get_all_todos_db(pool: &SqlitePool) -> Result<Vec<Todo>, SqlxError> {

    let todos = sqlx::query_as!(Todo, "SELECT * FROM todos")

        .fetch_all(pool)

        .await?;

    Ok(todos)

}

  

async fn get_todo_by_id_db(pool: &SqlitePool, id: i64) -> Result<Todo, SqlxError> {

    let todo = sqlx::query_as!(Todo, "SELECT * FROM todos WHERE id = ?", id)

        .fetch_one(pool)

        .await?;

    Ok(todo)

}

  

async fn update_todo_db(pool: &SqlitePool, id: i64, title: Option<String>, completed: Option<bool>) -> Result<Todo, SqlxError> {

    let mut conn = pool.acquire().await?;

    let old_todo = get_todo_by_id_db(pool, id).await?;

  

    let new_title = title.unwrap_or(old_todo.title);

    let new_completed = completed.unwrap_or(old_todo.completed);

  

    sqlx::query!(

        "UPDATE todos SET title = ?, completed = ? WHERE id = ?",

        new_title,

        new_completed,

        id

    )

    .execute(&mut *conn)

    .await?;

  

    get_todo_by_id_db(pool, id).await

}

  

async fn delete_todo_db(pool: &SqlitePool, id: i64) -> Result<(), SqlxError> {

    let mut conn = pool.acquire().await?;

    sqlx::query!("DELETE FROM todos WHERE id = ?", id)

        .execute(&mut *conn)

        .await?;

    Ok(())

}

  

async fn create_todo(

    State(pool): State<SqlitePool>,

    Json(payload): Json<CreateTodo>,

) -> Result<Json<Todo>, AppError> {

    payload.validate()?;

    let todo = create_todo_db(&pool, &payload.title).await?;

    Ok(Json(todo))

}

  

async fn get_all_todos(

    State(pool): State<SqlitePool>,

) -> Result<Json<Vec<Todo>>, AppError> {

    let todos = get_all_todos_db(&pool).await?;

    Ok(Json(todos))

}

  

async fn get_todo_by_id(

    State(pool): State<SqlitePool>,

    Path(id): Path<i64>,

) -> Result<Json<Todo>, AppError> {

    let todo = get_todo_by_id_db(&pool, id).await.map_err(|e| match e {

        SqlxError::RowNotFound => AppError::NotFound,

        _ => AppError::Sqlx(e)

    })?;

    Ok(Json(todo))

}

  

async fn update_todo(

    State(pool): State<SqlitePool>,

    Path(id): Path<i64>,

    Json(payload): Json<UpdateTodo>,

) -> Result<Json<Todo>, AppError> {

    payload.validate()?;

    let todo = update_todo_db(&pool, id, payload.title, payload.completed).await.map_err(|e| match e {

        SqlxError::RowNotFound => AppError::NotFound,

        _ => AppError::Sqlx(e)

    })?;

    Ok(Json(todo))

}

  

async fn delete_todo(

    State(pool): State<SqlitePool>,

    Path(id): Path<i64>,

) -> Result<StatusCode, AppError> {

    delete_todo_db(&pool, id).await.map_err(|e| match e {

        SqlxError::RowNotFound => AppError::NotFound,

        _ => AppError::Sqlx(e)

    })?;

    Ok(StatusCode::NO_CONTENT)

}

  

#[tokio::main]

async fn main() {

    dotenv::dotenv().ok();

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

    let pool = SqlitePool::connect(&database_url)

        .await

        .expect("Failed to create pool.");

  

    let app = Router::new()

        .route("/todos", get(get_all_todos).post(create_todo))

        .route("/todos/:id", get(get_todo_by_id).patch(update_todo).delete(delete_todo))

        .with_state(pool);

  

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

    println!("Listening on {}", listener.local_addr().unwrap());

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

}

第四部分:运行与测试

4.1 运行API服务

在项目根目录下运行:

bash 复制代码
cargo run

如果一切顺利,您将看到服务在0.0.0.0:3000上启动。

警告忽略即可:存在一个关于"未使用导入"(unused imports)的警告,不用当回事

4.2 使用cURL进行测试

创建一个新的Todo
bash 复制代码
curl -X POST -H "Content-Type: application/json" -d "{\"title\": \"学习 Rust\"}" http://localhost:3000/todos
获取所有Todo
bash 复制代码
curl http://localhost:3000/todos

总结

通过本文的实战项目,你学习到了如何使用Axum和SQLx构建一个高性能、类型安全的RESTful API。我们涵盖了从项目初始化、数据库迁移到实现CRUD操作、错误处理和数据验证的全过程。

使用Rust及其生态系统中的优秀库(如Axum和SQLx)来构建Web API,具有以下显著优势:

  • 性能卓越:Rust的编译时优化和对底层硬件的精细控制,使其构建的应用具有极高的运行效率和低延迟,非常适合高并发场景。
  • 内存安全:Rust的所有权和借用检查机制从根本上杜绝了空指针、悬垂指针等常见的内存安全问题,让您无需担心因内存管理不当而引发的运行时崩溃。
  • 可靠性高 :强大的类型系统和错误处理机制(如ResultOption)使得代码更加健壮,能够在编译阶段就发现潜在的逻辑错误。
  • 现代化的异步生态:基于Tokio的异步运行时,使得处理大量并发连接变得简单高效,能够充分利用多核处理器的性能。

这只是一个开始。Rust在Web开发领域的生态系统正在迅速成熟,Axum和SQLx是其中的佼佼者。希望本文能激发您使用Rust构建更多强大应用的兴趣。

想了解更多关于Rust语言的知识及应用,可前往华为开放原子旋武开源社区(https://xuanwu.openatom.cn/),了解更多资讯~

相关推荐
q***46522 小时前
对基因列表中批量的基因进行GO和KEGG注释
开发语言·数据库·golang
柠石榴2 小时前
GO-1 模型本地部署完整教程
开发语言·后端·golang
FAREWELL000753 小时前
Lua环境的配置 和 Lua的一些简单语法逻辑
开发语言·lua
Blossom.1183 小时前
大模型量化压缩实战:从FP16到INT4的生产级精度保持之路
开发语言·人工智能·python·深度学习·神经网络·目标检测·机器学习
野生工程师3 小时前
【Python爬虫基础-3】数据解析
开发语言·爬虫·python
道19933 小时前
python实现电脑手势识别截图
开发语言·python
奇树谦3 小时前
Qt 自定义菜单栏 / 工具栏按钮 QToolButton + InstantPopup 详细解析
开发语言·数据库·qt
草莓熊Lotso4 小时前
Git 本地操作入门:版本控制基础、跨平台部署与仓库核心流程
开发语言·人工智能·经验分享·git·后端·架构·gitee