最近花精力写了一套全新的orm框架,包含了大量富有诚意的设计。项目在
https://github.com/thegenius/taitan-orm
发布之初,可能还有大量不足的地方,欢迎大家批评指正。
TAITAN-ORM 有什么特色?
[1] 精心设计的API
每个库作者都会认为自己设计的API是最优雅的/最精心设计的,如果连这个自信都没有,就不用写库了。毕竟API是每个库的脸面,脸面不好看,没有人愿意了解你的内核。所以把API放到第一个特色来说,是有一些冒险的。但有两个点给了我这个自信,一来是我设计一个全新的ORM库的最大动力就是其他库的API不好用,我花费了巨量的精力不断重构和设计API,现在库是0.1,API设计其实已经经历过至少3遍的蹂躏,这里的蹂躏是指完全的设计+实现+推翻重来。二来是后面会有show me the code环节,我相信所有看到真实代码的人或多或少都能感受到诚意。
事实上各种现有框架的API抽象都有很多可以改进的地方。这些需要改进的地方包括但不限于insert on duplicate key的支持,更新时正确的处理null和不更新,查询方法select只筛选必需的字段,避免select的dto爆炸问题,错误更加清晰直观等等。这一系列的问题的根因都指向API设计得不足够优美。下面介绍几个例子来展示一下taitan-orm中的API设计
// 这是创建操作
let entity = User {
id: 1,
name: "Allen".to_string(),
age: Optional::Some(23),
};
let result = db.insert(&entity).await?;
// 这是insert on duplicate key update操作
let entity = User {
id: 1,
name: "Allen".to_string(),
age: Optional::Some(23),
};
let result = db.upsert(&entity).await?;
// 这是更新操作
let mutation = UserMutation {
name: Optional::None, // 这里可以正确区分3种状态:设置/不设置/设置为null
age: Optional::Some(24),
};
//所有主键和唯一键都可以成为更新的where条件部分
let result = db.update(&mutation, &UserPrimary { id: 1 }).await?;
// 这里是查询操作
// 这里可以筛选所有需要的字段,底层保证了sql的最优
let selection = UserSelectedEntity::full_fields();
let entity: Option<UserSelectedEntity>
= db.select(&selection, &UserPrimary { id: 1 }).await?;
// 这里是搜索操作,更加复杂的例子可以参考项目的examples
let selection = UserSelectedEntity::full_fields();
let location = UserLocationExpr::id(">=", 1)?;
let entities: Vec<UserSelectedEntity>
= db.search(&selection, &location, &None, &None).await?;
[2] 漂亮的事务抽象
我对事务最大的心愿就是“像写非事务操作一样写事务操作”,但是事务操作天然就有非常多多和常规操作不一样的地方,真正要做到事务和非事务一样,需要花费大量的精力和编译器搏斗,天知道有多少次倒在了cannot borrow xxx
as mutable more than once at a time。吐完苦水后,我比较自豪的是,taitan-orm框架下事务操作和非事务操作的API是基本一致的。我们来看看taitan-orm中的事务:
async fn trx_insert_user(
db: &mut SqliteDatabase,
user1: &User,
user2: &User,
) -> taitan_orm::Result<()> {
let mut trx = db.transaction().await?;
trx.insert(user1).await?; // insert api和非事务是一致的
trx.insert(user2).await?; // 当异常触发时,trx默认行为是回滚
trx.commit().await?; // 所有操作都正常,trx应该正确提交
Ok(()) // trx在drop前没有commit,会自动rollback
}
[3] 支持模板
我在ORM领域是坚定的SQL党,从骨子里不认为ORM能够解决所有SQL问题,一个支持手写SQL的模板引擎是必需品,例如用来做存储过程调用(这里先别争论我在某些情况下为什么会用存储过程,问就是极致性能考量)。这套模板方案的性能是极致的,同时内存占用也是极致小的,因为从最底层就避免了使用map作为context来构建了渲染方案,同时由于很多解析过程是完全发生在compile time的,在run time就拥有了极致的性能。
#[derive(TemplateRecord, Debug)]
#[sql = "UPDATE `user` SET name = #{name} WHERE `id` = #{id}"]
pub struct UserUpdateTemplate {
id: i32,
name: String,
}
let template = UserUpdateTemplate {
id: 1,
name: "Bob".to_string(),
};
let affected_rows = db.execute_by_template(&template).await?;
[4] 异步化
由于构建在sqlx的基础上,taitan-orm是完全异步化的。异步化不仅仅是性能考量,事实上很多同步操作性能是好于异步的,但是异步的本质好处还是在于在绝大多数情况下可以不阻塞,这在高并发后端场景非常重要。但是良好的异步支持其实是复杂的,现在rust的异步支持其实是不太成熟的,写异步代码是困难的,尤其是想写出通用化的异步化代码更是难上加难,结合各种lifetime和trait,和编译器搏斗的过程就只能用酸爽来形容了。但这一切都是值得的,良好的异步支持会让用户在类似axum等项目中集成地十分轻松,下面就是一个集成到axum的示例:
// 这里最漂亮的是share_state不需要mutex保护,其实state需要mutex是很多高并发应用的卡点
// 为了这个share_state不需要mutex,我甚至完全推翻过一次API设计
let mut shared_state = Arc::new(
AppState::build_sqlite("./workspace", "test.db")
.await
unwrap(),
);
let app = Router::new()
.route("/user", post(create_user))
.with_state(shared_state.clone());
async fn create_user(
State(state): State<Arc<AppState>>,
Json(entity): Json<User>,
) -> impl IntoResponse {
let success = state.insert(&entity).await.unwrap();
format!("insert {}", success)
}
帖子篇幅有限,加之taitan-orm刚刚才开源出来,文档和说明都有大量不足,但还是斗胆向大家自荐这个项目,希望得到大家的指正,推动项目的发展,最终让rust社区拥有一个趋向于完美的ORM框架,再次求轻喷。最后说明taitan是泰坦
的拼音。
Ext Link: https://github.com/thegenius/taitan-orm
评论区
写评论不错,看起来比sea orm简洁
star了,支持一下。