Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

afastorm

afastorm 是基于 SQLx 的 Rust ORM,支持 SQLite 和 PostgreSQL。它提供模型元数据、类型化 Repository、数据库迁移和事务操作。Repository 持有 ORM 连接上下文,因此每次操作不需要重复 传入连接池。

afastorm 与 AFaster 解耦。项目提供管理端元数据,但不会生成或托管 HTTP 管理页面;应用可以 根据元数据实现自己的管理界面和权限控制。

特性

  • 使用 Cargo feature 选择 SQLite、PostgreSQL 和可选 SQLx 类型。
  • 通过 #[derive(Model)] 生成模型元数据,并显式注册模型。
  • 提供类型化 CRUD、条件查询和 JSON 路径查找。
  • 支持软删除与乐观版本锁。
  • 支持事务中的 Repository 操作。
  • 提供迁移 API、迁移生成器和命令行工具。
  • 提供 Dialect trait,允许通过 SQLx Any 接入其他方言。

SQLite 和 PostgreSQL 的类型映射并不完全相同,请分别查阅SQLite 类型和 PostgreSQL 类型。

最低支持 Rust 版本为 1.94,与 SQLx 0.9 的要求一致。

快速开始

添加依赖

默认启用 derive 和 sqlite。需要迁移 API 或 CLI 时添加 migrate:

[dependencies]
afastorm = { version = "0.0.1", features = ["migrate"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde_json = "1"

使用 PostgreSQL 时关闭默认 feature 并选择所需功能:

afastorm = { version = "0.0.1", default-features = false, features = [
    "derive", "postgres", "migrate", "uuid", "chrono"
] }

更多 feature 及其依赖见 Cargo Features。字段类型的准确映射见 SQLite 类型和PostgreSQL 类型。

连接数据库

SQLite:

let orm = Orm::connect("sqlite://app.db?mode=rwc").await?;

PostgreSQL:

let orm = Orm::connect("postgres://user:password@localhost/app").await?;

请将凭据保存在环境变量或密钥管理系统中,不要提交到版本控制。连接字符串可从 DATABASE_URL 等环境变量读取。

注册模型并使用 Repository

use afastorm::{ModelRegistry, Orm};

let orm = Orm::connect(database_url)
    .await?
    .with_models(ModelRegistry::new().register::<User>());
let users = orm.repo::<User>()?;

创建 Repository 前必须注册对应模型。注册不会自动创建表;生产环境建议使用经过审查的迁移。 完整示例见模型、CRUD 指南和 Repository 与查询。

运行测试

SQLite 测试:

cargo test --no-default-features --features derive,sqlite,migrate

PostgreSQL 集成测试需要指向专用测试数据库的 AFASTORM_TEST_DATABASE_URL:

cargo test --no-default-features --features derive,postgres,migrate --test orm_postgres -- --ignored

这些测试会创建表并写入测试数据,绝不要将生产或共享数据库配置为测试目标。

Cargo Features

afastorm 通过 Cargo features 选择数据库驱动、派生宏、迁移功能和可选 SQLx 类型。以下列表与 crate 的 Cargo.toml 保持一致。

默认功能

默认启用:

derive
sqlite

仅使用 PostgreSQL 时,关闭默认功能并启用 postgres。设置 default-features = false 后,需要显式启用应用所需的所有功能。

功能列表

Feature默认功能与说明
derive是启用 #[derive(Model)],依赖 afastorm-derive;手动实现 Model 时可关闭。
sqlite是启用 SQLx SQLite 驱动,识别 sqlite: URL。
postgres否启用 SQLx PostgreSQL 驱动,识别 postgres:// 和 postgresql:// URL。
migrate否启用迁移 API 和 afastorm CLI。
uuid否支持 uuid::Uuid。
chrono否支持文档列出的 chrono 日期和时间类型。
time否支持 time crate 的日期和时间类型。
rust_decimal否支持 rust_decimal::Decimal;SQLite 以文本存储,PostgreSQL 使用 NUMERIC。
bigdecimal否支持 bigdecimal::BigDecimal;SQLite 以文本存储,PostgreSQL 使用 NUMERIC。
ipnetwork否支持 IpAddr 和 ipnetwork::IpNetwork,同时启用 postgres。
ipnet否支持 IpAddr 和 ipnet::IpNet,同时启用 postgres。
mac_address否支持 mac_address::MacAddress,同时启用 postgres。
bit-vec否支持 bit_vec::BitVec,同时启用 postgres。

serde_json::Value 始终可用,不需要额外的 JSON feature。SQLite 将 JSON 存为文本,PostgreSQL 默认映射为 JSONB。具体行为见SQLite 类型和PostgreSQL 类型。

ipnetwork、ipnet、mac_address 和 bit-vec 会在编译时启用 PostgreSQL feature,因为 SQLx 提供的相关类型集成面向 PostgreSQL。开启这些 feature 后仍可连接 SQLite,其 SQLite 映射使用文本。

可选 feature 只启用 afastorm 明确支持的模型字段类型,并不意味着 derive 支持 SQLx 的所有类型。 详情见模型。

示例

默认 SQLite 并启用迁移:

afastorm = { version = "0.0.1", features = ["migrate"] }

PostgreSQL、迁移、UUID 和 chrono:

afastorm = { version = "0.0.1", default-features = false, features = [
    "derive", "postgres", "migrate", "uuid", "chrono"
] }

模型

#[derive(Model)] 会为具名字段结构体生成表元数据、行解码、值转换和类型化字段代理。使用 Repository 前必须注册模型;注册本身不会创建或迁移数据库表。

use afastorm::Model;

#[derive(Debug, Model)]
#[orm(table = "users")]
struct User {
    #[orm(primary_key, auto_increment)]
    id: Option<i64>,
    #[orm(unique)]
    email: String,
    display_name: String,
    metadata: serde_json::Value,
    #[orm(soft_delete)]
    deleted_at: Option<String>,
    #[orm(version)]
    version: i64,
}

默认表名是结构体名称转为小写蛇形命名,不自动复数化,例如 BlogPost 对应 blog_post。 #[orm(table = "...")] 和 #[orm(column = "...")] 可分别覆盖表名和字段对应的列名。每个模型 最多声明一个主键、软删除字段和版本字段;auto_increment 必须与主键一起使用。

生成式 find_by 方法

字段上的 #[orm(find_by)] 会生成单字段查找;模型上的 find_by(...) 会生成组合查找,多个值 使用 AND 组合:

#[derive(Debug, Model)]
#[orm(find_by(tenant_id, username))]
struct User {
    #[orm(primary_key, auto_increment)]
    id: Option<i64>,
    #[orm(find_by)]
    email: String,
    tenant_id: i64,
    username: String,
}

上述声明会生成 find_by_email 和 find_by_tenant_id_username。模型级属性使用 Rust 字段名, 不是数据库列名;即使字段使用 #[orm(column = "...")] 重命名,查找属性仍写 Rust 字段名。 重复的方法名和未知字段会导致编译失败。可以重复声明多个模型级 find_by(...)。

方法由模型专属扩展 trait 提供,调用处需要导入该 trait:

use crate::models::{User, UserFindBy};

let user = orm.repo::<User>()?
    .find_by_tenant_id_username(7, "ada".to_owned())
    .await?;

返回类型为 Result<Option<User>>。方法只取一条匹配记录,不保证顺序,也不要求字段具有唯一约束。 对于 Option<T> 字段,参数类型是 T;要查询 SQL NULL,使用 first_where(User::field.is_null())。

生成式 JSON 查询

通过可重复的模型级 find_json 声明固定 JSON 路径及可用操作:

#[derive(Debug, Model)]
#[orm(
    find_json(profile.settings.theme, eq, ne, string_match),
    find_json(profile.settings.age, numeric_compare),
    find_json(profile.settings, contains, has_key, has_any_keys, has_all_keys)
)]
struct User {
    #[orm(primary_key, auto_increment)]
    id: Option<i64>,
    profile: serde_json::Value,
}

