如何在 Rust 中使用 Diesel ORM 定义具有 AUTO_INCRMENT 列的可查询和可插入结构?

问题描述 投票:0回答:1

我正在努力使用 Diesel ORM 完成一项看似基本的任务。我正在尝试定义一个结构,对于一个简单的表,我可以插入(无需手动指定 id)和查询。

CREATE TABLE users(
    id INT UNSIGNED AUTO_INCREMENT NOT NULL,
    name TEXT NOT NULL,
    PRIMARY KEY(id),
);

为了能够调用 Diesel 的

insert_into
select
函数,我在
models.rs
中创建了以下模型:

#[derive(Queryable, Debug, Selectable, Insertable)]
#[diesel(table_name = super::schema::users)]
pub struct User {
    pub id: i32,
    pub name: String,
}

这会导致一个问题:插入时,我不能不指定

id
并依赖自动增量。在 Diesel 文档 中,提到插入时使用
Option<T>
并将字段设置为
None
是预期的工作流程。但是,这会导致错误,该字段应为
NotNull
Queryable
(这完全有道理):

error[E0271]: type mismatch resolving `<Integer as SqlType>::IsNull == IsNullable`
  --> src/database/models.rs:13:13
   |
13 |     pub id: Option<i32>,
   |             ^^^^^^ expected `NotNull`, found `IsNullable`
   |
   = note: required for `Option<i32>` to implement `Queryable<diesel::sql_types::Integer, Sqlite>`
   = note: required for `Option<i32>` to implement `FromSqlRow<diesel::sql_types::Integer, Sqlite>`
   = help: see issue #48214

有没有办法同时导出

Queryable
Insertable
,并具有自动递增
PRIMARY KEY
?如果这很重要,我正在使用 SQLite。

注意:我确实不想想要定义两个具有相同字段的结构,并让它们各自继承一个特征,因为这是重复代码,对于更复杂的模式来说会变得不规则。

sql sqlite rust orm rust-diesel
1个回答
0
投票

正如评论中提到的 GitHub 讨论中所述,答案是,由于库设计的原因,这不可能直接实现。相反,预期的工作流程是为

Insertable
Queryable
数据创建单独的类型,即使这会导致大部分类型重复。现已删除的 Diesel 可插入数据指南草案表示:

实现 Insertable 时,您可能不会设置行的自动递增 id 字段。通常您还会忽略

created_at
updated_at
等字段。因此,由于
Queryable
的字段数量限制,不建议在同一结构体上使用
Insertable
Queryable
。创建另一个可用于数据库插入的结构,其中包含您想要设置的所有字段。

如果您仍然希望两者具有相同的结构,我发现的最简单的解决方案是对

PRIMARY KEY
使用 GUID。由于 GUID 是 128 位,因此
BINARY(16)
是合适的数据类型,对应于 Rust 中的
Vec<u8>
。根据这个问题,这应该不会引入重大的性能问题。

CREATE TABLE users(
    id BINARY(16) NOT NULL,
    name TEXT NOT NULL,
    PRIMARY KEY(id),
);

由于完全随机的 GUID 不能像可高效索引的数据库键一样工作得很好,所以需要注意的一件事是,最好使用基于时间戳的键。 Cargo uuid 板条箱的

documentation
说道:

如果您想使用 UUID 作为数据库键或需要对它们进行排序,请考虑版本 7 (v7) UUID。

考虑到这一点,Rust 代码将如下所示:

/* models.rs */
#[derive(Queryable, Debug, Selectable, Insertable)]
pub struct User {
    pub id: Vec<u8>,
    pub name: String,
}

/* wherever you interact with the DB */
/// For readability
type DbGuid = Vec<u8>;

/// Insert, return id. We don't worry about auto increment etc., since every time uuid::Uuid::now_v7() is guaranteed to be unique
fn insert_user(db: &mut SqliteConnection, name: &str) -> Result<DbGuid> {
    let to_add = models::User {
        id: uuid::Uuid::now_v7().into(),
        name: name.into(),
    };

    let result: Vec<DbGuid> = diesel::insert_into(schema::users::table)
        .values(to_add)
        .returning(schema::users::id)
        .get_results(db)?;

   match result.len() {
        1 => Ok(result[0].clone()),
        0 => Err(anyhow!("No row id when inserting user {}", name)),
        _ => Err(anyhow!(">1 row when inserting user {}", name)),
    }
}

/// Query, get user struct if one exists or None
fn get_user_id(db: &mut SqliteConnection, name: &str) -> Result<Option<models::User>> {
    let result: Vec<models::User> = schema::users::table
        .filter(schema::users::name.eq(name))
        .select(models::User::as_select())
        .load(db)?;

    match result.len() {
        0 => Ok(None),
        1 => Ok(Some(result[0].clone())),
        _ => Err(anyhow!(">1 user id for name {}", name)),
    }
}

如您所见,您最终克隆了一个值,但这并不是世界末日。也许有人可以在评论中提出一种不这样做的方法,但这不是问题的范围。重要的是:这是一种可靠的方法,可以使结构同时具有

Insertable
Queryable
,具有唯一的主键,并且无需手动管理键。

© www.soinside.com 2019 - 2024. All rights reserved.