#orm #sql-database #migration #sql-query #sqlite #database-migrations #love

app ormlitex-cli

为热爱 SQL 的人设计的 ORM。使用 ormlitex crate,而不是这个。

1 个不稳定版本

0.17.1 2023年9月4日

#2961 in 数据库接口

MIT 许可证

110KB
2.5K SLoC

GitHub Contributors Stars Build Status Downloads Crates.io

ormlite

ormlite 是一个面向热爱 SQL 的开发者的 Rust ORM。 让我们看看它的实际应用。

use ormlite::model::*;
use ormlite::sqlite::SqliteConnection;

#[derive(Model, Debug)]
pub struct Person {
    pub id: i32,
    pub name: String,
    pub age: i32,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// Start by making a database connection.
    let mut conn = SqliteConnection::connect(":memory:").await.unwrap();

    /// You can insert the model directly.
    let mut john = Person {
        id: 1,
        name: "John".to_string(),
        age: 99,
    }.insert(&mut conn).await?;

    println!("{:?}", john);

    /// After modifying the object, you can update all its fields.
    john.age += 1;
    john.update_all_fields(&mut conn).await?;

    /// Query builder syntax closely follows SQL syntax, translated into chained function calls.
    let people = Person::select()
        .where_("age > ?").bind(50)
        .fetch_all(&mut conn).await?;

    println!("{:?}", people);
}

你可能喜欢 ormlite 因为

  • 它可以从 Rust 结构体自动生成迁移。据我所知,这是唯一具有此功能的 Rust ORM。
  • 连接 API(处于 alpha 版)的移动部件比任何其他 Rust ORM 都要少。它只依赖于表 struct 本身,不依赖于关系特性和模块(SeaORM 或 Diesel)。
  • 几乎没有查询构建器语法需要学习。查询构建器基本上是将原始 SQL 的 &str 片段连接起来。它在可组合性和几乎零学习曲线之间找到了正确的抽象层次,这对于已经了解 SQL 的人来说是非常合适的。

快速入门

安装

使用 cargo 安装

# For postgres
cargo add ormlite --features postgres
# For sqlite
cargo add ormlite --features sqlite

或者更新你的 Cargo.toml

[dependencies]
# For postgres
ormlite = { version = "..", features = ["postgres"] }
# For sqlite
ormlite = { version = "..", features = ["sqlite"] }

支持其他数据库和运行时,但测试较少。如果你遇到任何问题,请提交问题。

环境设置

你需要在环境中设置 DATABASE_URL。我们推荐使用像 just 这样的工具,它可以引入一个 .env 文件,但为了简单起见,这里我们将直接使用 shell。

export DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres

迁移

如果你正在查询静态数据库且不需要迁移,请跳过本节。如果你需要迁移,请继续阅读。

首先,安装 ormlite-cli。目前,CLI 只支持 Postgres。虽然 ormlite-clisqlx-cli 是分开的,但它们是完全兼容的。sqlx-cli 不支持自动生成迁移或快照(在开发中无需编写迁移即可回滚),但它不如 ormlite-cli 先进,支持更多数据库类型。

cargo install ormlite-cli

接下来,创建数据库和迁移表。使用init命令会创建一个_sqlx_migrations表,用于跟踪你的迁移。

# Create the database if it doesn't exist. For postgres, that's:
# createdb <dbname>
ormlite init

让我们看看迁移是如何工作的。创建一个带有#[derive(Model)]的Rust结构体,CLI工具会检测到它来自动生成迁移。

# src/models.rs

use ormlite::model::*;

#[derive(Model, Debug)]
pub struct Person {
    pub id: i32,
    pub name: String,
    pub age: i32,
}

接下来,自动生成迁移。

ormlite migrate initial

这会在migrations/目录下创建一个普通的SQL文件。在我们执行它之前,让我们先查看一下。