路径第一段必须是 serde_json::Value 或 Option<serde_json::Value> 模型字段,之后的段落是 JSON 对象键,且必须为合法 Rust 标识符。方法名采用操作在前的形式,例如 find_json_eq_profile_settings_theme。未知字段、非 JSON 字段、无效路径和重复方法名会在 derive 阶段报错。宏检查的是 Rust 模型声明,不会在编译时读取外部数据库结构。

生成方法由 UserFindJson trait 提供。所有方法返回 Result<Option<User>>,查找第一条结果且 不保证顺序,并自动应用软删除过滤。

声明的操作生成的方法参数类型含义
eq、neeq、neserde_json::ValueJSON 值相等或不等;对象键顺序无关,数组顺序有关。
containscontainsserde_json::Value递归包含;对象键值子集和无序数组元素子集可匹配。
has_keyhas_keyString路径指向的对象包含指定键。
has_any_keys、has_all_keys同名方法Vec<String>路径指向的对象包含任意或全部指定键。
numeric_comparegt、gte、lt、lteserde_json::Number数值比较;缺失路径或非数字不匹配。
string_matchlike、starts_with、ends_with、string_containsStringSQL 模式或字面字符串匹配。

相等和包含的值可用 json!(...) 构造,包括 JSON null。JSON null 与 SQL NULL 不同; SQL 空值或不存在的路径不会匹配这些查找。like 中 %、_ 是 SQL 通配符,其余字符串方法 按区分大小写的字面值匹配。SQLite 使用 JSON1;PostgreSQL 默认将 JSON 字段映射为 JSONB。 自定义 Dialect 若未实现 render_json_lookup,会明确返回不支持错误。

