Rust中基于Diesel实现通用CRUD泛型方案的技术咨询
Rust Diesel 泛型持久化层实现问题解答
方案可行性判断
这个泛型trait抽离默认CRUD实现的思路完全可行,不存在原则性问题,也不算过度设计——只要你项目里的关系型模型CRUD逻辑确实高度雷同,这个抽象的维护收益远大于实现成本,我自己在多个生产级Diesel项目里用过同类实现,只要控制好抽象边界,比到处复制粘贴样板代码的维护效率高很多。
Diesel本身的强类型设计天生支持这类泛型抽象,所有表、列、模型的约束都可以在编译期校验,不会引入任何运行时开销。实际落地的时候注意两个点就行:
- 不要为了抽象强行兼容所有特殊场景,先覆盖80%的通用CRUD逻辑,个别有自定义查询需求的模型直接重写对应trait方法即可,不用硬把特殊逻辑塞进通用实现里把trait搞的无比复杂。
- 如果你的关系型模型总数少于3个,抽这个trait的收益不高,可以等模型量上来、重复代码确实已经成为维护负担的时候再做,不用提前过度抽象。
get_table与get_id_column的类型定义
你在Diesel文档里找不到对应统一返回类型是正常的:Diesel中每张表、每一列都是独立的零大小类型,没有公共的具体类型可以直接作为返回值。如果一定要写方法返回值,可以用impl Trait写法,但这种方式很难在泛型场景下补全必要的 trait 约束,编译器错误提示也非常不友好。更符合Rust惯例的做法是在trait中定义关联类型,配合Diesel内置的trait做约束。
基于Diesel 2.x版本的通用实现参考如下:
use anyhow::Result; use diesel::backend::Backend; use diesel::prelude::*; use diesel::query_builder::AsQuery; // 按实际使用的后端替换连接类型,比如SqliteConnection/MysqlConnection pub type DbConnection = diesel::PgConnection; pub trait RelationalPersistence<Model, DB: Backend>: Persistence<Model> { /// 模型对应的数据表类型 type Table: Table + AsQuery; /// 数据表的ID列类型,必须关联到上面定义的Table type IdColumn: Column<Table = Self::Table> + ExpressionMethods; /// 获取表实例,所有Diesel表/列都是零大小类型,提供默认实现即可,无需手动编写 fn table() -> Self::Table { Self::Table::default() } /// 获取ID列实例 fn id_col() -> Self::IdColumn { Self::IdColumn::default() } /// 通用create默认实现 fn create(&self, model: &Model) -> Result<String> where // 约束模型可以插入到对应表 Model: Insertable<Self::Table>, // 约束ID列可以作为返回字段,查询结果能反序列化为String Self::IdColumn: SelectableExpression<Self::Table> + ValidGrouping<()>, // 约束最终生成的insert语句可以在当前连接上执行,返回String类型结果 diesel::dsl::Returning< diesel::insert_statement::InsertStatement<Self::Table, <Model as Insertable<Self::Table>>::Values>, Self::IdColumn >: LoadQuery<'static, DbConnection, String>, { let mut conn = crate::db::get_connection()?; let new_id = diesel::insert_into(Self::table()) .values(model) .returning(Self::id_col()) .get_result::<String>(&mut conn)?; Ok(new_id) } // 其余CRUD方法(get_by_id、list、update、delete等)都可以用同样的方式加约束写默认实现 }
具体模型的持久化实现会非常简洁,只需要指定两个关联类型即可,不需要写任何重复的CRUD逻辑:
use crate::schema::workspaces::{self, dsl::*}; use diesel::pg::Pg; struct WorkspaceRelationalPersistence; // 只需要指定关联的表和ID列,通用方法直接用默认实现 impl RelationalPersistence<Workspace, Pg> for WorkspaceRelationalPersistence { type Table = workspaces::table; type IdColumn = workspaces::id; } // 空impl即可满足上层Persistence trait的要求 impl Persistence<Workspace> for WorkspaceRelationalPersistence {}
几个落地的时候容易踩的坑:
- 表和列都是零大小类型,和持久化结构体实例没有关联,不要用
&self去调用对应的获取方法,直接用关联函数Self::table()即可,避免不必要的生命周期问题。 - 每个通用方法单独加where约束,不要把所有约束都堆到trait定义层,否则实现trait的时候会被不必要的约束卡住,比如某个模型不需要delete功能,就不用为了满足trait约束强行实现对应的逻辑。
- 如果你的模型ID不是String类型(比如自增i32、UUID等),可以把ID类型也做成关联类型,进一步提升通用性。
内容的提问来源于stack exchange,提问作者outside2344
相关产品推荐
相关产品推荐

