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, 见事务。