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 数据库接口

Download history 192/week @ 2024-05-12 104/week @ 2024-05-19 1/week @ 2024-05-26 2/week @ 2024-06-09 27/week @ 2024-06-30 220/week @ 2024-07-21 13/week @ 2024-07-28

每月233次下载
用于 spaghettinuum

ISC许可证

285KB
7.5K SLoC

GOOD-ORMNING

Good-ormning可能是一个ORM。简而言之

  1. build.rs中定义模式和查询
  2. Good-ormning生成设置/迁移数据库的函数
  3. Good-ormning为每个查询生成函数

为什么你需要它

  • 你需要端到端类型安全,从表定义到查询,跨版本
  • 你希望全部在Rust中完成,不希望需要启动数据库和手动运行SQL

特性

  • 无宏
  • 无泛型
  • 无特质(好吧,只有简单特质来帮助指导自定义类型的实现 仅限
  • 无样板代码
  • 自动迁移,无迁移-模式不匹配
  • 查询参数类型检查 - 无运行时错误由于参数类型、计数或顺序
  • 通过查询模拟进行查询逻辑类型检查
  • 查询结果类型检查 - 无运行时错误由于结果类型、计数或顺序
  • 生成速度快,运行时开销最小

与其他Rust ORM一样,Good-ormning不会抽象实际数据库工作流程,而是旨在通过常规SQL增强类型检查。

下面是与其他Rust ORM的区别信息。

当前状态

  • 基本功能正常,这适用于我的基本用途
  • 中等测试覆盖率
  • 缺少高级功能 - 如果您需要某些功能,请告诉我
  • 一些可用性问题,接口可能在即将发布的版本中更改

支持的数据库

  • PostgreSQL(功能 pg)通过 tokio-postgres
  • Sqlite(功能 sqlite)通过 rusqlite

入门指南

第一次

  1. 您需要以下运行时依赖项

    • good-ormning-runtime
    • tokio-postgres 用于 PostgreSQL
    • rusqlite 用于 Sqlite

    以及 build.rs 依赖项

    • good-ormning

    您必须启用一个(或多个)数据库功能

    • PostgreSQL
    • SQLite

    可能还需要chrono来支持DateTime

  2. 创建一个build.rs文件,并定义您的初始模式版本和查询

  3. 调用goodormning::generate()来输出生成的代码

  4. 在您的代码中,在创建数据库连接后,调用migrate

模式更改

  1. 复制您之前的版本模式,保留旧模式版本不变。根据需要修改新模式和查询。
  2. 将旧模式和新的模式版本都传递给goodormning::generate(),它将生成新的迁移语句。
  3. 在运行时,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 - 如果需要进行交换,请在三个不同的版本中进行(例如:v0ABv1A_Bv2BA)。

查询、表达式和字段类型

使用type_*field_*函数来获取用于表达式/字段的类型构建器。

使用new_insert/select/update/delete来创建查询构建器。

还有一些用于构建查询的辅助函数,请参阅

  • field_param,匹配字段类型和名称的参数的快捷方式
  • set_field,在INSERT和UPDATE中设置字段值的快捷方式
  • eq_fieldgt_fieldgte_fieldlt_fieldlte_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