rust:axum+sqlx实战学习笔记4

2,198 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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引用PgPoolOptionsmysql使用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_manyfetch_optionalprepare_withexecuteexecute_manyfetchfetch_allfetch_oneprepare 等方法。

更具体的说明可以看文档,我们通过上面的例子和函数名字,基本也能够了解这些函数的用处。

总结:今天的sqlx操作数据库其实非常简单,只有几个常用api,然后写入对应的sql语句即可。(你要我写详细点,臣妾也做不到啊,毕竟官方文档就给了几个例子)

踩坑点: sqlx当然没有这么容易就结束了,真正的麻烦现在才开始。

踩坑一:rust类型与sqlx类型问题

rust类型与sqlx读写数据库的类型之间如何要匹配,sqlx文档其实有介绍,不同的数据库需要找到对应的类型,不然用query_as进行类型映射就会报错。

1660198503416.png

这里以我用的数据库MySQL为例,常见的文字、数字类型都有明确的类型。

image.png

而时间类型需要用到第三方库chrono或者time。特别要注意的是,还需要在Cargo Feature中配置对应的设置,要不然无法匹配时间类型。(我曾因为时间类型而卡了好几天,最后才发现要配置Feature,而且sqlx的Feature在官方文档居然没有说明,而是在github上面,真是巨坑!)

1660199205837.png

当然,大佬们更多都是看源码的。更多内容的可以看官方源码。