支持的模型字段类型

以下类型及其 Option<T> 形式可用于 derive:

  • 始终可用:bool、i16、i32、i64、f32、f64、String、Vec<u8>、 serde_json::Value。
  • uuid:uuid::Uuid。
  • chrono:NaiveDate、NaiveTime、NaiveDateTime、DateTime<Utc> 和 DateTime<FixedOffset>。
  • time:Date、Time、PrimitiveDateTime 和 OffsetDateTime。
  • rust_decimal:rust_decimal::Decimal;bigdecimal:bigdecimal::BigDecimal。
  • ipnetwork:IpAddr、ipnetwork::IpNetwork;ipnet:IpAddr、ipnet::IpNet。
  • mac_address:mac_address::MacAddress;bit-vec:bit_vec::BitVec。

sqlx::FromRow 对模型不是必需的,afastorm 会自行解码派生模型;直接使用 SQLx 查询结果结构体 时仍可使用它。

自定义类型

任意 newtype、具有不同 Rust 类型名的类型别名和自定义 SQLx 类型目前不能直接作为 derive 模型 字段。仅实现 IntoValue、SQLx 的 Type/Encode/Decode 或 PgArrayElement 并不会注册模型 字段类型及 schema 映射。PostgreSQL 用户自定义 enum 和 composite 字段也不受支持。

自定义查询结果可在所选驱动支持解码时通过 Orm::query_as 和 SQLx FromRow 使用。持久化自定义 字段可考虑在应用边界与 String 等内置类型相互转换,或手动实现 Model;当前没有 derive codec 属性。

启用 postgres 后,模型可使用 PgArray<T> 和 PgRange<T>;支持元素、范围和限制见 PostgreSQL 类型。它们不能用作 SQLite 模型字段。

软删除、自动时间戳和版本锁

#[orm(soft_delete)] 要求字段为 Option<T>。普通 Repository 读取和计数会过滤非空删除标记; delete 设置标记,restore 清除标记,force_delete 执行物理删除。

软删除和自动时间戳支持 bool、i32、i64、f32、f64、String、 serde_json::Value、日期时间及 decimal 类型;不支持 i16、UUID、字节、网络、MAC、bit-vector、 数组和范围。数据库生成值的形式因后端和类型而异,选择字段类型前请查阅后端文档。

#[orm(auto_create)] 在插入时填充值;#[orm(auto_update)] 在插入和每次 Repository 更新时填值。 它们不能与主键、自增、软删除或版本属性组合。

#[orm(version)] 要求非空 i32 或 i64。插入时初始化为 1,成功的 Repository 更新、删除和 恢复都会递增版本。save/update 默认使用模型当前版本;过期写入返回乐观锁错误。

注册模型

let registry = ModelRegistry::new()
    .register::<User>()
    .register::<Post>();
let orm = orm.with_models(registry);
let users = orm.repo::<User>()?;

注册表为 Repository、schema 快照和集成层提供元数据。数据库表结构由迁移负责创建和演进。

SQLite 类型

启用 sqlite Cargo feature 后使用 SQLx 原生 SQLite 连接池。SQLite 使用动态类型;下表是 afastorm 生成的列类型声明,不代表数据库会严格限制每个存入值的类型。

模型字段映射

Rust 类型SQLite 列类型说明
bool、i16、i32、i64INTEGER布尔值由 SQLx 绑定和解码。
f32、f64REAL
String、serde_json::ValueTEXTJSON 序列化为文本,读取时解析。
Vec<u8>BLOB原始字节。
uuid::Uuid(uuid)TEXT使用 SQLx SQLite UUID 编解码。
chrono / time 日期时间类型TEXT使用 SQLx SQLite 类型支持。
Decimal、BigDecimalTEXT避免数值亲和性造成精度损失,读取时解析。
IP 地址/网络类型TEXT文本存储,读取时解析。
MacAddressTEXT文本表示。
BitVecTEXT以 0 和 1 字符串存储。

可选类型须启用对应 afastorm feature。ipnetwork、ipnet、mac_address 和 bit-vec 同时 启用 PostgreSQL feature,但它们的 SQLite 文本映射仍可用于 SQLite 连接。

PgArray<T> 和 PgRange<T> 仅适用于 PostgreSQL。即使它们出现在 SQLite 模型元数据中,列也只 会声明为 TEXT,afastorm 不会将其解码为 SQLite 模型字段,不要在 SQLite 模型或 Repository 中使用它们。

