我正在努力使用 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。
注意:我确实不想想要定义两个具有相同字段的结构,并让它们各自继承一个特征,因为这是重复代码,对于更复杂的模式来说会变得不规则。
正如评论中提到的 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
,具有唯一的主键,并且无需手动管理键。