获取已确认订阅者列表

我们需要编写一个新的查询来检索所有已确认订阅者的列表。

状态列上的 WHERE 子句足以隔离我们关心的行:

//! src/routes/newsletters.rs
// [...]
struct ConfirmedSubscriber {
    email: String,
}

#[tracing::instrument(name = "Get confirmed subscribers", skip(pool))]
async fn get_confirmed_subscribers(
    pool: &PgPool,
) -> Result<Vec<ConfirmedSubscriber>, anyhow::Error> {
    let rows = sqlx::query_as!(
        ConfirmedSubscriber,
        r#"
        SELECT email
        FROM subscriptions
        WHERE status = 'confirmed'
        "#,
    )
    .fetch_all(pool)
    .await?;

    Ok(rows)
}

这里有一些新特性:我们使用 sqlx::query_as! 而不是 sqlx::query!

sqlx::query_as! 将检索到的行映射到其第一个参数指定的类型, ConfirmedSubscriber, 从而省去了我们大量的样板代码。 请注意, ConfirmedSubscriber 只有一个字段 - email。我们正在最小化从数据库获取的数据量,将查询限制在实际需要发送新闻通讯的列上。数据库的工作量更少,网络上传输的数据也更少。

在这种情况下,这不会带来明显的区别,但在处理数据占用空间更大的大型应用程序时,牢记这一点是一个好习惯。

为了在我们的处理程序中使用 get_confirmed_subscribers, 我们需要一个 PgPool——我们可以从应用程序状态中提取一个,就像我们在 POST /subscriptions 中所做的那样。

//! src/routes/newsletters.rs
// [...]
pub async fn publish_newsletter(
    _body: web::Json<BodyData>,
    pool: web::Data<PgPool>,
) -> HttpResponse {
    let _subscribers = get_confirmed_subscribers(&pool).await?;
    HttpResponse::Ok().finish()
}

这并不能编译通过:

  --> src/routes/newsletters.rs:24:62
   |
23 |   ) -> HttpResponse {
   |  ___________________-
24 | |     let _subscribers = get_confirmed_subscribers(&pool).await?;
   | |                                                              ^ cannot use
 the `?` operator in an async function that returns `HttpResponse`
25 | |     HttpResponse::Ok().finish()
26 | | }
   | |_- this function should return `Result` or `Option` to accept `?`

SQL 查询可能会失败, get_confirmed_subscribers 也可能会失败——我们需要更改 publish_newsletter 的返回类型。

我们需要返回一个带有适当错误类型的 Result, 就像我们在上一章中所做的那样:

//! src/routes/newsletters.rs
// [...]
use actix_web::{http::StatusCode, web, HttpResponse, ResponseError};
use sqlx::PgPool;

use crate::routes::error_chain_fmt;

#[derive(thiserror::Error)]
pub enum PublishError {
    #[error(transparent)]
    UnexpectedError(#[from] anyhow::Error),
}

// Same logic to get the full error chain on `Debug`
impl std::fmt::Debug for PublishError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        error_chain_fmt(self, f)
    }
}

impl ResponseError for PublishError {
    fn status_code(&self) -> actix_web::http::StatusCode {
        match self {
            PublishError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }
}

pub async fn publish_newsletter(
    _body: web::Json<BodyData>,
    pool: web::Data<PgPool>,
) -> Result<HttpResponse, PublishError> {
    let _subscribers = get_confirmed_subscribers(&pool).await?;
    Ok(HttpResponse::Ok().finish())
}
// [...]

注: 你还需要自己把 error_chain_fmt 移动到 routes.rs 并修改访问修饰符为 pub

利用我们在第 8 章中学到的知识,推出一个新的错误类型并不需要花费太多精力!

需要说明的是,我们正在对代码进行一些面向未来的设计: 我们将 PublishError 建模为枚举, 但目前只有一种变体。结构体 (或 actix_web::error::InternalError)暂时就足够了。

cargo check 现在应该可以通过了。