SQLite 特有行为

  • 自增主键声明为 INTEGER PRIMARY KEY AUTOINCREMENT。
  • auto_create、auto_update 和软删除字段会根据类型使用布尔值 0/1、Unix 秒、浮点 Unix 秒、CURRENT_TIMESTAMP 或 JSON 时间戳字符串。
  • 迁移生成器拒绝 SQLite 列类型变更和含糊/破坏性 schema 变更;这类变更应手动编写并审查。
  • 内置迁移 runner 为每个迁移开启事务。

通用模型用法见模型,PostgreSQL 映射见PostgreSQL 类型。

PostgreSQL 类型

启用 postgres Cargo feature 后使用 SQLx 原生 PostgreSQL 连接池。derive 支持的常见模型类型 见模型;这不代表 SQLx 可编码的所有类型都能用于模型。

标量字段映射

Rust 模型类型PostgreSQL 列类型
boolBOOLEAN
i16、i32、i64SMALLINT、INTEGER、BIGINT
f32、f64REAL、DOUBLE PRECISION
StringTEXT
Vec<u8>BYTEA
serde_json::ValueJSONB
uuid::Uuid(uuid)UUID
chrono/time 日期时间类型DATE、TIME、TIMESTAMP 或 TIMESTAMPTZ
rust_decimal::Decimal、bigdecimal::BigDecimalNUMERIC
IpAddr、IpNetwork、IpNetINET
MacAddressMACADDR
BitVecBIT VARYING

Rust Option<T> 对应可空列。日期时间、UUID、decimal、网络、MAC 和 bit-vector 类型需要启用 对应 Cargo feature。

sqlx::postgres::types::PgInterval 目前仅支持作为 PgArray 元素,不能独立作为派生模型字段。 其他 SQLx PostgreSQL 专有标量类型也不会自动成为 derive 可用类型。

数组

PostgreSQL 数组字段使用 PgArray<T>。支持的元素类型:

  • 始终可用:bool、i8、i16、i32、i64、f32、f64、String、 serde_json::Value 和 PgInterval。
  • uuid:uuid::Uuid。
  • chrono:NaiveDate、NaiveTime、NaiveDateTime、DateTime<Utc> 和 DateTime<FixedOffset>。
  • time:Date、Time、PrimitiveDateTime、OffsetDateTime。
  • rust_decimal / bigdecimal:对应 decimal 类型。
  • ipnetwork:IpAddr、IpNetwork;ipnet:IpAddr、IpNet。
  • mac_address:MacAddress;bit-vec:BitVec。

PgArray<Option<T>> 表示非空数组中的 NULL 元素;Option<PgArray<T>> 表示数组列本身可空; 空数组使用空的 PgArray 表示。数组通过文本形式编码,并绑定显式元素类型转换,仅用于上列内置 元素实现。单独实现 PgArrayElement 不会自动支持任意自定义 PostgreSQL 类型。

范围

使用 PgRange<T> 表示 PostgreSQL 范围:

Rust 元素类型PostgreSQL 范围类型
i32INT4RANGE
i64INT8RANGE
chrono::NaiveDate 或 time::DateDATERANGE
chrono::NaiveDateTime 或 time::PrimitiveDateTimeTSRANGE
chrono::DateTime<Utc> / DateTime<FixedOffset> 或 time::OffsetDateTimeTSTZRANGE
rust_decimal::Decimal 或 bigdecimal::BigDecimalNUMRANGE

可通过 PgRange::from(1_i32..10) 转换 Rust 标准范围,也可用 PgRange::new(Bound, Bound) 指定端点是否包含及无界端点。Option<PgRange<T>> 表示 NULL 范围列。当前不支持自定义范围类型。

PostgreSQL 行为与限制

  • 自增主键使用 BIGINT GENERATED BY DEFAULT AS IDENTITY。
  • JSON 使用 JSONB,decimal 使用 NUMERIC。
  • 网络、MAC 和 bit-vector 字段使用 PostgreSQL 原生类型;SQLite 不提供相同的操作符和语义。
  • PostgreSQL 模型元数据为数组和范围提供显式列类型。
  • schema 迁移生成器用于列删除/类型变更比较的解析器目前只识别 INTEGER、BIGINT、 REAL、DOUBLE PRECISION、TEXT、BOOLEAN 和 JSONB。类型变更还要求约束未变化且没有 主键或唯一约束。生成语句会显式转换类型,应用前应检查数据及双向转换。
  • derive 不支持 PostgreSQL 用户自定义 enum 和 composite 字段。

更多字段属性见模型,迁移细节见数据库迁移。

Repository 与查询

获取已注册模型的 Repository:

let users = orm.repo::<User>()?;
let mut user = User {
    id: None,
    email: "ada@example.com".to_owned(),
    display_name: "Ada".to_owned(),
    deleted_at: None,
    version: 0,
};
users.insert(&mut user).await?;
let loaded = users.find_by_id(user.id.expect("generated ID")).await?;

