Как объединить несколько функций с помощью Diesel в одну посредством абстракции?

У меня две следующие функции:

pub fn get_most_recent_eth_entry(conn: &SqliteConnection) -> Result<i32, Error> {
    let res = types::ethereum::table
        .order(types::ethereum::time.desc())
        .limit(1)
        .load::<types::ETHRecord>(&*conn);
    match res {
        Ok(x) => {
            if x.len() > 0 {
                Ok(x.get(0).unwrap().time)
            } else {
                Ok(0)
            }
        }
        Err(err) => Err(format_err!("Error here! {:?}", err)),
    }
}

pub fn get_most_recent_btc_entry(conn: &SqliteConnection) -> Result<i32, Error> {
    let res = types::bitcoin::table
        .order(types::bitcoin::time.desc())
        .limit(1)
        .load::<types::BTCRecord>(&*conn);
    match res {
        Ok(x) => {
            if x.len() > 0 {
                Ok(x.get(0).unwrap().time)
            } else {
                Ok(0)
            }
        }
        Err(err) => Err(format_err!("Error here! {:?}", err)),
    }
}

Я хочу объединить оба в одну функцию. Я пробовал несколько разных способов, но

  1. Я новичок в Rust
  2. У Дизеля есть странные типы (или, по крайней мере, так кажется)

Какими способами можно объединить эти две функции (которые отличаются только полями types::ethereum и ETHRecord в одну объединенную функцию get_most_recent_entry?

Это мои определения структуры базы данных (эквивалентно определены схемы SQL):

#[derive(Insertable, Queryable, Debug)]
#[table_name="bitcoin"]
pub struct BTCRecord {
    pub time: i32,
    pub market_cap: f32,
    pub price_btc: f32,
    pub price_usd: f32,
    pub vol_usd: f32,
}

и тип

`types::ethereum::time` is `database::types::__diesel_infer_schema::infer_bitcoin::bitcoin::columns::time`

и тип

`types::ethereum::table` is
`database::types::__diesel_infer_schema::infer_bitcoin::bitcoin::table`

person DaveTheAl    schedule 18.12.2017    source источник
comment
Чтобы получить какой-либо полезный ответ, вам нужно будет включить достаточное количество модуля схемы, чтобы мы знали, что есть bitcoin, ethereum и соответствующие типы.   -  person Shepmaster    schedule 18.12.2017
comment
Я могу сказать, что в настоящее время вы не можете абстрагироваться от полей структуры (например, .time); это нужно будет переместить в метод признака.   -  person Shepmaster    schedule 18.12.2017
comment
Спасибо @Shepmaster, добавил еще немного информации :) Но я действительно не понимаю, что вы имеете в виду, говоря о переносе .time в метод трейда? В конце концов, у метода все равно должен быть тип возврата, и этот тип возврата отличается от таблицы к таблице, или я что-то упускаю?   -  person DaveTheAl    schedule 18.12.2017


Ответы (1)


Во-первых, давайте начнем с MCVE. Это инструмент, который используют профессиональные программисты, пытаясь понять проблему. Он удаляет лишние детали, но предоставляет достаточно деталей, чтобы любой мог уловить их и воспроизвести ситуацию. Сравните, сколько здесь кода, который вы не предоставили. Каждый недостающий фрагмент - это то, о чем отвечающий должен догадаться, а также ваше время и его время, которое они генерируют.

[dependencies]
diesel = { version = "1.0.0-beta", features = ["sqlite"] }
#[macro_use]
extern crate diesel;

use diesel::prelude::*;
use diesel::SqliteConnection;

mod types {
    table! {
        bitcoin (time) {
            time -> Int4,
        }
    }

    table! {
        ethereum (time) {
            time -> Int4,
        }
    }

    #[derive(Insertable, Queryable, Debug)]
    #[table_name="bitcoin"]
    pub struct BtcRecord {
        pub time: i32,
    }

    #[derive(Insertable, Queryable, Debug)]
    #[table_name="ethereum"]
    pub struct EthRecord {
        pub time: i32,
    }
}

