sqlx概览
开始之前,我们需要一些准备工作,包括:创建用于演示的数据库及数据、创建一个 Rust 项目以及为项目编写一些基础性代码。sqlx的查询构造器
本章将讨论 sqlx 的查询构造器。sqlx 提供了多种查询构造器:`query`、`query_as`、`query!`、`query_as!`、`QueryBuilder`等,合理使用将提升效率。这些查询构造器用来生成数据库所需要的 SQL 语句,不要被它们的名字迷惑了——认为它们是用来查询数据(SQL中的`SELECT`行为)sqlx查询数据
本章将讨论使用 sqlx 执行 `SELECT` 语句,对数据进行查询。sqlx实现增删改
本章将讨论使用 sqlx 进行增删改(`INSERT/DELETE/UPDATE`)操作。使用sqlx的事务实现转账
本章我们将通过用户之间转账来讨论 sqlx 的事务。为了保证转账的完整性、正确性,我们必须使用事务来处理。使用sqlx的QueryBuilder构建复杂、动态的查询
本章我们讨论如何优雅方便地使用 sqlx 构建复杂的、动态的 SQL。sqlx 提供了 [`QueryBuilder`](https://docs.rs/sqlx/latest/sqlx/struct.QueryBuilder.html) 结构体,它可以方便地实现 SQL 的构建。sqlx优雅地实现IN查询
在日常开发中,`IN` 查询是非常常见的需求,你会怎么来处理呢?借助 sqlx 的`QueryBuilder` 可以方便地实现。
sqlx查询数据
本章将讨论使用 sqlx 执行 SELECT
语句,对数据进行查询。
第2章已经讲解过 fetch_xxxx()
系列的方法,本章我们通过实例来进行说明。为了便于展示,我们引入模板引擎,请在 Cargo.toml
加入依赖:
[dependencies]
# ...
askama = "0.12"
模板
之前专题已经讲解过模板引擎的用法,这里不再重复。请查看本章代码的 templates
和 src/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_one
和 fetch_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/查询数据分支。