Repository 持有 ORM 连接上下文,因此操作不需要额外传入连接池。API 包括插入、保存、更新、 按主键查找、条件/列表/计数查询、软删除恢复和物理删除。模型必须先通过 Orm::with_models 注册。

模型查询

derive 为模型字段生成 ModelField<T> 代理,比较操作的值类型与字段 Rust 类型一致;对于 Option<T> 字段,比较方法接收 T。声明 #[orm(soft_delete)] 后,查询构造器会自动添加 删除标记为 NULL 的条件。

let statement = users
    .query()
    .filter(User::email.like(String::from("%@example.com")))
    .filter(User::id.gte(1_i64))
    .order_by(User::email.asc())
    .limit(20)
    .offset(0)
    .build()?;
let page: Vec<User> = users.fetch_all(&statement).await?;

支持 eq、ne、gt、gte、lt、lte 和 like 比较;条件可用 and、or 组合。字段 还支持 is_null、is_not_null、is_true,排序支持 asc() 和 desc()。Rust 条件类型不保证 数据库列一定支持对应操作符。

已知列名但需要动态条件时可使用 Field::new("column")。select 和 order_by_name 接收字符串 列名;构建 SQL 时会验证标识符,运行时值则作为绑定参数传递。

原始 SQL

Repository 查询构造器之外的开发者编写 SQL 可使用 Orm::query 或 query_as。请绑定运行时 数据,不要通过字符串插值拼接:

let matching: Vec<User> = orm
    .query_as("SELECT id, email, display_name, deleted_at, version FROM users WHERE email = ?")
    .bind("ada@example.com")
    .fetch_all()
    .await?;

SQL 语句为静态字符串,参数独立绑定。? 占位符会为 PostgreSQL 重写,PostgreSQL JSON 操作符会 保留。数组和范围用法见PostgreSQL 类型,SQLite 映射见SQLite 类型。

CRUD 指南

本章介绍当前提供的模型 Repository 方法,以及查找、过滤、更新和删除操作。示例使用 SQLite; Repository API 对 PostgreSQL 相同。

定义模型并连接

use afastorm::{Model, ModelRegistry, Orm};
use serde_json::Value;

#[derive(Debug, Model)]
#[orm(table = "users")]
struct User {
    #[orm(primary_key, auto_increment)]
    id: Option<i64>,
    #[orm(unique)]
    email: String,
    display_name: String,
    active: bool,
    profile: Value,
    #[orm(auto_create)]
    created_at: String,
    #[orm(soft_delete)]
    deleted_at: Option<i64>,
    #[orm(version)]
    version: i64,
}

let orm = Orm::connect("sqlite://app.db?mode=rwc")
    .await?
    .with_models(ModelRegistry::new().register::<User>());
let users = orm.repo::<User>()?;

调用 repo 前必须注册模型。注册不会创建表;应用 schema 建议通过审查过的迁移维护。 Repository::create_table() 适用于测试或简单开发场景。自动时间戳、软删除和版本锁详见 模型,数据库字段映射详见SQLite 类型和 PostgreSQL 类型。

方法概览

方法返回值行为
insert(&mut model)Result<u64>插入记录;数据库生成的主键会写回模型。
save(&mut model)Result<u64>主键缺失时插入,否则更新。
update(&mut model, expected_version)Result<u64>按主键更新可变字段;默认用模型版本,也可显式传入期望版本。
find_by_id(id)Result<Option<M>>查找主键对应记录。
生成的 find_by_*Result<Option<M>>由字段或模型 find_by 属性生成类型化查找。
生成的 find_json_*Result<Option<M>>由模型 find_json 属性生成 JSON 路径查找。
first_where(condition)Result<Option<M>>返回第一条匹配记录。
all()Result<Vec<M>>读取所有匹配记录;过滤和分页请使用 query()。
count() / count_where(condition)Result<i64>统计总数或匹配数量。
delete(id)Result<u64>配置软删除时标记删除,否则物理删除。
restore(id)Result<u64>清除软删除标记;模型必须声明软删除字段。
force_delete(id)Result<u64>物理删除,包括软删除模型。
delete_where(condition)Result<u64>物理删除所有匹配记录,不执行软删除。
delete_versioned / restore_versionedResult<u64>按版本删除或恢复;冲突返回 Error::OptimisticLock。

除自动主键插入成功时返回 1 外,u64 表示影响行数。影响行数为 0 表示没有记录被更改; 版本检查操作则会将并发冲突作为乐观锁错误返回。

创建:insert 与 save

insert 无论主键当前是否有值都会执行插入;自增主键会写回输入模型:

