域名 AXUM.RS 将于2025年10月到期。我们无意再对其进行续费,我们希望你能够接续这个域名,让更多 AXUM 开发者继续受益。
  • 方案1️⃣AXUM.RS 域名 = 3000
  • 方案2️⃣方案1️⃣ + 本站所有专题原始 Markdown 文档 = 5000
  • 方案3️⃣方案2️⃣ + 本站原始数据库 = 5500
如果你有意接续这份 AXUM 情怀,请与我们取得联系。
说明:
  1. 如果有人购买 AXUM.RS 域名(方案1️⃣),或者该域名到期,本站将启用新的免费域名继续提供服务。
  2. 如果有人购买了 AXUM.RS 域名,且同时购买了内容和/或数据库(方案2️⃣/方案3️⃣),本站将关闭。届时我们或许会以另一种方式与你再相遇。

sqlx查询数据

本章将讨论使用 sqlx 执行 SELECT 语句,对数据进行查询。

第2章已经讲解过 fetch_xxxx() 系列的方法,本章我们通过实例来进行说明。为了便于展示,我们引入模板引擎,请在 Cargo.toml 加入依赖:

[dependencies]
# ...
askama = "0.12"

模板

之前专题已经讲解过模板引擎的用法,这里不再重复。请查看本章代码的 templatessrc/view 目录。

本专题页面使用的是 bootstrap,并且直接通过 CDN 进行引入,需要你能正常访问 CDN 网站才能加载 bootstrap 的 CSS 和 JS

本专题页面使用的是 bootstrap,并且直接通过 CDN 进行引入,需要你能正常访问 CDN 网站才能加载 bootstrap 的 CSS 和 JS

会员列表

会员列表功能是通过分页的形式显示所有会员,它需要使用到两个方法:

  • fetch_one:统计数据总记录数
  • fetch_all:获取记录列表
// src/db/member.rs

pub async fn list(
    conn: &sqlx::MySqlPool,
    page: u32,
) -> Result<Paginate<Vec<model::member::Member>>> {
    let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM member")
        .fetch_one(conn)
        .await
        .map_err(Error::from)?;

    let sql = format!(
        "SELECT * FROM member ORDER BY id DESC LIMIT {} OFFSET {}",
        DEFAULT_PAGE_SIZE,
        page * DEFAULT_PAGE_SIZE
    );
    let data = sqlx::query_as(&sql)
        .fetch_all(conn)
        .await
        .map_err(Error::from)?;

    Ok(Paginate::new(count.0 as u32, page, data))
}

参数

  • conn: &sqlx::MySqlPool:数据库连接池的引用,由函数调用者提供。本案例中,是由 handler 从状态共享中获取并传递给该函数
  • page: u32:当前页码。从 0 开始
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM member")
        .fetch_one(conn)
        .await
        .map_err(Error::from)?;

使用 query_as() 配合 fetch_one() 来获取统计结果。上一章讲过,对于 SELECT COUNT 来说,可以使用 (i64,) 作为 query_as() 的映射对象。

 let sql = format!(
        "SELECT * FROM member ORDER BY id DESC LIMIT {} OFFSET {}",
        DEFAULT_PAGE_SIZE,
        page * DEFAULT_PAGE_SIZE
    );
    let data = sqlx::query_as(&sql)
        .fetch_all(conn)
        .await
        .map_err(Error::from)?;
  • 由于需要分页,所以我们首先拼装完整的 SQL 语句。注意,MySQL 8 才支持 LIMIT n OFFSET m 的写法
  • 通过 query_as() 配合 fetch_all() 获取到记录列表
    • 此例中,我们并不需要显式地指定 data 变量的类型(let data: Vec<Member> = sqlx::query_as...),因为 rust 根据函数签名中的返回值,可以推断出该变量的类型
  • 此例中,我们并不需要显式地指定 data 变量的类型(let data: Vec<Member> = sqlx::query_as...),因为 rust 根据函数签名中的返回值,可以推断出该变量的类型

分页对象

// src/db/mod.rs

#[derive(Serialize)]
pub struct Paginate<T: Serialize> {
    pub total: u32,
    pub total_page: u32,
    pub page: u32,
    pub page_size: u32,
    pub data: T,
}

