携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情
通过前面3个章节,我们对axum的基本使用已经大概掌握,能够进行简单上手了。一个后端服务,除了路由分发框架,自然需要操作数据库。在这里,我们选择sqlx访问数据库。 至于sqlx,以下是它的简介:
sqlx 是 rust 中的一个数据库访问工具。具有以下特点:
- 异步:原生就支持异步,在并发性高的场合能够得到更好的支持
- 编译时检查查询:sqlx可以在 cargo build 的时候检查执行sql和响应值
- 多数据库支持:PostgresSQL,MySql,SqlLite,MSSql,MariaDB
- 多运行时支持:支持主流 rust 运行时。async-std,tokio,actix,native-tls,rustls
- 内置连接池,支持查询缓存
但是,sqlx 并不是 orm 框架
接下来,我们进入sqlx的世界。
首先还是看官方文档:
use sqlx::postgres::PgPoolOptions;
// use sqlx::mysql::MySqlPoolOptions;
// etc.
#[async_std::main]
// or #[tokio::main]
// or #[actix_web::main]
async fn main() -> Result<(), sqlx::Error> {
// Create a connection pool
// for MySQL, use MySqlPoolOptions::new()
// for SQLite, use SqlitePoolOptions::new()
// etc.
let pool = PgPoolOptions::new()
.max_connections(5)
.connect("postgres://postgres:password@localhost/test").await?;
// Make a simple query to return the given parameter (use a question mark `?` instead of `$1` for MySQL)
let row: (i64,) = sqlx::query_as("SELECT $1")
.bind(150_i64)
.fetch_one(&pool).await?;
assert_eq!(row.0, 150);
Ok(())
}
这里只是简单得演示了sqlx如何连接数据库,这部分也没啥好说的,唯一要注意的是,连接不同的数据库,最好使用不同的连接池postgres引用PgPoolOptions,mysql使用MySqlPoolOptions。
当然,如果你想要一个通用的连接池,那可以使用 sqlx::any::AnyPoolOptions
接下来才是重点,sqlx中如何进行sql操作,大体上可以分成两类:
方法一:使用query!宏
// let mut conn = <impl sqlx::Executor>;
let account = sqlx::query!("select (1) as id, 'Herp Derpinson' as name")
.fetch_one(&mut conn)
.await?;
// anonymous struct has `#[derive(Debug)]` for convenience
println!("{:?}", account);
println!("{}: {}", account.id, account.name);
也可以使用struct对返回的数据进行映射
#[derive(Debug)]
struct Account {
id: i32,
name: String
}
// let mut conn = <impl sqlx::Executor>;
let account = sqlx::query_as!(
Account,
"select * from (select (1) as id, 'Herp Derpinson' as name) accounts where id = ?",
1i32
)
.fetch_one(&mut conn)
.await?;
println!("{:?}", account);
println!("{}: {}", account.id, account.name);
方法二:使用query函数
pub async fn create_item(payload: CreateTodoItem) -> Result<u64, Error> {
let sql = "insert into todo_item(title, list_id) values (?, ?)";
let pool = db::get_pool().unwrap();
let affect_rows = sqlx::query(sql)
.bind(payload.title)
.bind(payload.list_id)
.execute(pool)
.await?
.rows_affected();
Ok(affect_rows)
}
同理,用struct对返回的数据进行映射用query_as函数,但是需要实现FromRow这个特质
/// 待办事项模型
#[derive(Debug, Clone, Deserialize, Serialize, FromRow)]
pub struct TodoListItem {
pub id: u32,
pub title: String,
pub checked: bool,
pub list_id: u32,
}
pub async fn item_list(id: u32) -> Result<Vec<TodoListItem>, Error> {
let pool = db::get_pool().unwrap();
let sql = "select id, title, checked, list_id from todo_item where list_id = ?";
let list = sqlx::query_as::<_, TodoListItem>(sql)
.bind(id)
.fetch_all(pool)
.await?;
Ok(list)
}
当然,操作数据库的宏和函数不止这几个,不过常用的操作已经够了。
接下来是执行函数:
pub trait Executor<'c>: Send + Debug {
type Database: Database;
fn fetch_many<'e, 'q, E>(
self,
query: E
) -> Pin<Box<dyn Stream<Item = Result<Either<<Self::Database as Database>::QueryResult, <Self::Database as Database>::Row>, Error>> + Send + 'e, Global>>
where
'q: 'e,
'c: 'e,
E: 'q + Execute<'q, Self::Database>;
fn fetch_optional<'e, 'q, E>(
self,
query: E
) -> Pin<Box<dyn Future<Output = Result<Option<<Self::Database as Database>::Row>, Error>> + Send + 'e, Global>>
where
'q: 'e,
'c: 'e,
E: 'q + Execute<'q, Self::Database>;
fn prepare_with<'e, 'q>(
self,
sql: &'q str,
parameters: &'e [<Self::Database as Database>::TypeInfo]
) -> Pin<Box<dyn Future<Output = Result<<Self::Database as HasStatement<'q>>::Statement, Error>> + Send + 'e, Global>>
where
'q: 'e,
'c: 'e;
fn execute<'e, 'q, E>(
self,
query: E
) -> Pin<Box<dyn Future<Output = Result<<Self::Database as Database>::QueryResult, Error>> + Send + 'e, Global>>
where
'q: 'e,
'c: 'e,
E: 'q + Execute<'q, Self::Database>,
{ ... }
fn execute_many<'e, 'q, E>(
self,
query: E
) -> Pin<Box<dyn Stream<Item = Result<<Self::Database as Database>::QueryResult, Error>> + Send + 'e, Global>>
where
'q: 'e,
'c: 'e,
E: 'q + Execute<'q, Self::Database>,
{ ... }
fn fetch<'e, 'q, E>(
self,
query: E
) -> Pin<Box<dyn Stream<Item = Result<<Self::Database as Database>::Row, Error>> + Send + 'e, Global>>
where
'q: 'e,
'c: 'e,
E: 'q + Execute<'q, Self::Database>,
{ ... }
fn fetch_all<'e, 'q, E>(
self,
query: E
) -> Pin<Box<dyn Future<Output = Result<Vec<<Self::Database as Database>::Row, Global>, Error>> + Send + 'e, Global>>
where
'q: 'e,
'c: 'e,
E: 'q + Execute<'q, Self::Database>,
{ ... }
fn fetch_one<'e, 'q, E>(
self,
query: E
) -> Pin<Box<dyn Future<Output = Result<<Self::Database as Database>::Row, Error>> + Send + 'e, Global>>
where
'q: 'e,
'c: 'e,
E: 'q + Execute<'q, Self::Database>,
{ ... }
fn prepare<'e, 'q>(
self,
query: &'q str
) -> Pin<Box<dyn Future<Output = Result<<Self::Database as HasStatement<'q>>::Statement, Error>> + Send + 'e, Global>>
where
'q: 'e,
'c: 'e,
{ ... }
}
可以看出主要有: fetch_many、fetch_optional、prepare_with、execute、execute_many、 fetch、fetch_all、 fetch_one、prepare 等方法。
更具体的说明可以看文档,我们通过上面的例子和函数名字,基本也能够了解这些函数的用处。
总结:今天的sqlx操作数据库其实非常简单,只有几个常用api,然后写入对应的sql语句即可。(你要我写详细点,臣妾也做不到啊,毕竟官方文档就给了几个例子)
踩坑点: sqlx当然没有这么容易就结束了,真正的麻烦现在才开始。
踩坑一:rust类型与sqlx类型问题
rust类型与sqlx读写数据库的类型之间如何要匹配,sqlx文档其实有介绍,不同的数据库需要找到对应的类型,不然用query_as进行类型映射就会报错。
这里以我用的数据库MySQL为例,常见的文字、数字类型都有明确的类型。
而时间类型需要用到第三方库chrono或者time。特别要注意的是,还需要在Cargo Feature中配置对应的设置,要不然无法匹配时间类型。(我曾因为时间类型而卡了好几天,最后才发现要配置Feature,而且sqlx的Feature在官方文档居然没有说明,而是在github上面,真是巨坑!)
当然,大佬们更多都是看源码的。更多内容的可以看官方源码。