let mut user = User {
    id: None,
    email: "ada@example.com".to_owned(),
    display_name: "Ada".to_owned(),
    active: true,
    profile: serde_json::json!({ "role": "author" }),
    created_at: String::new(),
    deleted_at: None,
    version: 0,
};

users.insert(&mut user).await?;
let id = user.id.expect("database-generated ID");
assert_eq!(user.version, 1);

配置 auto_create/auto_update 的字段由方言生成写入表达式,调用者提供的值会被忽略。若需读取 数据库最终写入值,请重新加载模型。

save 根据模型是否有主键来选择插入或更新:

users.save(&mut new_user).await?;       // 没有主键:插入
users.save(&mut existing_user).await?;  // 有主键:更新

它不会先查询主键对应记录是否存在,因此不是 upsert。save 要求模型声明主键。

读取:查找方法

find_by_id 根据主键返回可选模型:

let found: Option<User> = users.find_by_id(id).await?;
if let Some(user) = found {
    println!("{}", user.email);
}

字段上的 #[orm(find_by)] 会为该字段生成查找方法;模型级 #[orm(find_by(...))] 会生成组合 查找,多个字段以 AND 连接。调用处需要导入模型对应的扩展 trait:

#[derive(Model)]
#[orm(find_by(tenant_id, username))]
struct User {
    #[orm(primary_key, auto_increment)]
    id: Option<i64>,
    #[orm(find_by)]
    email: String,
    tenant_id: i64,
    username: String,
}

use crate::models::{User, UserFindBy};

let by_email = users.find_by_email("ada@example.com".to_owned()).await?;
let by_tenant_and_name = users
    .find_by_tenant_id_username(7, "ada".to_owned())
    .await?;

以上声明生成 find_by_email 和 find_by_tenant_id_username,返回 Result<Option<User>>。方法命名、参数类型和编译期检查见模型。

固定 JSON 路径通过模型级 #[orm(find_json(...))] 声明。操作决定生成的方法;使用时需导入 UserFindJson trait:

#[derive(Model)]
#[orm(
    find_json(profile.settings.theme, eq, string_match),
    find_json(profile.settings, contains)
)]
struct User {
    #[orm(primary_key, auto_increment)]
    id: Option<i64>,
    profile: serde_json::Value,
}

use crate::models::{User, UserFindJson};
use serde_json::json;

let by_theme = users
    .find_json_eq_profile_settings_theme(json!("dark"))
    .await?;
let by_theme_prefix = users
    .find_json_starts_with_profile_settings_theme("da".to_owned())
    .await?;
let with_published_settings = users
    .find_json_contains_profile_settings(json!({"published": true}))
    .await?;

完整操作列表、参数类型和查询行为见模型。

first_where 接收 Condition,无匹配时返回 None:

let found = users
    .first_where(User::email.eq("ada@example.com".to_owned()))
    .await?;
let active_user = users
    .first_where(User::active.is_true())
    .await?;

它会使用 LIMIT 1,但不附加排序。若有多条匹配且结果顺序重要,请使用 query() 建立带排序的查询。

all() 加载 Repository 查询返回的全部记录:

let query = users
    .query()
    .filter(User::active.is_true())
    .order_by(User::email.asc())
    .build()?;
let active_users: Vec<User> = users.fetch_all(&query).await?;

all() 不接收过滤或分页参数。find_by_id、first_where、all 和 query 等普通读取操作会 自动排除软删除记录。

过滤和计数

derive 生成的字段代理提供类型化条件。Option<T> 字段的比较接收 T。支持 eq、ne、gt、gte、lt、lte 和 like:

let condition = User::active
    .is_true()
    .and(User::email.like("%@example.com".to_owned()))
    .and(User::id.gte(10_i64));

let query = users
    .query()
    .filter(condition)
    .order_by(User::created_at.desc())
    .limit(25)
    .offset(0)
    .build()?;
let page: Vec<User> = users.fetch_all(&query).await?;

let total = users.count().await?;
let matching = users.count_where(User::active.is_true()).await?;

条件可通过 and/or 组合;可空字段可用 is_null 和 is_not_null:

let not_deleted = User::deleted_at.is_null();
let has_deleted_at = User::deleted_at.is_not_null();
let empty_email_or_inactive = User::email
    .eq("".to_owned())
    .or(User::active.eq(false));

模型声明软删除时,count 和 count_where 同样会忽略软删除记录。条件值会通过绑定参数传递, 不会插入 SQL 字符串。

动态列名可使用无类型 Field API:

use afastorm::Field;

let query = users
    .query()
    .filter(Field::new("email").like("%@example.com"))
    .build()?;

与 User::email 不同,Field::new 不提供编译期字段和值类型检查。构建查询时会校验标识符; 列名只应来自应用控制的数据,不要直接使用任意客户端输入。

更新、删除与恢复