pub fn get_most_recent_eth_entry(conn: &SqliteConnection) -> Result<i32, String> {
    let res = types::ethereum::table
        .order(types::ethereum::time.desc())
        .limit(1)
        .load::<types::EthRecord>(&*conn);
    match res {
        Ok(x) => {
            if x.len() > 0 {
                Ok(x.get(0).unwrap().time)
            } else {
                Ok(0)
            }
        }
        Err(err) => Err(format!("Error here! {:?}", err)),
    }
}

pub fn get_most_recent_btc_entry(conn: &SqliteConnection) -> Result<i32, String> {
    let res = types::bitcoin::table
        .order(types::bitcoin::time.desc())
        .limit(1)
        .load::<types::BtcRecord>(&*conn);
    match res {
        Ok(x) => {
            if x.len() > 0 {
                Ok(x.get(0).unwrap().time)
            } else {
                Ok(0)
            }
        }
        Err(err) => Err(format!("Error here! {:?}", err)),
    }
}

Затем выполните различие между двумя частями кода, чтобы выявить различия. Вы заявили:

которые отличаются только полями types::ethereum и ETHRecord

Однако они различаются по четырем расположениям. Тот факт, что у чего-то есть такой же префикс, не означает, что вы можете передавать этот префикс. Модули - это не концепции, существующие во время выполнения в Rust:

pub fn get_most_recent_eth_entry(conn: &SqliteConnection) -> Result<i32, String> {
    // ^^^^^^^^^^^^^^^^^^^^^^^^^
    let res = types::ethereum::table
    //               ^^^^^^^^ 
        .order(types::ethereum::time.desc())
    //                ^^^^^^^^ 
        .limit(1)
        .load::<types::EthRecord>(&*conn);
    //                 ^^^^^^^^^

Скопируем и вставим одну из функций и заменим все уникальные значения на фиктивные:

pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
    conn: &SqliteConnection,
    table: Tbl,
    time: Expr,
) -> Result<i32, String> {
    let res = table
        .order(time.desc())
        .limit(1)
        .load::<Record>(&*conn);
    // ...

Следующая часть некрасива. По сути, компилятор будет сообщать вам каждую границу признака, которая не соблюдается, одно за другим. Вы «просто» копируете каждую ошибку обратно в код, чтобы установить все ограничения:

pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
    conn: &SqliteConnection,
    table: Tbl,
    time: Expr,
) -> Result<i32, String>
where
    Expr: diesel::ExpressionMethods,
    Tbl: OrderDsl<Desc<Expr>>,
    <Tbl as OrderDsl<Desc<Expr>>>::Output: LimitDsl,
    <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: RunQueryDsl<SqliteConnection> + Query,
    Sqlite: HasSqlType<<<<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output as Query>::SqlType>,
    <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: QueryFragment<Sqlite>,
    <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: QueryId,
    Record: Queryable<<<<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output as Query>::SqlType, Sqlite>,

Это приводит к новой ошибке:

error[E0609]: no field `time` on type `&Record`
  --> src/main.rs:64:38
   |
64 |                 Ok(x.get(0).unwrap().time)
   |                                      ^^^^

Вы не можете предполагать какие-либо поля в универсальном типе, нам нужна черта:

pub trait Time {
    fn time(&self) -> i32;
}

Ты:

  • реализовать трейт для обоих конкретных типов
  • добавить эту черту, привязанную к Record
  • вызов .time() в методе

Все вместе:

#[macro_use]
extern crate diesel;

use diesel::prelude::*;
use diesel::SqliteConnection;

mod types {
    table! {
        bitcoin (time) {
            time -> Int4,
        }
    }

    table! {
        ethereum (time) {
            time -> Int4,
        }
    }

    #[derive(Insertable, Queryable, Debug)]
    #[table_name = "bitcoin"]
    pub struct BtcRecord {
        pub time: i32,
    }

    #[derive(Insertable, Queryable, Debug)]
    #[table_name = "ethereum"]
    pub struct EthRecord {
        pub time: i32,
    }
}

pub trait Time {
    fn time(&self) -> i32;
}

impl Time for types::EthRecord {
    fn time(&self) -> i32 {
        self.time
    }
}

impl Time for types::BtcRecord {
    fn time(&self) -> i32 {
        self.time
    }
}

