【问题标题】:How do I implement Queryable and Insertable for custom field types in Diesel?如何为 Diesel 中的自定义字段类型实现 Queryable 和 Insertable?
【发布时间】:2018-08-12 01:03:57
【问题描述】:

我有一个我想通过 Diesel 使用的 SQL 表:

CREATE TABLE records (
    id BIGSERIAL PRIMARY KEY,
    record_type SMALLINT NOT NULL,
    value DECIMAL(10, 10) NOT NULL
)

此表生成以下架构:

table! {
    records (id) {
        id -> Int8,
        record_type -> Int2,
        value -> Numeric,
    }
}

Diesel 将小数导出为 bigdecimal::BigDecimal,但我想改用 decimal::d128。我还想将record_type 映射到一个枚举,所以我这样声明我的模型:

use decimal::d128;

pub enum RecordType {
    A,
    B,
}

pub struct Record {
    pub id: i64,
    pub record_type: RecordType,
    pub value: d128,
}

由于非标准类型映射,我无法使用#derive(Queryable, Insertable),所以我尝试自己实现这些特征:

impl Queryable<records::SqlType, Pg> for Record {
    type Row = (i64, i16, BigDecimal);

    fn build(row: Self::Row) -> Self {
        Record {
            id: row.0,
            record_type: match row.1 {
                1 => RecordType::A,
                2 => RecordType::B,
                _ => panic!("Wrong record type"),
            },
            value: d128!(format!("{}", row.2)),
        }
    }
}

我不知道如何实现InsertableValues 关联类型是什么? Diesel 的文档对此不是很清楚。

也许有更好的方法来实现我想要做的事情?

Cargo.toml:

[dependencies]
bigdecimal = "0.0.10"
decimal = "2.0.4"
diesel = { version = "1.1.1", features = ["postgres", "bigdecimal", "num-bigint", "num-integer", "num-traits"] }
dotenv = "0.9.0"

【问题讨论】:

    标签: rust rust-diesel


    【解决方案1】:

    我发现创建实现ToSqlFromSql 的新类型包装器更方便。然后,您可以使用这些基本块进行构建,以创建可以派生 Queryable / Insertable 的更大类型。

    此示例仅显示如何执行枚举与SmallInt 的映射,但小数的情况相同。唯一的区别在于您执行转换的方式:

    #[macro_use]
    extern crate diesel;
    
    mod types {
        use diesel::sql_types::*;
        use diesel::backend::Backend;
        use diesel::deserialize::{self, FromSql};
        use diesel::serialize::{self, ToSql, Output};
        use std::io;
    
        table! {
            records (id) {
                id -> BigInt,
                record_type -> SmallInt,
            }
        }
    
        #[derive(Debug, Copy, Clone, AsExpression, FromSqlRow)]
        #[sql_type = "SmallInt"]
        pub enum RecordType {
            A,
            B,
        }
    
        impl<DB: Backend> ToSql<SmallInt, DB> for RecordType
        where
            i16: ToSql<SmallInt, DB>,
        {
            fn to_sql<W>(&self, out: &mut Output<W, DB>) -> serialize::Result
            where
                W: io::Write,
            {
                let v = match *self {
                    RecordType::A => 1,
                    RecordType::B => 2,
                };
                v.to_sql(out)
            }
        }
    
        impl<DB: Backend> FromSql<SmallInt, DB> for RecordType
        where
            i16: FromSql<SmallInt, DB>,
        {
            fn from_sql(bytes: Option<&DB::RawValue>) -> deserialize::Result<Self> {
                let v = i16::from_sql(bytes)?;
                Ok(match v {
                    1 => RecordType::A,
                    2 => RecordType::B,
                    _ => return Err("replace me with a real error".into()),
                })
            }
        }
    
        #[derive(Insertable, Queryable, Debug)]
        #[table_name = "records"]
        pub struct Record {
            pub id: i64,
            pub record_type: RecordType,
        }
    }
    

    有一个draft guide describing all the derives and their annotations,但它还没有提到整个类型的#[sql_type]。这让 Diesel 知道数据库内部需要什么样的底层存储。

    另见the Diesel tests for custom types

    【讨论】:

    • 是的,这将是理想的,但不幸的是,decimal::d128 有大约 30 个方法并实现了 >50 个特征,因此为包装器结构重新实现那些将是一个真正的麻烦。
    • @hweom 你可以添加一个Deref 实现,所以你不必实现这些。
    【解决方案2】:

    有时,了解宏的作用(派生只是宏的不同形式)的最简单方法是向编译器询问扩展代码。使用 nightly 编译器,您可以使用以下命令执行此操作:

    cargo rustc -- -Z unstable-options --pretty expanded > expanded.rs
    

    这将在expanded.rs中输出扩展代码。

    我们现在可以查看此文件以了解 #[derive(Insertable)] 扩展为什么。自然,我首先更改了Record 的定义以匹配 Diesel 的类型。经过一番清理,这是生成的代码:

    impl<'insert> diesel::insertable::Insertable<records::table> for &'insert Record {
        type Values = <(
            Option<diesel::dsl::Eq<records::id, &'insert i64>>,
            Option<diesel::dsl::Eq<records::record_type, &'insert i16>>,
            Option<diesel::dsl::Eq<records::value, &'insert BigDecimal>>
        ) as diesel::insertable::Insertable<records::table>>::Values;
    
        #[allow(non_shorthand_field_patterns)]
        fn values(self) -> Self::Values {
            let Record {
                id: ref id,
                record_type: ref record_type,
                value: ref value,
            } = *self;
            diesel::insertable::Insertable::values((
                Some(::ExpressionMethods::eq(records::id, id)),
                Some(::ExpressionMethods::eq(records::record_type, record_type)),
                Some(::ExpressionMethods::eq(records::value, value))))
        }
    }
    
    impl diesel::query_builder::UndecoratedInsertRecord<records::table> for Record {
    }
    

    我们现在可以为我们的自定义类型调整 Insertable 实现。请注意,我已将 Values 关联类型更改为直接返回值而不是对值的引用,因为对于其中两个,值是在 values 方法中创建的,因此我们无法返回引用,并且对于另一个,返回引用并不会在性能方面获得太多收益。

    impl<'insert> diesel::insertable::Insertable<records::table> for &'insert Record {
        type Values = <(
            Option<diesel::dsl::Eq<records::id, i64>>,
            Option<diesel::dsl::Eq<records::record_type, i16>>,
            Option<diesel::dsl::Eq<records::value, BigDecimal>>
        ) as diesel::insertable::Insertable<records::table>>::Values;
    
        #[allow(non_shorthand_field_patterns)]
        fn values(self) -> Self::Values {
            let Record {
                id: ref id,
                record_type: ref record_type,
                value: ref value,
            } = *self;
            let record_type = match *record_type {
                RecordType::A => 1,
                RecordType::B => 2,
            };
            let value: BigDecimal = value.to_string().parse().unwrap();
            diesel::insertable::Insertable::values((
                Some(::ExpressionMethods::eq(records::id, *id)),
                Some(::ExpressionMethods::eq(records::record_type, record_type)),
                Some(::ExpressionMethods::eq(records::value, value))))
        }
    }
    

    【讨论】:

    • 感谢您的详细解释以及如何查看扩展代码的提示。仍然会接受 Shepmaster 的回答,因为我不想明确使用 Diesel 内部结构,这看起来很可怕。
    猜你喜欢
    • 2021-12-05
    • 2020-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-24
    • 1970-01-01
    相关资源
    最近更新 更多