修改已加载模型后调用 save 或 update。声明版本字段时,默认使用模型当前版本;版本过期会 返回 Error::OptimisticLock。显式提供 expected_version 可进行版本校验。成功更新后版本递增。

delete(id) 在模型配置软删除时写入删除标记,否则删除数据库行。软删除模型可通过 restore(id) 清除标记;force_delete(id) 始终物理删除。delete_versioned 和 restore_versioned 会检查调用者期望的版本。delete_where(condition) 是物理删除,使用前应 谨慎确认条件。

以上操作受 Repository 的模型注册和软删除设置约束。若需要事务,请从事务对象获取 Repository, 见事务。

事务

Orm::transaction 接收异步回调并提供 OrmTransaction。在回调中从事务对象获取模型 Repository, 即可确保操作使用同一连接:

let user_id = orm
    .transaction(async |transaction| {
        let mut user = User {
            id: None,
            email: "ada@example.com".into(),
            display_name: "Ada".into(),
            deleted_at: None,
            version: 0,
        };
        transaction.repo::<User>()?.insert(&mut user).await?;

        let mut profile = Profile {
            id: None,
            user_id: user.id.expect("generated id"),
            biography: "Mathematician".into(),
        };
        transaction.repo::<Profile>()?.insert(&mut profile).await?;

        Ok(user.id.expect("generated id"))
    })
    .await?;

回调返回 Ok 时事务提交;返回错误时 ORM 回滚并返回该错误。若回滚本身失败,返回错误会同时报告 回调错误与回滚错误。

也可以显式控制事务:

let mut transaction = orm.begin().await?;
transaction.repo::<User>()?.delete(user_id).await?;
transaction.commit().await?;

从根 Orm 创建的 Repository 不会自动绑定到事务。在事务回调或手动事务中,必须通过事务对象 获取 Repository。

数据库迁移

启用 migrate feature 可使用迁移 API 和 afastorm CLI。迁移文件成对命名:

<version>_<name>.up.sql
<version>_<name>.down.sql

同一迁移目录中的版本必须唯一。请将两个文件纳入版本控制,并在执行前审查 SQL。

创建手写迁移

cargo run --no-default-features --features derive,sqlite,migrate --bin afastorm -- \
  --dir migrations makemigrations add_user_bio

该命令创建空的 up/down SQL 模板,不会读取数据库或模型代码。

根据模型元数据生成迁移

在应用辅助代码中导出当前已注册模型 schema:

orm.write_schema_snapshot("schema.json")?;

再将快照交给 CLI:

cargo run --no-default-features --features derive,sqlite,migrate --bin afastorm -- \
  --dir migrations --schema schema.json makemigrations add_user_bio

生成器将新快照与上次生成的 migrations/schema.json 比较。若没有旧快照,会为新快照中的模型 生成建表 SQL。生成迁移后会更新 migrations/schema.json,请将它与迁移历史一起保存。生成过程 不会执行迁移。

目前支持新增表、添加可空且非唯一列,以及删除部分无其他约束的可空非唯一列。列定义解析能力 有限:SQLite 删除列支持生成的 INTEGER、REAL 和 TEXT;PostgreSQL 删除/类型比较当前识别 INTEGER、BIGINT、REAL、DOUBLE PRECISION、TEXT、BOOLEAN、JSONB。即使是可空列, 其他声明也需要手写并审查迁移。

生成器拒绝删除表、主键或唯一约束变更、添加非空列、SQLite 类型变更及其他含糊或破坏性变更。 PostgreSQL 类型变更还要求约束集合不变且列没有主键或唯一约束。生成语句会显式转换类型,应用 前应检查数据及可能的数据丢失。恢复已删除列的 down migration 无法恢复已删除列值。

执行、查看和回滚

cargo run --no-default-features --features derive,sqlite,migrate --bin afastorm -- \
  --database-url 'sqlite://app.db?mode=rwc' --dir migrations migrate

cargo run --no-default-features --features derive,sqlite,migrate --bin afastorm -- \
  --database-url 'sqlite://app.db?mode=rwc' status

cargo run --no-default-features --features derive,sqlite,migrate --bin afastorm -- \
  --database-url 'sqlite://app.db?mode=rwc' --dir migrations rollback VERSION

数据库命令支持 --database-url URL 或 DATABASE_URL。status 在需要时会创建 _afastorm_migrations 跟踪表。独立 CLI 不会加载应用配置文件,并拒绝 --config。

runner 按文件名/版本顺序执行未运行的迁移,并记录成功版本。SQLite 和 PostgreSQL 的每个迁移 (包括跟踪记录)都在单独事务中执行。回滚会事务性地执行对应 .down.sql 并删除跟踪记录; 缺少 down 文件或 SQL 执行失败都会报告错误。

需要与框架命令集成时可调用公开的 migration API。后端差异见SQLite 类型和 PostgreSQL 类型。