use diesel::sqlite::Sqlite;
use diesel::types::HasSqlType;
use diesel::query_dsl::methods::{LimitDsl, OrderDsl};
use diesel::expression::operators::Desc;
use diesel::query_builder::{Query, QueryFragment, QueryId};
use diesel::Queryable;

pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
    conn: &SqliteConnection,
    table: Tbl,
    time: Expr,
) -> Result<i32, String>
where
    Expr: diesel::ExpressionMethods,
    Tbl: OrderDsl<Desc<Expr>>,
    <Tbl as OrderDsl<Desc<Expr>>>::Output: LimitDsl,
    <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: RunQueryDsl<SqliteConnection> + Query,
    Sqlite: HasSqlType<<<<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output as Query>::SqlType>,
    <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: QueryFragment<Sqlite>,
    <<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output: QueryId,
    Record: Queryable<<<<Tbl as OrderDsl<Desc<Expr>>>::Output as LimitDsl>::Output as Query>::SqlType, Sqlite> + Time,
{
    let res = table.order(time.desc()).limit(1).load::<Record>(&*conn);
    match res {
        Ok(x) => {
            if x.len() > 0 {
                Ok(x.get(0).unwrap().time())
            } else {
                Ok(0)
            }
        }
        Err(err) => Err(format!("Error here! {:?}", err)),
    }
}

pub fn get_most_recent_eth_entry(conn: &SqliteConnection) -> Result<i32, String> {
    get_most_recent_entry::<_, _, types::EthRecord>(
        conn,
        types::ethereum::table,
        types::ethereum::time,
    )
}

pub fn get_most_recent_btc_entry(conn: &SqliteConnection) -> Result<i32, String> {
    get_most_recent_entry::<_, _, types::BtcRecord>(
        conn,
        types::bitcoin::table,
        types::bitcoin::time,
    )
}

Следующие шаги потребуют более глубокого погружения в Дизель. Модуль helper_types содержит псевдонимы типов, позволяющие сократить границы:

pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
    conn: &SqliteConnection,
    table: Tbl,
    time: Expr,
) -> Result<i32, String>
where
    Expr: diesel::ExpressionMethods,
    Tbl: OrderDsl<Desc<Expr>>,
    Order<Tbl, Desc<Expr>>: LimitDsl,
    Limit<Order<Tbl, Desc<Expr>>>: RunQueryDsl<SqliteConnection>
        + Query
        + QueryFragment<Sqlite>
        + QueryId,
    Sqlite: HasSqlType<<Limit<Order<Tbl, Desc<Expr>>> as Query>::SqlType>,
    Record: Queryable<<Limit<Order<Tbl, Desc<Expr>>> as Query>::SqlType, Sqlite> + Time,

Также есть черта, которая объединяет все связанные с Query* подтексты: _16 _ . Используя это, мы можем уменьшить его до:

pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
    conn: &SqliteConnection,
    table: Tbl,
    time: Expr,
) -> Result<i32, String>
where
    Expr: diesel::ExpressionMethods,
    Tbl: OrderDsl<Desc<Expr>>,
    Order<Tbl, Desc<Expr>>: LimitDsl,
    Limit<Order<Tbl, Desc<Expr>>>: LoadQuery<SqliteConnection, Record>,
    Record: Time,

Затем вы можете использовать функцию Diesel first и комбинаторы Results, чтобы сократить всю функцию:

use diesel::expression::operators::Desc;
use diesel::helper_types::{Limit, Order};
use diesel::query_dsl::methods::{LimitDsl, OrderDsl};
use diesel::query_dsl::LoadQuery;

pub fn get_most_recent_entry<'a, Tbl, Expr, Record>(
    conn: &SqliteConnection,
    table: Tbl,
    time: Expr,
) -> Result<i32, String>
where
    Expr: diesel::ExpressionMethods,
    Tbl: OrderDsl<Desc<Expr>>,
    Order<Tbl, Desc<Expr>>: LoadQuery<SqliteConnection, Record> + LimitDsl,
    Limit<Order<Tbl, Desc<Expr>>>: LoadQuery<SqliteConnection, Record>,
    Record: Time,
{
    table
        .order(time.desc())
        .first(conn)
        .optional()
        .map(|x| x.map_or(0, |x| x.time()))
        .map_err(|e| format!("Error here! {:?}", e))
}
person Shepmaster    schedule 19.12.2017