如何用同一结构体同时适配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
相关产品推荐
相关产品推荐