管理端元数据

Model derive 可在模型和字段元数据中保存可选的管理端标签及能力标志。afastorm 不生成或提供 管理页面,不负责授权,也不会在 Repository 方法中强制执行这些标志。应用集成层需要自行解释元 数据并在服务端实施访问控制。

#[derive(Model)]
#[orm(
    table = "posts",
    admin(label = "文章", list, detail, create, update, delete)
)]
struct Post {
    #[orm(primary_key, auto_increment, admin(label = "ID", list, detail, readonly))]
    id: Option<i64>,
    #[orm(admin(label = "标题", list, detail, create, update, search))]
    title: String,
    #[orm(admin(label = "正文", detail, create, update))]
    body: String,
    #[orm(soft_delete, admin(hidden))]
    deleted_at: Option<String>,
    #[orm(version, admin(label = "版本", detail, readonly))]
    version: i64,
}

模型级选项:label、list、detail、create、update、delete。字段级选项: label、list、detail、create、update、filter、search、readonly、hidden。 它们只作为元数据标签/布尔值保存,不会自动决定 UI 展示内容;hidden 和 readonly 也不是安全 边界。

可从 M::INFO 或 ORM 返回的注册模型元数据中读取这些设置。Web 集成仍应校验提交的字段名和值, 独立执行用户权限检查,并通过 Repository/查询 API 访问数据,不能将客户端控件视为授权机制。

自定义方言

启用相应 feature 时,SqliteDialect 和 PostgresDialect 使用 afastorm 的 SQLx 原生连接池。 自定义 Dialect 可配合 Orm::connect_with_dialect 使用,连接经由 SQLx Any;目标数据库 必须受 SQLx Any 支持。

use afastorm::{Dialect, FieldType, Orm, Result, SqliteDialect};

#[derive(Clone, Copy)]
struct CustomSqliteDialect;

impl Dialect for CustomSqliteDialect {
    fn name(&self) -> &'static str { "sqlite" }
    fn quote_identifier(&self, identifier: &str) -> Result<String> {
        SqliteDialect.quote_identifier(identifier)
    }
    fn placeholder(&self, index: usize) -> String {
        SqliteDialect.placeholder(index)
    }
    fn column_type(&self, field_type: FieldType) -> &'static str {
        SqliteDialect.column_type(field_type)
    }
    fn auto_increment_primary_key(&self) -> &'static str {
        SqliteDialect.auto_increment_primary_key()
    }
    fn boolean_default(&self, value: bool) -> &'static str {
        SqliteDialect.boolean_default(value)
    }
    fn current_value(&self, field_type: FieldType) -> &'static str {
        SqliteDialect.current_value(field_type)
    }
}

let orm = Orm::connect_with_dialect("sqlite::memory:", CustomSqliteDialect, 1).await?;

示例委托 SQLite SQL 语法并通过 SQLx Any 连接;它用于展示扩展入口,并未新增数据库后端。

该 trait 控制标识符引用、占位符、列类型、自增主键、布尔默认值和当前值表达式。默认字段映射 使用 column_type;PostgreSQL 数组/范围的字段级 schema 元数据由内置 PostgreSQL 方言处理。

SQLx Any 存在驱动和类型编码限制。请确认 URL scheme、SQL 语法、RETURNING、事务行为和 所有模型类型均受目标 SQLx driver 支持。如果模型使用 auto_create、auto_update 或软删除, current_value 必须返回类型兼容的表达式。自定义方言不会自动增加自定义模型字段编解码器, 详见模型。

发布

本仓库通过 GitHub Actions 发布 afastorm crate 及其 derive 宏。

  1. 更新 Cargo.toml 中的 version。若 derive 宏有变更,还需更新 afastorm-derive/Cargo.toml 的版本和主 crate 对 afastorm-derive 的依赖要求。
  2. 更新 CHANGELOG.md 并运行 CI 检查。
  3. 在仓库 Actions secrets 中配置 crates.io 发布令牌 CARGO_REGISTRY_TOKEN。
  4. 创建并推送符合 v<version> 格式的 tag,例如 v0.0.1。

发布 workflow 会校验 tag 与主 crate 版本一致;若该版本尚未发布,则先发布 derive 依赖,再发布 afastorm。文档 workflow 会构建中英文 mdBook 并部署到 GitHub Pages。

最低支持 Rust 版本(MSRV)为 1.94,与 SQLx 0.9 依赖要求一致。CI 会测试 Rust 1.94 和 stable。

已发布的 crates.io 版本不可覆盖;任何 crate 变更都必须发布新版本。通过 Git/path 依赖集成的 应用发布后应切换到 crates.io 版本。AFaster 仓库使用 afastorm 的 crates.io 版本,并在首次 发布后更新 lockfile。