21个版本
0.1.20 | 2024年7月21日 |
---|---|
0.1.18 | 2024年5月18日 |
0.1.15 | 2023年12月9日 |
0.1.12 | 2023年11月19日 |
0.1.6 | 2023年3月1日 |
#137 in 数据库接口
每月233次下载
用于 spaghettinuum
285KB
7.5K SLoC
GOOD-ORMNING
Good-ormning可能是一个ORM。简而言之
- 在
build.rs
中定义模式和查询 - Good-ormning生成设置/迁移数据库的函数
- Good-ormning为每个查询生成函数
为什么你需要它
- 你需要端到端类型安全,从表定义到查询,跨版本
- 你希望全部在Rust中完成,不希望需要启动数据库和手动运行SQL
特性
- 无宏
- 无泛型
- 无特质(好吧,只有简单特质来帮助指导自定义类型的实现 仅限)
- 无样板代码
- 自动迁移,无迁移-模式不匹配
- 查询参数类型检查 - 无运行时错误由于参数类型、计数或顺序
- 通过查询模拟进行查询逻辑类型检查
- 查询结果类型检查 - 无运行时错误由于结果类型、计数或顺序
- 生成速度快,运行时开销最小
与其他Rust ORM一样,Good-ormning不会抽象实际数据库工作流程,而是旨在通过常规SQL增强类型检查。
下面是与其他Rust ORM的区别信息。
当前状态
- 基本功能正常,这适用于我的基本用途
- 中等测试覆盖率
- 缺少高级功能 - 如果您需要某些功能,请告诉我
- 一些可用性问题,接口可能在即将发布的版本中更改
支持的数据库
- PostgreSQL(功能
pg
)通过tokio-postgres
- Sqlite(功能
sqlite
)通过rusqlite
入门指南
第一次
-
您需要以下运行时依赖项
good-ormning-runtime
tokio-postgres
用于 PostgreSQLrusqlite
用于 Sqlite
以及
build.rs
依赖项good-ormning
您必须启用一个(或多个)数据库功能
PostgreSQL
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类型。或者,您可以手动维护模式(但可能因输入错误或匹配错误而导致查询问题)。
- 列数限制,构建时间慢
- 支持更多语法,经得起时间的考验
良好的ormning
- 查询必须在
build.rs
文件中单独定义 - 所有查询都必须在
build.rs
中提前定义 - 不需要编写任何结构,所有内容都是从模式和查询信息生成的
- 自定义类型可以无模板地集成到模式中
- 通过模式版本之间的差异和额外的迁移元数据自动推导迁移
- 由于没有宏和泛型,错误信息清晰
- 代码生成速度快,编译简单生成的代码也快
- Alpha
与SQLx比较
SQLx
- SQLx没有模式的概念,因此它只能对本地SQL类型执行类型检查(不考虑新类型、blob编码等)
- 开发期间需要运行数据库
良好的ormning
- 用于生成迁移的相同模式用于类型检查,并原生支持自定义类型
- 开发期间不使用实时数据库,但所有查询语法必须在Good-ormning中手动实现,因此您可能会遇到缺少的功能
与SeaORM比较
SeaORM侧重于运行时检查而不是编译时检查。
关于未来的几点想法
显然编写SQL虚拟机不是很好。理想的解决方案是让流行的数据库将其类型检查例程作为库公开,以便它们可以被导入到外部程序中,就像Go发布可重用的ast解析和类型检查库一样。
依赖关系
~14–25MB
~407K SLoC