cat migrations/*.sql

在查看满意之后,你可以执行它。

ormlite up

默认情况下,up也会创建一个快照,所以如果需要,你可以使用ormlite down来回滚。你也可以选择生成成对的up和down迁移,而不仅仅是up迁移。

设置完成。现在让我们看看如何运行查询。

插入和更新

README顶部提供的插入和更新语法对UUID主键表最为有效。

use ormlite::model::*;
use uuid::Uuid;

#[derive(Model, Debug)]
pub struct Event {
    pub id: Uuid,
    pub name: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut conn = ormlite::sqlite::SqliteConnection::connect(":memory:").await.unwrap();

    let mut event = Event {
        id: Uuid::new_v4(),
        name: "user_clicked".to_string(),
    }.insert(&mut conn).await?;

    println!("{:?}", event);
}

这个语法可能存在两个问题。首先,id不是Option类型,所以它必须被设置,这可能会导致自增id字段出现问题。其次,结构体无法跟踪哪些字段被修改,因此更新方法必须更新所有列。

插入结构体

为了解决自增问题,你可以使用这里展示的插入结构体,或者下面的构建器。

use ormlite::types::Json;
use serde_json::Value;

#[derive(Model, Debug)]
#[ormlite(insertable = InsertPerson)]
pub struct Person {
    pub id: i32,
    // Because the other fields are the primary key, and marked as default and default_value respectively,
    // `name` is the only field in the InsertPerson struct.
    pub name: String,
    // This field will not be part of the InsertPerson struct,
    // and rows will take the database-level default upon insertion.
    #[ormlite(default)]
    pub archived_at: Option<DateTime<Utc>>,
    // This field will not be part of the InsertPerson struct, 
    // which will always pass the provided value when inserting.
    #[ormlite(default_value = "serde_json::json!({})")]
    pub metadata: Json<Value>,
}

async fn insertion_struct_example(conn: &mut SqliteConnection) {  
    let john: Person = InsertPerson {
        name: "John".to_string(),
    }.insert(&mut conn).await?;

    println!("{:?}", john);
}

如果派生的结构体不符合你的需求,你可以手动定义一个只包含你想要的字段的struct,并指定table = "<table>"来将struct路由到相同的数据库表。

#[derive(Model, Debug)]
#[ormlite(table = "person")]
pub struct InsertPerson {
    pub name: String,
    pub age: i32,
}

插入构建器和更新构建器

你也可以使用构建器语法来进行插入或仅更新某些字段。

#[derive(Model, Debug)]
pub struct Person {
    pub id: i32,
    pub name: String,
    pub age: i32,
}

async fn builder_syntax_example() {    
    // builder syntax for insert
    let john = Person::builder()
        .name("John".to_string())
        .age(99)
        .insert(&mut conn).await?;

    println!("{:?}", john);

    // builder syntax for update
    let john = john.update_partial()
        .age(100)
        .update(&mut conn).await?;

    println!("{:?}", john);
}

选择查询

你可以使用Model::select来使用Rust逻辑构建一个SQL查询。

注意:Postgres使用编号美元符号占位符构建查询的方法在构建查询时很快就会失效。因此,即使是Postgres,你也可以使用?作为参数,ormlite将在构建最终查询时将?占位符替换为$占位符。

#[derive(Model, Debug)]
pub struct Person {
    pub id: i32,
    pub name: String,
    pub age: i32,
}

async fn query_builder_example() {
    let people = Person::select()
        .where_("age > ?")
        .bind(50i32)
        .fetch_all(&mut conn)
        .await?;
    println!("All people over 50: {:?}", people); 
}

原始查询

如果你不希望ORM方法为你工作,你可以回退到原始查询。你可以包含手写的字符串,或者如果你想要更底层的查询构建器,你可以使用sqlmo,它是ormlite查询构建器和迁移自动生成背后的底层引擎。

async fn model_query_example() {
    // Query using the Model to still deserialize results into the struct
    let _person = Person::query("SELECT * FROM person WHERE id = ?")
        .bind(1)
        .fetch_one(&mut conn)
        .await?;
}

async fn raw_query_example() {
    // You can also use the raw query API, which will return tuples to decode as you like
    let _used_ids: Vec<i32> = ormlite::query_as("SELECT id FROM person")
        .fetch_all(pool)
        .await
        .unwrap()
        .into_iter()
        .map(|row: (i32, )| row.0)
        .collect();
}

表定制

属性在这些结构体中定义。

以下示例展示了它们是如何工作的。

#[derive(Model, Debug)]
#[ormlite(table = "people", insertable = InsertPerson)]
pub struct Person {
    #[ormlite(primary_key)]
    pub id: i32,
    pub name: String,
    #[ormlite(column = "name_of_column_in_db")]
    pub age: i32,
}

连接

连接支持目前处于alpha阶段。目前,ormlite只支持多对一关系(例如,Person属于Organization)。多对多和一对多支持计划中。如果你使用此功能,请报告你遇到的任何错误。

#[derive(Model, Debug)]
pub struct Person {
    pub id: Uuid,
    pub name: String,
    pub age: i32,
    
    // Note that we don't declare a separate field `pub organization_id: Uuid`.
    // It is implicitly defined by the Join and the join_column attribute.
    #[ormlite(join_column = "organization_id")]
    pub organization: Join<Organization>,
}

#[derive(Model, Debug)]
pub struct Organization {
    pub id: Uuid,
    pub name: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Note we don't need to insert it.
    let org = Organization {
        id: Uuid::new_v4(),
        name: "Acme".to_string(),
    };
    
    let user = Person {
        id: Uuid::new_v4(),
        name: "John".to_string(),
        age: 99,
        organization: Join::new(org),
    };
    
    let mut conn = ormlite::sqlite::SqliteConnection::connect(":memory:").await.unwrap();

    let user = user.insert(&mut conn).await?;
    assert_eq!(user.organization.loaded(), true);
    println!("{:?}", user);
    
    // You can choose whether you want to load the relation or not. The value will be Join::NotQueried if you don't 
    // opt-in to loading it.
    let users = Person::select()
        .join(Person::organization())
        .fetch_all(&mut conn)
        .await?;

    for user in users {
        assert!(user.organization.loaded());
        println!("{:?}", user);
    }
}

特性与数据类型

Uuid、Chrono和Time

如果你想要Uuid或DateTime,并结合serde,你需要直接依赖uuidtimechrono,并为每个添加serde特性。

# Cargo.toml
[dependencies]
uuid = { version = "...", features = ["serde"] } 
chrono = { version = "...", features = ["serde"] }
time = { version = "...", features = ["serde"] }
use ormlite::model::*;
use serde::{Serialize, Deserialize};
use ormlite::types::Uuid;
use ormlite::types::chrono::{DateTime, Utc};

#[derive(Model, Debug, Serialize, Deserialize)]
pub struct Person {
    pub uuid: Uuid,
    pub created_at: DateTime<Utc>,
    pub name: String,
}

Json/Jsonb 列

您可以使用 ormlite::types::Json 来处理 JSON 或 JSONB 字段。对于非结构化数据,使用 serde_json::Value 作为内部类型。对于结构化数据,使用具有 Deserialize + Serialize 的结构体作为泛型。

use ormlite::model::*;
use ormlite::types::Json;
use serde_json::Value;

#[derive(Debug, Serialize, Deserialize)]
pub struct JobData {
    pub name: String,
}

#[derive(Model, Serialize, Deserialize)]
pub struct Job {
    pub id: i32,
    pub structured_data: Json<JobData>,
    pub unstructured_data: Json<Value>,
}

日志记录

您可以使用 sqlx 的日志记录器来记录查询: RUST_LOG=sqlx=info

路线图

  • 直接在模型实例上执行插入、更新、删除操作
  • 部分更新和插入的构建器
  • 用户可以创建忽略默认值的插入模型
  • 选择查询构建器
  • 构建 derive 宏
  • 用于获取单个实体的 Get() 函数。
  • 可以指定表名和主键列名
  • 自动生成插入模型
  • 自动生成迁移
  • 消除 FromRow 宏的需求
  • 多对一连接
  • 自动为迁移生成索引
  • 多对多连接
  • 一对多连接
  • 确保功能正确连接以支持 mysql 和不同的运行时及 SSL 库。
  • 宏选项以自动调整如 updated_at 等列
  • Upsert 功能
  • 批量插入
  • 批量更新的查询构建器
  • 处理批量更新中的 on conflict 子句
  • 与原生 SQL、sqlx、ormx、seaorm、sqlite3-sys、pg、diesel 的基准测试
  • 宏选项以使用 deleted_at 而不是 DELETE 来删除
  • 支持补丁记录,即带有静态字段的更新
  • 考虑一个阻塞接口,可能是仅为 sqlite/Rusqlite。

贡献

开源项目依赖于贡献,ormlite 是一个社区项目。我们欢迎您提交错误报告、功能请求、更好的文档请求、拉取请求等!

依赖项

45–62MB
~1M SLoC