afastorm
afastorm 是基于 SQLx 的 Rust ORM,支持 SQLite 和 PostgreSQL。它提供模型元数据、类型化
Repository、数据库迁移和事务操作。Repository 持有 ORM 连接上下文,因此每次操作不需要重复
传入连接池。
afastorm 与 AFaster 解耦。项目提供管理端元数据,但不会生成或托管 HTTP 管理页面;应用可以 根据元数据实现自己的管理界面和权限控制。
特性
- 使用 Cargo feature 选择 SQLite、PostgreSQL 和可选 SQLx 类型。
- 通过
#[derive(Model)]生成模型元数据,并显式注册模型。 - 提供类型化 CRUD、条件查询和 JSON 路径查找。
- 支持软删除与乐观版本锁。
- 支持事务中的 Repository 操作。
- 提供迁移 API、迁移生成器和命令行工具。
- 提供
Dialecttrait,允许通过 SQLxAny接入其他方言。
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、ne | eq、ne | serde_json::Value | JSON 值相等或不等;对象键顺序无关,数组顺序有关。 |
contains | contains | serde_json::Value | 递归包含;对象键值子集和无序数组元素子集可匹配。 |
has_key | has_key | String | 路径指向的对象包含指定键。 |
has_any_keys、has_all_keys | 同名方法 | Vec<String> | 路径指向的对象包含任意或全部指定键。 |
numeric_compare | gt、gte、lt、lte | serde_json::Number | 数值比较;缺失路径或非数字不匹配。 |
string_match | like、starts_with、ends_with、string_contains | String | SQL 模式或字面字符串匹配。 |
相等和包含的值可用 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、i64 | INTEGER | 布尔值由 SQLx 绑定和解码。 |
f32、f64 | REAL | |
String、serde_json::Value | TEXT | JSON 序列化为文本,读取时解析。 |
Vec<u8> | BLOB | 原始字节。 |
uuid::Uuid(uuid) | TEXT | 使用 SQLx SQLite UUID 编解码。 |
| chrono / time 日期时间类型 | TEXT | 使用 SQLx SQLite 类型支持。 |
Decimal、BigDecimal | TEXT | 避免数值亲和性造成精度损失,读取时解析。 |
| IP 地址/网络类型 | TEXT | 文本存储,读取时解析。 |
MacAddress | TEXT | 文本表示。 |
BitVec | TEXT | 以 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 列类型 |
|---|---|
bool | BOOLEAN |
i16、i32、i64 | SMALLINT、INTEGER、BIGINT |
f32、f64 | REAL、DOUBLE PRECISION |
String | TEXT |
Vec<u8> | BYTEA |
serde_json::Value | JSONB |
uuid::Uuid(uuid) | UUID |
| chrono/time 日期时间类型 | DATE、TIME、TIMESTAMP 或 TIMESTAMPTZ |
rust_decimal::Decimal、bigdecimal::BigDecimal | NUMERIC |
IpAddr、IpNetwork、IpNet | INET |
MacAddress | MACADDR |
BitVec | BIT 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 范围类型 |
|---|---|
i32 | INT4RANGE |
i64 | INT8RANGE |
chrono::NaiveDate 或 time::Date | DATERANGE |
chrono::NaiveDateTime 或 time::PrimitiveDateTime | TSRANGE |
chrono::DateTime<Utc> / DateTime<FixedOffset> 或 time::OffsetDateTime | TSTZRANGE |
rust_decimal::Decimal 或 bigdecimal::BigDecimal | NUMRANGE |
可通过 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_versioned | Result<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 宏。
- 更新
Cargo.toml中的version。若 derive 宏有变更,还需更新afastorm-derive/Cargo.toml的版本和主 crate 对afastorm-derive的依赖要求。 - 更新
CHANGELOG.md并运行 CI 检查。 - 在仓库 Actions secrets 中配置 crates.io 发布令牌
CARGO_REGISTRY_TOKEN。 - 创建并推送符合
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。