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

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