如何在 Diesel 中为自定义字段类型实现可查询和可插入?
How do I implement Queryable and Insertable for custom field types in Diesel?
我有一个 SQL table,我想通过 Diesel 使用它:
CREATE TABLE records (
id BIGSERIAL PRIMARY KEY,
record_type SMALLINT NOT NULL,
value DECIMAL(10, 10) NOT NULL
)
此 table 生成以下架构:
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)),
}
}
}
我不知道如何实施 Insertable
。 Values
关联类型是什么? 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"
有时,了解宏功能的最简单方法(派生只是宏的一种不同形式)是向编译器询问扩展代码。使用夜间编译器,您可以使用以下命令执行此操作:
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
关联类型直接更改为 return 值,而不是对值的引用,因为其中两个值是在 values
方法中创建的,因此我们不能 return 参考,而对于另一个,return参考在性能方面并没有太大的提高。
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))))
}
}
我发现创建实现 ToSql
和 FromSql
的新型包装器更方便。然后,您可以使用这些基本块构建更大的类型,这些类型可以派生 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 知道数据库内部需要什么样的底层存储。
我有一个 SQL table,我想通过 Diesel 使用它:
CREATE TABLE records (
id BIGSERIAL PRIMARY KEY,
record_type SMALLINT NOT NULL,
value DECIMAL(10, 10) NOT NULL
)
此 table 生成以下架构:
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)),
}
}
}
我不知道如何实施 Insertable
。 Values
关联类型是什么? 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"
有时,了解宏功能的最简单方法(派生只是宏的一种不同形式)是向编译器询问扩展代码。使用夜间编译器,您可以使用以下命令执行此操作:
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
关联类型直接更改为 return 值,而不是对值的引用,因为其中两个值是在 values
方法中创建的,因此我们不能 return 参考,而对于另一个,return参考在性能方面并没有太大的提高。
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))))
}
}
我发现创建实现 ToSql
和 FromSql
的新型包装器更方便。然后,您可以使用这些基本块构建更大的类型,这些类型可以派生 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 知道数据库内部需要什么样的底层存储。