如何为具有映射到多个柴油列的自定义字段的类型派生 Queryable?

Posted

技术标签:

【中文标题】如何为具有映射到多个柴油列的自定义字段的类型派生 Queryable?【英文标题】:How can I derive Queryable for a type with a custom field that maps to more than one column with diesel? 【发布时间】:2020-12-13 16:35:39 【问题描述】:

我正在使用Diesel crate 执行一些数据库工作。在某些表中,应将表的两列一起视为一个键。

这种模式在数据库中的许多地方都重复出现,因此最好避免使用大量重复的复制粘贴代码来处理这种情况。但是,我无法说服 Diesel 自动生成可以在查询或插入中使用的类型。

考虑表格

table! 
    records (iid) 
        iid -> Integer,
        id_0 -> BigInt,
        id_1 -> BigInt,
        data -> Text,
    

理想类型

#[derive(Debug, Copy, Clone, FromSqlRow)]
pub struct RecordId 
    id_0: i64,
    id_1: i64,


#[derive(Queryable, Debug)]
pub struct Record 
    pub iid: i32,
    pub id: RecordId,
    pub data: String,

这段代码编译正常,但是当我尝试使用它时出现错误,例如:

pub fn find(connection: &SqliteConnection) -> types::Record 
    records
        .find(1)
        .get_result::<types::Record>(connection)
        .unwrap()

产生:

error[E0277]: the trait bound `(i32, types::RecordId, std::string::String): diesel::Queryable<(diesel::sql_types::Integer, diesel::sql_types::BigInt, diesel::sql_types::BigInt, diesel::sql_types::Text), _>` is not satisfied
  --> src/main.rs:76:21
   |
76 |     records.find(1).get_result::<types::Record>(connection).unwrap()
   |                     ^^^^^^^^^^ the trait `diesel::Queryable<(diesel::sql_types::Integer, diesel::sql_types::BigInt, diesel::sql_types::BigInt, diesel::sql_types::Text), _>` is not implemented for `(i32, types::RecordId, std::string::String)`
   |
   = help: the following implementations were found:
             <(A, B, C) as diesel::Queryable<(SA, SB, SC), __DB>>
             <(A, B, C) as diesel::Queryable<diesel::sql_types::Record<(SA, SB, SC)>, diesel::pg::Pg>>
   = note: required because of the requirements on the impl of `diesel::Queryable<(diesel::sql_types::Integer, diesel::sql_types::BigInt, diesel::sql_types::BigInt, diesel::sql_types::Text), _>` for `types::Record`
   = note: required because of the requirements on the impl of `diesel::query_dsl::LoadQuery<_, types::Record>` for `diesel::query_builder::SelectStatement<types::records::table, diesel::query_builder::select_clause::DefaultSelectClause, diesel::query_builder::distinct_clause::NoDistinctClause, diesel::query_builder::where_clause::WhereClause<diesel::expression::operators::Eq<types::records::columns::iid, diesel::expression::bound::Bound<diesel::sql_types::Integer, i32>>>>`

如果我创建的版本不包含RecordId,但直接有子片段,则没有错误:

pub struct RecordDirect 
    pub iid: i32,
    pub id_0: i64,
    pub id_1: i64,
    pub data: String,


// ...

pub fn find_direct(connection: &SqliteConnection) -> types::RecordDirect 
    records
        .find(1)
        .get_result::<types::RecordDirect>(connection)
        .unwrap()

同样,我可以手动实现 Queryable 特征,它也可以正常工作,

#[derive(Debug)]
pub struct RecordManual 
    pub iid: i32,
    pub id: RecordId,
    pub data: String,


impl Queryable<records::SqlType, diesel::sqlite::Sqlite> for RecordManual 
    type Row = (i32, i64, i64, String);
    fn build(row: Self::Row) -> Self 
        RecordManual 
            iid: row.0,
            id: RecordId 
                id_0: row.1,
                id_1: row.2,
            ,
            data: row.3,
        
    


