Rust与SQL手工映射以及特殊枚举处理方法


使用的库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。