7

我想设置一个接受 SQLx PgPoolMySqlPool的通用函数。

use dotenv::dotenv;
use sqlx::postgres::PgPool;
use sqlx::{Pool, Database};
use std::env;

#[derive(Debug)]
struct Todo {
    id: i64,
    description: String,
    done: bool,
}

#[actix_web::main]
async fn main() -> anyhow::Result<()> {
    dotenv().ok();
    let pool = PgPool::connect(&env::var("DATABASE_URL")?).await?;
    list_todos(&pool).await?;
    Ok(())
}



async fn list_todos<D: Database>(pool: &Pool<D>) -> anyhow::Result<Vec<Todo>> {
    let todos = sqlx::query_as!(
        Todo,
        r#"
        SELECT id, description, done
        FROM todos
        ORDER BY id
        "#
    )
    .fetch_all(pool)
    .await?;

    Ok(todos)
}

我看到的错误是:

32 |     .fetch_all(pool)
   |      ^^^^^^^^^ expected type parameter `D`, found struct `Postgres`
   |
   = note: expected type parameter `D`
                      found struct `Postgres`

error[E0277]: the trait bound `for<'c> &'c mut <D as sqlx::Database>::Connection: Executor<'c>` is not satisfied

关于如何设置函数以接受 PgPool 或 MySqlPool 参数的任何提示?谢谢

4

3 回答 3

1

查看宏的 proc 宏扩展query!,看起来它们根据.env文件中的连接字符串填充了底层 DB 的特定类型。因此,宏在函数内部Postgres实现生成专门的代码,但您的函数在不同的实现者上是通用的Database,即期望Pool<Db> where Db: Database.

cargo 通过一些重新格式化来扩展输出:

    use ::sqlx::Arguments as _;
    let query_args =
        <sqlx::postgres::Postgres as ::sqlx::database::HasArguments>::Arguments::default();
    let todos = ::sqlx::query_with::<sqlx::postgres::Postgres, _>(
        "\n        SELECT id, description, done\n        FROM todos\n        ORDER BY id\n        ",
        query_args,
    )
    .try_map(|row: sqlx::postgres::PgRow| {
        use ::sqlx::Row as _;
        let sqlx_query_as_id = row.try_get_unchecked::<i32, _>(0usize)?;
        let sqlx_query_as_description = row.try_get_unchecked::<String, _>(1usize)?;
        let sqlx_query_as_done = row.try_get_unchecked::<bool, _>(2usize)?;
        Ok(Todo {
            id: sqlx_query_as_id,
            description: sqlx_query_as_description,
            done: sqlx_query_as_done,
        })
    })
    .fetch_all(pool)
    .await?;
    Ok(todos)

即使您按照@Netwave 的建议使用 generic Executor,宏仍然会在函数内部产生相同的非泛型代码并拒绝编译。虽然,@Netwave 的答案使用query函数而不是编译时检查的query!宏是有区别的。

使用Executortrait 调度查询并放弃对编译时检查的支持允许编写如下内容:

async fn list_todos_unchecked<'e, Exe>(pool: Exe) -> anyhow::Result<()>
where
    Exe: Executor<'e>,
    <Exe::Database as HasArguments<'e>>::Arguments:
        IntoArguments<'e, <Exe as Executor<'e>>::Database>,
{
    let todos = sqlx::query(
        "\n        SELECT id, description, done\n        FROM todos\n        ORDER BY id\n        ",
    )
    .fetch_all(pool)
    .await?;
    Ok(())
}

sqlx还支持Any在运行时支持代理到任何启用的 DB 驱动程序的驱动程序。sqlx尚不支持基于Any驱动程序的宏,但已在此问题中进行了跟踪。

于 2022-01-03T13:59:45.137 回答
0

查询宏文档指出:

QueryAs 实例将绑定到与 query!() 编译时所针对的相同数据库类型(例如,您不能针对 Postgres 数据库构建然后针对 MySQL 数据库运行查询)。

