< 返回版块

thegenius 发表于 2025-01-04 22:53

Tags:orm

最近花精力写了一套全新的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

评论区

写评论
miaomiao1992 2025-01-05 17:22

不错,看起来比sea orm简洁

Happy-Feet-hhh 2025-01-05 09:20

star了,支持一下。

1 共 2 条评论, 1 页