Как реализовать возможность запроса и вставку для настраиваемых типов полей в Diesel?

У меня есть таблица SQL, с которой я хочу работать через Diesel:

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,
    }
}

Дизель экспортирует десятичные числа как 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 связанный тип? Документация Дизеля по этому поводу не очень ясна.

Может быть, есть лучший способ добиться того, что я пытаюсь сделать?

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"

person hweom    schedule 04.03.2018    source источник


Ответы (2)


Мне удобнее создавать оболочки нового типа, которые реализуют 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,
    }
}

Существует черновик руководства, описывающий все производные и их аннотации, но еще не упоминается #[sql_type] для всего типа. Это позволяет Diesel узнать, какое основное хранилище необходимо внутри базы данных.

См. Также тесты Diesel для пользовательских типов.

person Shepmaster    schedule 04.03.2018
comment
Да, это было бы идеально, но, к сожалению, decimal :: d128 имеет ~ 30 методов и реализует ›50 трейтов, поэтому было бы очень неприятно реализовывать их для структуры оболочки. - person hweom; 04.03.2018
comment
@hweom вы можете добавить реализацию Deref, поэтому вам не нужно ее реализовывать. - person Neikos; 20.09.2020

Иногда самый простой способ понять, что делает макрос (производные - это просто другая форма макросов), - это запросить у компилятора расширенный код. С ночным компилятором вы можете сделать это с помощью этой команды:

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))))
    }
}
person Francis Gagné    schedule 04.03.2018
comment
Спасибо за подробное объяснение, а также за совет о том, как просматривать расширенный код. Все еще собираюсь принять ответ Шепмастера, так как я не хочу явно использовать внутренние компоненты Diesel, которые выглядят достаточно страшно. - person hweom; 18.03.2018