但是,该功能没有此限制。知道了这一点,我们可以构建一些通用的函数,这些函数对任何数据库类型的任何执行器类型都是通用的。


当您尝试对 sqlx 中的任何后端进行通用时,您需要对数据库(sqlite、mysql、postgresql 等)、执行程序(PgPool、、SqlitePoola raw Connection、aTransaction等)、任何编码(参数/绑定变量)和解码(query_as 返回类型)值。

例如,创建一个表:

pub async fn create_products_table<'a, DB, E>(e: E) -> Result<(), Error>
where
    DB: Database,
    <DB as HasArguments<'a>>::Arguments: IntoArguments<'a, DB>,
    E: Executor<'a, Database = DB>,
{
    sqlx::query(include_str!(
        "../sql/create_products_table.sql"
    ))
    .execute(e)
    .await?;
    Ok(())
}

从表中选择项目:

pub struct Product {
    name: String,
    other: String,
}

impl<'r, R> FromRow<'r, R> for Product
where
    R: Row,
    //Here we have bounds for index by str
    for<'c> &'c str: ColumnIndex<R>,
    //We need this bound because `Product` contains `String`
    for<'c> String: Decode<'c, R::Database> + Type<R::Database>,
{
    fn from_row(row: &'r R) -> Result<Self, Error> {
        //Here we index by str
        let name = row.try_get("name")?;
        //Here we index by str
        let other = row.try_get("other")?;
        Ok(Self { name, other })
    }
}

pub async fn select_products<'a, 'b, DB, E>(
    e: E,
    name: &'a str,
) -> impl Stream<Item = Result<Product, Error>> + 'b
where
    'a: 'b,
    DB: Database,
    <DB as HasArguments<'a>>::Arguments: IntoArguments<'a, DB>,
    for<'c> E: 'a + Executor<'c, Database = DB>,

    //We need this bound because `Product` contains `String`
    for<'c> String: Decode<'c, DB> + Type<DB>,

    //We need this bound because 'name' function argument is of type `&str`
    for<'c> &'c str: Encode<'c, DB> + Type<DB>,

    //We need this bound for `FromRow` implementation or if we intend on indexing rows by str.
    //You will probably need to write your own `FromRow` implementation.
    for<'c> &'c str: ColumnIndex<<DB as Database>::Row>,
{
    query_as(include_str!("../sql/select_products.sql"))
        .bind(name)
        .fetch(e)
}

我希望更精简或记录,因为它似乎成熟的宏。也许我会为 sqlx 的 PR 起草一些文档。如果您需要更多示例,请告诉我。


我也知道Any数据库驱动程序,但是它具有明显的缺点,即需要不透明类型而没有简单的方法将具体类型转换为不透明类型。这意味着您必须在AnyPool所有对数据库通用的地方使用,并且永远不能使用具体的PgPoolSqlitePool. 您还必须完全依赖连接字符串来区分数据库实现(恶心)

于 2022-01-04T03:15:06.043 回答
0

无法正确测试它,但您可以Executor直接将其用作泛型。从文档中:

包含或可以提供用于对数据库执行查询的数据库连接的类型。

不保证连续查询在同一物理数据库连接上运行。

Connection 是一个 Executor,它保证连续查询在同一个物理数据库连接上运行。

为以下项目实施:

&Pool &mut 池连接 &mut 连接

async fn list_todos<'e, Exe: Executor<'e>>(executor: Exe) -> anyhow::Result<()>
where
    <Exe::Database as HasArguments<'e>>::Arguments:
        IntoArguments<'e, <Exe as Executor<'e>>::Database>,
{
    let todos = sqlx::query(
        r#"
        SELECT id, description, done
        FROM todos
        ORDER BY id
        "#,
    )
    .execute(executor)
    .await?;

    Ok(())
}

免责声明:返回类型和其他事情发生了一些变化,因为我没有正确测试它的环境。但是对于 Executor 绑定,它编译没有问题。你必须适应剩下的东西。

于 2022-01-03T11:11:47.323 回答