13个版本
0.1.20 | 2024年7月21日 |
---|---|
0.1.19 | 2024年7月21日 |
0.1.18 | 2024年5月18日 |
0.1.16 | 2024年4月13日 |
0.1.8 | 2023年6月29日 |
#1879 in 数据库接口
225 每月下载次数
用于 spaghettinuum
17KB
126 行
GOOD-ORMNING
Good-ormning可能是一个ORM,概括来说
- 在
build.rs
中定义模式和查询 - Good-ormning生成设置/迁移数据库的函数
- Good-ormning为每个查询生成函数
你为什么需要它
- 你想要从表定义到查询,跨版本的端到端类型安全
- 你希望在Rust中完成所有事情,你不想需要启动数据库并手动运行SQL
功能
- 无宏
- 无泛型
- 无特质(好吧,只有简单的特质来帮助自定义类型的实现 仅限)
- 无样板代码
- 自动迁移,无迁移-模式不匹配
- 查询参数类型检查 - 无运行时错误由于参数类型、数量或顺序
- 通过查询模拟进行查询逻辑类型检查
- 查询结果类型检查 - 无运行时错误由于结果类型、数量或顺序
- 生成速度快,运行时开销最小
与其他Rust ORM一样,Good-ormning不会从实际数据库工作流程中抽象出来,而是旨在通过正常的SQL增强类型检查。
请参阅下面的比较,了解Good-ormning与其他Rust ORM的不同之处。
当前状态
- 基本功能正常,这对我的基本用途来说足够了
- 中等测试覆盖率
- 缺少高级功能 - 如果您需要某些功能,请告诉我
- 一些易用性问题,接口可能在即将发布的版本中更改
支持数据库
- PostgreSQL(功能
pg
)通过tokio-postgres
- Sqlite(功能
sqlite
)通过rusqlite
入门
第一次
-
你需要以下运行时依赖项
good-ormning-runtime
tokio-postgres
用于PostgreSQLrusqlite
用于Sqlite
以及
build.rs
依赖项good-ormning
并且您必须启用(或多个)以下数据库功能之一
pg
sqlite
可能需要添加
chrono
以支持DateTime
。 -
创建一个
build.rs
文件,并定义您的初始架构版本和查询。 -
调用
goodormning::generate()
输出生成的代码。 -
在您的代码中,在创建数据库连接后,调用
migrate
。
架构变更
- 复制您的前一个版本架构,保持旧架构版本不变。根据需要修改新架构和查询。
- 将旧架构版本和新架构版本都传递给
goodormning::generate()
,它会生成新的迁移语句。 - 在运行时,
migrate
调用将确保数据库更新到新的架构版本。
示例
此 build.rs
文件
use std::{
path::PathBuf,
env,
};
use good_ormning::sqlite::{
Version,
schema::{
field::*,
constraint::*,
},
query::{
expr::*,
select::*,
},
*
};
fn main() {
println!("cargo:rerun-if-changed=build.rs");
let root = PathBuf::from(&env::var("CARGO_MANIFEST_DIR").unwrap());
let mut latest_version = Version::default();
let users = latest_version.table("zQLEK3CT0", "users");
let id = users.rowid_field(&mut latest_version, None);
let name = users.field(&mut latest_version, "zLQI9HQUQ", "name", field_str().build());
let points = users.field(&mut latest_version, "zLAPH3H29", "points", field_i64().build());
good_ormning::sqlite::generate(&root.join("tests/sqlite_gen_hello_world.rs"), vec![
// Versions
(0usize, latest_version)
], vec![
// Latest version queries
new_insert(&users, vec![(name.clone(), Expr::Param {
name: "name".into(),
type_: name.type_.type_.clone(),
}), (points.clone(), Expr::Param {
name: "points".into(),
type_: points.type_.type_.clone(),
})]).build_query("create_user", QueryResCount::None),
new_select(&users).where_(Expr::BinOp {
left: Box::new(Expr::Field(id.clone())),
op: BinOp::Equals,
right: Box::new(Expr::Param {
name: "id".into(),
type_: id.type_.type_.clone(),
}),
}).return_fields(&[&name, &points]).build_query("get_user", QueryResCount::One),
new_select(&users).return_field(&id).build_query("list_users", QueryResCount::Many)
]).unwrap();
}
生成类似的内容
pub fn migrate(db: &mut rusqlite::Connection) -> Result<(), GoodError> {
// ...
}
pub fn create_user(db: &mut rusqlite::Connection, name: &str, points: i64) -> Result<(), GoodError> {
// ...
}
pub struct DbRes1 {
pub name: String,
pub points: i64,
}
pub fn get_user(db: &mut rusqlite::Connection, id: i64) -> Result<DbRes1, GoodError> {
// ...
}
pub fn list_users(db: &mut rusqlite::Connection) -> Result<Vec<i64>, GoodError> {
// ...
}
并可用于
fn main() {
use sqlite_gen_hello_world as queries;
let mut db = rusqlite::Connection::open_in_memory().unwrap();
queries::migrate(&db).unwrap();
queries::create_user(&db, "rust human", 0).unwrap();
for user_id in queries::list_users(&db).unwrap() {
let user = queries::get_user(&db, user_id).unwrap();
println!("User {}: {}", user_id, user.name);
}
Ok(())
}
User 1: rust human
使用说明
功能
pg
- 启用为 PostgreSQL 生成代码sqlite
- 启用为 Sqlite 生成代码chrono
- 启用日期时间字段/表达式类型
架构 ID 和 ID
"架构 ID" 是用于匹配不同版本中字段的内部 ID,用于识别重命名、删除等。一旦在某个版本中使用,架构 ID 不应改变。建议使用通过键宏生成的随机 ID。更改架构 ID 将导致先删除后创建。
"ID" 适用于 SQL(字段)和 Rust(参数和返回的数据结构),因此必须在两者中都有效(尽管 Rust 中的 ID 如果与关键字冲突会自动进行一些修改)。根据数据库的不同,您可以在架构版本之间任意更改 ID,但连续版本之间的 ID 交换目前不支持 - 如果需要交换,请通过三个不同的版本进行(例如:v0
: A
和 B
,v1
: A_
和 B
,v2
: B
和 A
)。
查询、表达式和字段类型
使用 type_*
和 field_*
函数来获取用于表达式/字段的类型构建器。
使用 new_insert/
select/
update/
delete
创建查询构建器。
还有一些构建查询的辅助函数,请参阅
field_param
,匹配字段类型和名称的参数的快捷方式set_field
,在 INSERT 和 UPDATE 中设置字段值的快捷方式eq_field
、gt_field
、gte_field
、lt_field
、lte_field
是与相同类型参数比较字段的表达式的快捷方式expr_and
,AND 表达式的快捷方式
您使用的数据库。
自定义类型
在模式中定义字段时,请调用字段类型构建器上的.custom("mycrate::MyString", type_str().build())
(或者直接创建类型结构时,将其作为Some("mycreate::MyType".to_string())
传递)。
类型必须具有将自身转换为/从原生SQL类型转换的方法。存在一些特质来指导实现。
pub struct MyString(pub String);
impl good_ormning_runtime::pg::GoodOrmningCustomString<MyString> for MyString {
fn to_sql(value: &MyString) -> &str {
&value.0
}
fn from_sql(s: String) -> Result<MyString, String> {
Ok(Self(s))
}
}
方法
Expr::Call
变体允许您创建方法调用表达式。您必须在compute_type
中提供一个辅助方法来检查参数的类型并确定调用评估的类型。
第一个参数是评估上下文,其中包含用于报告错误的errs
。第二个是一个从评估树根到调用的路径,用于确定查询表达式中错误发生的位置。第三个参数是传递给调用的参数向量。每个参数可以是一个单一类型或由多个类型组成的记录(如()
中的where (x, y, z) < (b.x, b.y, b.z)
)。如果没有错误,则必须返回Some(...)
。
在表达式检查期间错误处理是懒惰的 - 即使发生错误,处理也可以继续(并在终止前识别更多错误)。
如果有错误,请将错误记录在ctx.errs.err(path.add(format!("参数 0")), format!("错误"))
中。如果调用中的评估无法继续,则返回None
,否则继续。
参数和返回类型
具有相同名称的参数会被去重 - 如果您定义了一个具有相同名称但不同类型的多个参数的查询,您将得到一个错误。
具有相同多字段返回的不同查询将使用相同的返回类型。
比较
与 Diesel 的比较
Good-ormning 在功能上与 Diesel 最相似。
Diesel
- 您可以在使用它们的地方附近定义查询和结果结构。
- 您可以动态定义查询(例如,根据输入交换运算符等)。
- 结果结构必须手动定义,并且必须注意字段顺序与查询相匹配。
- 您可以在模式中使用新类型,这些类型将在查询中进行检查,尽管这需要大量的模板代码。
- 需要许多宏和特质实现。
- 为了同步您的迁移和代码中的模式,您可以使用具有应用的迁移的实时数据库和CLI。但是,这会重置模式中的任何自定义SQL类型为内置SQL类型。或者,您也可以手动维护模式(但可能会因为输入错误或匹配错误而导致查询问题)。
- 列计数限制,构建时间慢。
- 支持更多语法,经得起时间的考验。
良好的ORM设计。
- 必须在
build.rs
文件中单独定义查询。 - 所有查询都必须在
build.rs
中预先定义。 - 您不必编写任何结构,所有内容都是从模式和查询信息生成的。
- 自定义类型可以无缝融入模式,无需模板代码。
- 通过模式版本之间的差异和额外的迁移元数据自动推导迁移。
- 由于没有宏和泛型,错误消息清晰。
- 代码生成速度快,编译简单生成的代码也快。
- alpha版。
与SQLx相比
SQLx
- SQLx没有模式的概念,因此它只能对原生SQL类型进行类型检查(不考虑新类型、blob编码等)。
- 开发期间需要运行数据库。
良好的ORM设计。
- 用于生成迁移的相同模式用于类型检查,并原生支持自定义类型。
- 开发期间未使用实时数据库,但所有查询语法必须在Good-ormning中手动实现,因此您可能会遇到缺少的功能。
与SeaORM相比
SeaORM侧重于运行时检查,而不是编译时检查。
关于未来的几句话
显然编写SQL虚拟机并不是一个好主意。理想的解决方案是让流行的数据库将它们的类型检查例程作为库公开,这样它们就可以导入到外部程序中,就像Go发布可重用的ast解析和类型检查库一样。
依赖项
~0–12MB
~142K SLoC