You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用同一结构体同时适配sqlx查询与askama::Template渲染?

让结构体同时兼容sqlx与askama::Template的几种方案

问题背景

我需要复用同一个结构体,既要用sqlx从数据库读取数据(可空varchar列对应Option<String>类型),又要传入askama::Template做渲染,但Option<String>未实现Display trait,直接派生Template会触发如下错误:

error[E0277]: `std::option::Option<std::string::String>` doesn't implement `std::fmt::Display`
  --> src/root.rs:35:10
   |
35 | #[derive(Template)]
   |          ^^^^^^^^ `std::option::Option<std::string::String>` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `std::option::Option<std::string::String>`, which is required by `&std::option::Option<std::string::String>: std::fmt::Display`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
   = note: this error originates in the macro `$crate::format_args` which comes from the expansion of the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)

目前我用的是双结构体+From trait转换的方式,但需要写大量重复代码,有没有更高效的解决办法?

当前实现(双结构体转换)

use askama::Template;
use axum::{extract::Query, Extension};
use chrono::{DateTime, Utc};
use serde::Deserialize;
use sqlx::PgPool;

#[derive(sqlx::FromRow, Debug)]
pub struct LatestProvisioningRequest {
    uid: String,
    ts: DateTime<Utc>,
    ip: Option<String>,
}

pub struct LatestProvisioningRequestForTemplate {
    uid: String,
    ts: DateTime<Utc>,
    ip: String,
}

impl From<LatestProvisioningRequest> for LatestProvisioningRequestForTemplate {
    fn from(value: LatestProvisioningRequest) -> Self {
        let ip = match value.ip {
            Some(str) => str,
            None => "".to_owned(),
        };
        LatestProvisioningRequestForTemplate {
            uid: value.uid,
            ts: value.ts,
            ip
        }
    }
}

#[derive(Template)]
#[template(path = "root.html")]
pub struct RootTemplate {
    requests: Vec<LatestProvisioningRequestForTemplate>
}

#[derive(Debug, Deserialize)]
pub struct RootQueryParameters {}

pub async fn fetch_unenrolled(pool: &PgPool) -> Vec<LatestProvisioningRequestForTemplate> {
    let rows = sqlx::query_as::<_, LatestProvisioningRequest>(r#"SELECT * FROM latest_provisioning_requests"#)
    .fetch_all(pool)
    .await
    .unwrap();

    rows.into_iter().map(|x| x.into()).collect()
}

pub async fn root(
    Extension(pool): Extension<PgPool>,
    Query(RootQueryParameters {}): Query<RootQueryParameters>
) -> RootTemplate {
    RootTemplate {
        requests: fetch_unenrolled(&pool).await
    }
}

更优方案

方案1:直接在askama模板中处理Option(推荐)

askama的模板语法支持直接处理Option类型,不需要修改结构体。你可以在模板里用unwrap_or方法给空值设置默认显示内容,或者用条件判断:

模板示例(root.html)

{% for req in requests %}
  <tr>
    <td>{{ req.uid }}</td>
    <td>{{ req.ts }}</td>
    <td>{{ req.ip.unwrap_or("") }}</td>
  </tr>
{% endfor %}

修改后的结构体代码
直接给原结构体派生Template,不需要额外的转换结构体:

#[derive(sqlx::FromRow, Debug, Template)]
#[template(path = "root.html")]
pub struct LatestProvisioningRequest {
    uid: String,
    ts: DateTime<Utc>,
    ip: Option<String>,
}

#[derive(Template)]
#[template(path = "root.html")]
pub struct RootTemplate {
    requests: Vec<LatestProvisioningRequest>
}

// 数据查询逻辑也简化了:
pub async fn fetch_unenrolled(pool: &PgPool) -> Vec<LatestProvisioningRequest> {
    sqlx::query_as::<_, LatestProvisioningRequest>(r#"SELECT * FROM latest_provisioning_requests"#)
    .fetch_all(pool)
    .await
    .unwrap()
}

这种方式完全省去了结构体转换的代码,最简洁高效。

方案2:自定义askama过滤器(适合多场景复用)

如果很多地方需要统一处理Option<String>的空值显示,可以自定义一个过滤器,避免在模板里重复写unwrap_or(""):

过滤器实现

use askama::Result;

// 自定义过滤器:将Option<String>转为空字符串或实际值
pub fn empty_on_none(opt: &Option<String>) -> Result<String> {
    Ok(opt.as_deref().unwrap_or("").to_string())
}

在模板中使用过滤器
先在模板顶部声明过滤器,然后调用:

{% filter empty_on_none %}
{% for req in requests %}
  <tr>
    <td>{{ req.uid }}</td>
    <td>{{ req.ts }}</td>
    <td>{{ req.ip|empty_on_none }}</td>
  </tr>
{% endfor %}

方案3:用宏自动生成转换代码(适合坚持双结构体的场景)

如果一定要保留双结构体的模式,可以用宏来自动生成From trait的实现,减少重复代码。比如用syn和quote crate编写自定义宏,自动遍历结构体字段并处理Option类型的转换:

宏示例(需要引入syn和quote依赖)

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(FromSqlxToTemplate)]
pub fn derive_from_sqlx_to_template(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let struct_name = &input.ident;
    let template_struct_name = format!("{}ForTemplate", struct_name);
    let template_struct_ident = syn::Ident::new(&template_struct_name, struct_name.span());

    let fields = match &input.data {
        syn::Data::Struct(s) => &s.fields,
        _ => panic!("Only structs are supported"),
    };

    let field_conversions = fields.iter().map(|f| {
        let name = &f.ident;
        let ty = &f.ty;
        match ty {
            syn::Type::Path(p) if p.path.segments.last().unwrap().ident == "Option" => {
                quote! { #name: value.#name.unwrap_or_default() }
            }
            _ => quote! { #name: value.#name },
        }
    });

    let expanded = quote! {
        impl From<#struct_name> for #template_struct_ident {
            fn from(value: #struct_name) -> Self {
                Self {
                    #(#field_conversions,)*
                }
            }
        }
    };

    TokenStream::from(expanded)
}

使用宏

#[derive(sqlx::FromRow, Debug, FromSqlxToTemplate)]
pub struct LatestProvisioningRequest {
    uid: String,
    ts: DateTime<Utc>,
    ip: Option<String>,
}

#[derive(Template)]
#[template(path = "root.html")]
pub struct LatestProvisioningRequestForTemplate {
    uid: String,
    ts: DateTime<Utc>,
    ip: String,
}

这种方式适合有大量类似结构体的场景,一次性减少重复代码。


内容的提问来源于stack exchange,提问作者fadedbee

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 15:44:55