// ...

pub fn find_manual(connection: &SqliteConnection) -> types::RecordManual 
    records
        .find(1)
        .get_result::<types::RecordManual>(connection)
        .unwrap()

这种情况很难维护,我不知道如何让它为插入工作 - 手动实现 Insertable 似乎比 Queryable 有点棘手。

为了让任何看到它的人都能更轻松地使用它,我创建了一个存储库,其中包含一个几乎可以编译的小型复制器,其中包含这篇文章中的代码块。 (通常我会把它放在生锈的操场上,但这不支持柴油)。您可以在 https://github.com/mikeando/diesel_custom_type_demo 找到该代码。

有没有办法让#[derive(Queryable)](和#[derive(Insertable)])适用于这类情况?


初始失败案例的最小复制器是:

#[macro_use]
extern crate diesel;

use diesel::prelude::*;

mod types 
    use diesel::deserialize::Queryable;
    use diesel::sqlite::SqliteConnection;

    table! 
        records (iid) 
            iid -> Integer,
            id_0 -> BigInt,
            id_1 -> BigInt,
            data -> Text,
        
    

    #[derive(Debug, Copy, Clone, FromSqlRow)]
    pub struct RecordId 
        id_0: i64,
        id_1: i64,
    

    // Using a RecordId in a Record compiles, but 
    // produces an error when used in an actual query
    #[derive(Queryable, Debug)]
    pub struct Record 
        pub iid: i32,
        pub id: RecordId,
        pub data: String,
    


use types::records::dsl::*;

pub fn find(connection:&SqliteConnection) -> types::Record 
    records.find(1).get_result::<types::Record>(connection).unwrap()

【问题讨论】:

如果为RecordId 派生Queryable 会发生什么? @Jmb 同样的错误。 如果我使用显式列指定select 并在RecordId 上指定Queryable,并在select 中使用额外的括号,就像这样records.select((iid,(id_0,id_1),data)).find(1)... 它可以工作。 (正如github.com/diesel-rs/diesel/issues/1613 所建议的那样)。但这也不能很好地扩展。 IMO 手动实现它并没有那么复杂,它使您的代码在实现中看起来很简单,而不是在一些宏魔术中中继。 那么iid是主键,复合键(id_0, id_1)是另一个唯一的整数键?两者都需要吗? 【参考方案1】:

有没有办法让#[derive(Queryable)](和#[derive(Insertable)])适用于这类情况?

对于#[derive(Insertable)],只需将#[diesel(embedded)] 添加到id 字段并在两个结构上添加#[derive(Insertable)] 即可。有关详细信息,请参阅Insertable 的文档。 对于#[derive(Queryable)],这是不可能的,因为Queryable 应该是从查询结果到结构的普通映射,基本假设输出的“形状”保持不变(至少对于派生而言)。

【讨论】:

这似乎是真的,但对于Queryable 来说却令人失望。你真的可以像这样重用Insertable 结构吗?该文档指出:“派生此特征的结构也必须使用#[table_name = "some_table_name"] 进行注释”。如果它不能在多个地方重复使用,那么即使是 Insertable 的组合也不是特别有用。 派生要求您传入一个记录在案的表名。通过为泛型参数T 指定不同类型,此派生实现的特征允许为多个表实现。您可能想在这里为您的内部结构编写一个通用实现。有关详细信息,请参阅文档。

以上是关于如何为具有映射到多个柴油列的自定义字段的类型派生 Queryable?的主要内容,如果未能解决你的问题,请参考以下文章

如何为android中的所有视图使用相同的自定义字体?

NHibernate 映射 - 带有被重用列的用户类型?

如何为每个 wordpress 点击后创建单独的弹出表单

如何为数组中的每个创建一个具有唯一图像和选择器的自定义 UIButton?

如何为具有多种父类型的子场景编写 EF 代码优先映射

如何为具有不同图形和媒体资产的不同发送方应用程序实现相同的自定义接收方应用程序?