使用的库tokio-postgres为特征提供了一些基本实现,可用于将应用程序类型转换为 SQL 类型,反之亦然。例如,有FromSQL,它会自动将 Rust 原语转换为 PosgreSQL 类型:bool转为bool、i64 转换为 bigint, &str 或 String 转换为 text。
关于 ORM 的争论很长,并且充满了细微差别。除其他外,有两个要点:
- ORM 迫使开发人员耦合实体和基础设施层。
- 除了基本情况外,ORM 无法填补对象和关系结构之间的空白。对于任何不平凡的情况,SQL 执行的优化都会丢失,并且会重复查询以构建所需的对象。
缺少 ORM 并且需要在 Rust 和 Postgres 类型之间手动映射并不是一个缺点,而是一个很好的实践。从 SQL 映射到 Rust
库tokio-postgres公开一个FromSQL特征,其中包含基元和某些特定类型的基本实现。有了这个实现,text 转为 String, 或 bigint 转为 i64。
Row类型可以与Rust的trait From一起使用,在Postgres行与Rust实体对象之间转换。例如,对于一个给定的结构Article,存储库上的From实现可能看起来像这样:
// Domain pub struct Article { pub id: String, pub title: String, pub created_at: DateTime, } // Repository impl From<Row> for Article { fn from(row: Row) -> Self { Self { id: row.get("id"), title: row.get("title"), created_at: row.get("created_at"), } } }
let result = client.query("select * from article").await?; let articles: Vec<Article> = result.into_iter() .map(Article::from) .collect();
|
麻烦是:两者枚举之间转换:
// PostgreSQL create type myenum as enum('variant_a', 'variant_b'); Postgres type myenum
// Rust pub enum MyEnum { VariantA, VariantB, }
|
tokio-postgres不支持枚举转换,因为这个库不提供任何对象 <-> 关系映射功能:系统不知道如何将一个映射到另一个。这就是库公开的FromSQL特征可以方便地为所需的 Rust 枚举创建自己的实现的地方。FromSQL是一个公开四个方法的特征,其中两个对这种情况有用:from_sql,它将负责实际的类型转换,以及accepts,它将检查是否应为当前类型执行类型转换。
pub trait FromSql<'a>: Sized { fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>>; […] fn accepts(ty: &Type) -> bool; }
|
任务其实很简单: from_sql必须返回正确的枚举变体,这可以实现与原始二进制字符串的匹配;在里面accepts方法可以对枚举的名称进行检查。
impl FromSql<'_> for MyEnum { fn from_sql(_sql_type: &Type, value: &[u8]) -> Result<Self, Box<dyn Error + Sync + Send>> { match value { b"variant_a" => Ok(MyEnum::VariantA), b"variant_b" => Ok(MyEnum::VariantB), _ => Ok(MyEnum::VariantA), } } fn accepts(sql_type: FromSqlType) -> bool { sql_type.name() == "myenum" } }
|
由于此实现与 Postgres 相关,因此它可以存在于 Postgres 存储库或数据库相关代码中,也可以存在于实体和域层之外。这样就避免了域和基础设施的耦合.
此实现将告诉 tokio-postgres如何针对此特定类型执行 SQL 和 Rust 之间的类型转换,因此每次查询返回一个myenum Postgres 类型,它将在应用程序中显示为MyEnum.
从 Rust 映射到 SQL
相反的情况是:当数据库中有一个myenum类型如何转到Rust?
tokio-postgres可以执行这种类型转换,但这需要使用ToSql trait的自定义实现to_sql和accept方法:
pub trait ToSql: fmt::Debug { fn to_sql(&self, ty: &Type, out: &mut BytesMut) -> Result<IsNull, Box<dyn Error + Sync + Send>> where Self: Sized; fn accepts(ty: &Type) -> bool where Self: Sized; fn to_sql_checked( &self, ty: &Type, out: &mut BytesMut, ) -> Result<IsNull, Box<dyn Error + Sync + Send>>; }
|
它也需要实现to_sql_checked,但是 types::to_sql_checked! 宏可以自动生成这个方法的实现。
当实现to_sql时,Rust枚举变体应该首先被转换为&str,这可以通过实现MyEnum的Display来实现,以便在MyEnum变体和字符串之间进行映射。
use std::fmt::{Display, Formatter, Result }; impl Display for MyEnum { fn fmt(&self, f: &mut Formatter) -> Result { match self { MyEnum::VariantA => write!(f, "variant_a"), MyEnum::VariantB => write!(f, "variant_b"), } } }
|
在为MyEnum实现了Display之后,有可能为MyEnum实现ToSql。
use tokio_postgres::{ types::{to_sql_checked, ToSql, IsNull, Type}, }; use bytes::BytesMut; use std::{error::Error, result::Result}; impl ToSql for Role { fn to_sql(&self, ty: &Type, out: &mut BytesMut) -> Result<IsNull, Box<dyn Error + Sync + Send>>; { format!("{}", self).to_sql(ty, out) } fn accepts(sql_type: &Type) -> bool { sql_type.name() == "myenum" } to_sql_checked!(); }
|
有了这个,每次Postgres收到Rust MyEnum时,它都会被转换成Postgres myenum。