如何通过抽象将使用 Diesel 的多个功能合二为一?

Dav*_*eAl 1 generics types traits rust rust-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)),
    }
}
Run Code Online (Sandbox Code Playgroud)

我想将两者合并为一个功能。我尝试了几种不同的方法,但是

  1. 我对 Rust 很陌生
  2. 柴油有奇怪的类型(或者至少这就是它的感觉)

有哪些方法可以合并这两个功能(仅在字段上不同types::ethereumETHRecord合并为一个统一功能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,
}
Run Code Online (Sandbox Code Playgroud)

和类型

`types::ethereum::time` is `database::types::__diesel_infer_schema::infer_bitcoin::bitcoin::columns::time`
Run Code Online (Sandbox Code Playgroud)

和类型

`types::ethereum::table` is
`database::types::__diesel_infer_schema::infer_bitcoin::bitcoin::table`
Run Code Online (Sandbox Code Playgroud)

She*_*ter 7

首先,让我们从MCVE开始。这是专业程序员在尝试理解问题时使用的工具。它删除了无关的细节,但提供了足够的细节,让任何人都能够捡起它并重现这种情况。比较这里有多少您没有提供的代码。每个缺失的部分都是回答者必须猜测的东西,以及你的时间和他们产生的时间。

[dependencies]
diesel = { version = "1.0.0-beta", features = ["sqlite"] }
Run Code Online (Sandbox Code Playgroud)
#[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)),
    }
}
Run Code Online (Sandbox Code Playgroud)

接下来,在两段代码之间执行差异以识别差异。你说:

仅在领域types::ethereumETHRecord

但是,它们在四个位置有所不同。仅仅因为某些东西具有相同的前缀并不意味着您可以传递该前缀。模块不是 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);
    //                 ^^^^^^^^^
Run Code Online (Sandbox Code Playgroud)

让我们复制并粘贴其中一个函数,并用哑元替换所有唯一值:

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);
    // ...
Run Code Online (Sandbox Code Playgroud)

下一部分并不漂亮。基本上,编译器会一一告诉您每个未满足的特征界限。您“只是”将每个错误复制回代码以设置所有约束:

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>,
Run Code Online (Sandbox Code Playgroud)

这导致了新的错误:

[dependencies]
diesel = { version = "1.0.0-beta", features = ["sqlite"] }
Run Code Online (Sandbox Code Playgroud)

您不能假设泛型类型的任何字段,我们需要一个特征:

pub trait Time {
    fn time(&self) -> i32;
}
Run Code Online (Sandbox Code Playgroud)

你:

  • 为两种具体类型实现 trait
  • 将此特征添加到 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,
    )
}
Run Code Online (Sandbox Code Playgroud)

接下来的步骤需要更深入地研究 Diesel。该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,
Run Code Online (Sandbox Code Playgroud)

还有一个 trait 包含了所有Query*-related subtraits: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>>: LimitDsl,
    Limit<Order<Tbl, Desc<Expr>>>: LoadQuery<SqliteConnection, Record>,
    Record: Time,
Run Code Online (Sandbox Code Playgroud)

然后,您可以使用 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))
}
Run Code Online (Sandbox Code Playgroud)