impl<T: Serialize> Paginate<T> {
    pub fn new(total: u32, page: u32, data: T) -> Self {
        let total_page = f64::ceil(total as f64 / DEFAULT_PAGE_SIZE as f64) as u32;
        Self {
            total,
            page,
            total_page,
            page_size: DEFAULT_PAGE_SIZE,
            data,
        }
    }
    pub fn has_prev(&self) -> bool {
        self.page > 0
    }
    pub fn has_next(&self) -> bool {
        self.page < self.last_page()
    }
    pub fn last_page(&self) -> u32 {
        self.total_page - 1
    }
}
  • 分别定义了总记录数(total)、总页数(total_page)、当前页码(page)、每页记录条数(page_size),以及分页数据(data)
  • 注意泛型签名,由于最终要通过 axum 输出到屏幕,所以泛型T必须实现 Serialize

handler 的定义

最后,我们看一下 handler 的定义。

// src/handler.rs

fn get_conn(state: &AppState) -> Arc<sqlx::MySqlPool> {
    state.pool.clone()
}

#[derive(Deserialize)]
pub struct PageQuery {
    pub page: Option<u32>,
}

pub async fn index(
    Extension(state): Extension<Arc<AppState>>,
    Query(q): Query<PageQuery>,
) -> Result<Html<String>> {
    let conn = get_conn(&state);

    let p = member::list(&conn, q.page.unwrap_or(0)).await?;

    let tpl = view::Home { p };
    let html = tpl.render().map_err(Error::from)?;
    Ok(Html(html))
}
  • get_conn():用于从共享状态中获取 MySQL 连接池。由于连接池对象由 Arc 包裹,所以可以放心的 Clone 它——只是增加引用计数,并没有克隆整个连接池对象
  • PageQuery 结构体:用于将用户提交的 Url 参数反序列化。目前只有一个 page,它用于接收当前页码。为了容错,这里使用了 Option<>
  • index():会员列表的 handler
    • 首先,通过 get_conn 获取到数据库连接池
    • 然后,调用 db::member::list函数,获取到分页数据。这里就将 db::member::list 所需要的连接池引用传递过去了。
    • 之后,实例化一下模板对象,并将分页数据传递过去
    • 最后渲染模板并返回最终结果
  • 首先,通过 get_conn 获取到数据库连接池
  • 然后,调用 db::member::list函数,获取到分页数据。这里就将 db::member::list 所需要的连接池引用传递过去了。
  • 之后,实例化一下模板对象,并将分页数据传递过去
  • 最后渲染模板并返回最终结果

会员详情

还记得上一章讲过的 fetch_onefetch_optional 的区别吗?为什么在统计数据的时候可以放心大胆的使用 fetch_one,而在会员详情时,却要谨慎地使用 fetch_optional 呢?

// src/db/member.rs

pub async fn find(conn: &sqlx::MySqlPool, id: u32) -> Result<Option<model::member::Member>> {
    let m = sqlx::query_as("SELECT * FROM member WHERE id=?")
        .bind(id)
        .fetch_optional(conn)
        .await
        .map_err(Error::from)?;
    Ok(m)
}

通过 query_as()fetch_optional(),获取到单条记录。注意,它的返回值是 Option<>。我们来看看 handler 中是怎么进行处理的。

// src/handler.rs

pub async fn detail(
    Extension(state): Extension<Arc<AppState>>,
    Path(id): Path<u32>,
) -> Result<Html<String>> {
    let conn = get_conn(&state);

    let m = member::find(&conn, id).await?;

    match m {
        Some(m) => {
            let tpl = view::Detail { m };
            let html = tpl.render().map_err(Error::from)?;
            Ok(Html(html))
        }
        None => Err(Error::not_found("不存在的会员")),
    }
}

我们发现,在拿到结果之后,handler 通过 match 来处理这个 Option<Member>

match m {
    Some(m) => {
        let tpl = view::Detail { m };
        let html = tpl.render().map_err(Error::from)?;
        Ok(Html(html))
    }
    None => Err(Error::not_found("不存在的会员")),
}

本章代码位于03/查询数据分支。

要查看完整内容,请先登录