在构建任何复杂的应用程序时,数据持久化都是核心环节。通常会采用**仓储模式(Repository Pattern)**来解耦业务逻辑和数据访问逻辑。这带来了清晰的架构,但也引入了一个常见的问题:大量的样板代码(Boilerplate Code)。
每个实体都需要一个仓储接口,以及对应的实现。find_by_id, find_by_email, exists_by_name, delete_by_id... 这些简单却重复的方法,我们一遍又一遍地编写,不仅枯燥,还容易出错。
痛点分析:传统仓储层的重复劳动
下面是 UserRepository 在没有自动化工具时的完整面貌:
1. Trait 定义 - 接口膨胀
// src/domain/repositories/user_repository.rs
pub trait UserRepository {
// 基础CRUD
async fn find_by_id(&self, txn: &mut Txn, id: &UserId) -> Result<Option<User>>;
async fn save(&self, txn: &mut Txn, user: &mut User) -> Result<()>;
async fn delete_by_id(&self, txn: &mut Txn, id: &UserId) -> Result<()>;
// 各种查询方法
async fn find_id_by_email(&self, txn: &mut Txn, email: &Email) -> Result<Option<UserId>>;
async fn find_id_by_user_name(&self, txn: &mut Txn, user_name: &str) -> Result<Option<UserId>>;
async fn exists_by_email(&self, txn: &mut Txn, email: &Email) -> Result<bool>;
async fn find_by_status(&self, txn: &mut Txn, status: UserStatus) -> Result<Vec<User>>;
// ... 更多类似方法,接口越来越庞大
}
2. 实现类 - 重复的SQL模板
// src/infrastructure/repositories_impl/user_repository_impl.rs
pub struct UserRepositoryImpl;
#[async_trait::async_trait]
impl UserRepository for UserRepositoryImpl {
async fn find_by_id(&self, txn: &mut Txn, id: &UserId) -> Result<Option<User>> {
// 每个方法都要写几乎相同的模板代码
txn.query_sql_one("SELECT * FROM ua_user WHERE id = $1", [id.into()]).await
}
async fn find_id_by_email(&self, txn: &mut Txn, email: &Email) -> Result<Option<UserId>> {
// 只是表名、字段名、参数位置变化,结构完全一样
txn.query_sql_one("SELECT id FROM ua_user WHERE email = $1", [email.into()]).await
}
async fn find_id_by_user_name(&self, txn: &mut Txn, user_name: &str) -> Result<Option<UserId>> {
// 重复第三次...
txn.query_sql_one("SELECT id FROM ua_user WHERE user_name = $1", [user_name.into()]).await
}
async fn exists_by_email(&self, txn: &mut Txn, email: &Email) -> Result<bool> {
// 稍微复杂一点,但模式仍然固定
txn.query_sql_one("SELECT EXISTS(SELECT 1 FROM ua_user WHERE email = $1)", [email.into()]).await
}
// 保存逻辑更复杂,但也是固定模式
async fn save(&self, txn: &mut Txn, user: &mut User) -> Result<()> {
if user.is_new() {
// INSERT 逻辑
let sql = "INSERT INTO ua_user (id, email, user_name, ...) VALUES ($1, $2, $3, ...)";
txn.execute_sql(sql, params![user.id(), user.email(), ...]).await?;
} else {
// UPDATE 逻辑
let sql = "UPDATE ua_user SET email = $1, user_name = $2, ... WHERE id = $3";
txn.execute_sql(sql, params![user.email(), user.user_name(), user.id()]).await?;
}
Ok(())
}
}
3. 测试类 - 更多的重复
// tests/user_repository_test.rs
// 还需要为每个方法编写测试,Mock 各种依赖...
这种模式的问题很明显:
- 代码重复:每个查询方法都是相似的模板
- 维护困难:表结构变更需要修改多个地方
- 容易出错:手写SQL容易有拼写错误
- 效率低下:花费大量时间在机械性编码上
✨ 解决方案:#[repository] 宏的威力
现在让我们看看使用 #[repository] 宏之后的变化:
// src/domain/repositories/user_repository.rs
use core_common::repository;
#[repository(aggregate = User, table_name = "ua_user")]
pub trait UserRepository: Send + Sync {
// 复杂的、需要特殊逻辑的查询,保持手写实现
async fn get_all_permissions(
&self,
user_id: &UserId,
tenant_id: Option<TenantId>,
) -> Result<PermissionIdHashSet, RepositoryError> {
// 这里仍然可以手写复杂实现
// 比如联表查询、特殊业务逻辑等
}
// 简单查询:只需定义方法签名,空函数体 {}
// 宏会自动生成完整的SQL实现!
// 根据ID查询
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, RepositoryError> {}
// 根据各种字段查询ID
async fn find_id_by_user_name(&self, user_name: &str) -> Result<Option<UserId>, RepositoryError> {}
async fn find_id_by_email(&self, email: &Email) -> Result<Option<UserId>, RepositoryError> {}
async fn find_id_by_phone(&self, phone: &Phone) -> Result<Option<UserId>, RepositoryError> {}
// 存在性检查
async fn exists_by_email(&self, email: &Email) -> Result<bool, RepositoryError> {}
async fn exists_by_user_name(&self, user_name: &str) -> Result<bool, RepositoryError> {}
// 复杂条件查询
async fn find_by_email_and_status(&self, email: &Email, status: UserStatus) -> Result<Vec<User>, RepositoryError> {}
async fn find_by_email_or_phone(&self, email: &Email, phone: &Phone) -> Result<Vec<User>, RepositoryError> {}
// 统计查询
async fn count_by_status(&self, status: UserStatus) -> Result<i64, RepositoryError> {}
// 删除操作
async fn delete_by_id(&self, id: &UserId) -> Result<(), RepositoryError> {}
// 保存操作 - 宏会自动处理INSERT/UPDATE逻辑
async fn save(&self, user: &mut User) -> Result<(), RepositoryError> {}
}
宏的魔法效果详解:
-
自动实现生成
- 编译时自动为每个空方法生成完整的SQL实现
- 无需手动编写
UserRepositoryImpl - 生成的代码与手写代码质量相当,但零错误
-
智能参数注入
- 自动添加事务参数
txn: &mut TC - 自动处理参数类型转换(如
Email→ 数据库类型) - 最终方法签名实际上是:
find_id_by_email(&self, txn: &mut TC, email: &Email)
- 自动添加事务参数
-
CRUD 功能继承
- 通过
aggregate = User自动继承CrudRepository<User, TC> - 免费获得:
find_by_id,save,delete_by_id,exists_by_id等标准方法 - 无需重复定义基础CRUD操作
- 通过
-
完整的测试支持
- 自动利用
mockall生成Trait的Mock实现 - 在测试中可以直接:
let mut mock = MockUserRepository::new(); - 极大简化单元测试的编写
- 自动利用
🧙♂️ 魔法揭秘:命名约定解析引擎
宏的核心是一个强大的方法名解析器,它将自然语言风格的方法名转换为精确的SQL查询。
查询类型推断规则
| 方法名前缀 | 生成的 SQL 类型 | 返回类型 | 示例 |
|---|---|---|---|
find_by_... |
SELECT * |
Option<T> 或 Vec<T> |
find_by_email |
find_{field}_by_... |
SELECT {field} |
Option<FieldType> |
find_id_by_email |
find_{field1}_and_{field2}_by_... |
SELECT {field1}, {field2} |
元组或自定义结构体 | find_id_and_email_by_phone |
exists_by_... |
SELECT EXISTS(...) |
bool |
exists_by_email |
count_by_... |
SELECT COUNT(*) |
i64 |
count_by_status |
delete_by_... |
DELETE |
() |
delete_by_id |
条件运算符映射
| 方法名后缀 | SQL 运算符 | 示例 | 生成 WHERE 子句 |
|---|---|---|---|
| (无后缀) | = |
find_by_email |
email = $1 |
_not |
!= |
find_by_status_not |
status != $1 |
_like |
LIKE |
find_by_name_like |
name LIKE $1 |
_gte |
>= |
find_by_age_gte |
age >= $1 |
_lte |
<= |
find_by_age_lte |
age <= $1 |
_between |
BETWEEN |
find_by_age_between |
age BETWEEN $1 AND $2 |
_in |
IN |
find_by_status_in |
status IN ($1, $2, ...) |
_is_null |
IS NULL |
find_by_deleted_at_is_null |
deleted_at IS NULL |
逻辑连接符处理
| 连接词 | SQL 逻辑 | 示例 | 生成条件 |
|---|---|---|---|
_and_ |
AND |
find_by_email_and_status |
email = $1 AND status = $2 |
_or_ |
OR |
find_by_email_or_phone |
email = $1 OR phone = $2 |
排序和分页支持
| 后缀 | SQL 子句 | 示例 | 效果 |
|---|---|---|---|
_order_by_{field}_asc |
ORDER BY field ASC |
find_by_status_order_by_created_at_asc |
按创建时间升序 |
_order_by_{field}_desc |
ORDER BY field DESC |
find_by_status_order_by_created_at_desc |
按创建时间降序 |
_first / _top |
LIMIT 1 |
find_by_status_first |
只返回第一条 |
_limit_{n} |
LIMIT n |
find_by_status_limit_10 |
返回前10条 |
实际解析示例
简单查询:
async fn find_id_by_email(&self, email: &Email) -> Result<Option<UserId>> {}
↓ 生成 SQL:
SELECT id FROM "ua_user" WHERE email = $1
复杂查询:
async fn find_id_by_email_and_status_order_by_created_at_desc(
&self,
email: &Email,
status: UserStatus
) -> Result<Option<UserId>> {}
↓ 生成 SQL:
SELECT id FROM "ua_user"
WHERE email = $1 AND status = $2
ORDER BY created_at DESC
存在性检查:
async fn exists_by_email_and_tenant_id(
&self,
email: &Email,
tenant_id: &TenantId
) -> Result<bool> {}
↓ 生成 SQL:
SELECT EXISTS(
SELECT 1 FROM "ua_user"
WHERE email = $1 AND tenant_id = $2
)
⚙️ 灵活的配置选项
宏支持丰富的配置参数来适应不同场景:
// 基础配置
#[repository(aggregate = User, table_name = "ua_user")]
// 多租户支持
#[repository(aggregate = User, table_name = "ua_user", tenant = true)]
// 禁用Mock生成(用于性能敏感场景)
#[repository(aggregate = User, table_name = "ua_user", mock = false)]
// 自定义事务上下文
#[repository(
aggregate = User,
table_name = "ua_user",
context = "db: &Db" // 使用自定义的db参数而非默认txn
)]
// 复杂配置组合
#[repository(
aggregate = User,
table_name = "ua_user",
tenant = true,
mock = true,
context = "conn: &mut PgConnection"
)]
🎯 最佳实践和适用场景
推荐使用宏的场景:
- 简单CRUD操作:95%的数据库查询都适合
- 标准查询模式:等值查询、范围查询、存在性检查等
- 快速原型开发:快速验证业务逻辑,后期可替换为手写优化SQL
- 团队规范统一:确保所有开发者使用一致的查询模式
建议手写实现的场景:
- 复杂联表查询:涉及多个表的复杂JOIN操作
- 自定义聚合查询:GROUP BY、HAVING等复杂聚合
- 数据库特定优化:需要数据库特定语法或优化提示
- 存储过程调用:调用数据库存储过程或函数
📊 实际效果对比
代码量减少:
- 传统方式:每个查询方法需要 5-10 行实现代码
- 宏方式:每个查询方法只需 1 行声明
- 节省比例:约 80-90% 的代码量
开发效率提升:
- 新查询方法:从 5分钟/个 减少到 30秒/个
- 维护成本:表结构变更时,只需修改宏参数,无需改动多个方法
- 错误率:编译时检查替代运行时SQL错误
总结
#[repository] 宏不仅仅是一个代码生成工具,它代表了一种声明式数据访问的新范式。通过将开发者的重心从"如何实现"转移到"想要什么",它真正实现了:
- ⚡ 极致效率:声明即实现,开发速度提升5-10倍
- 🔒 绝对安全:编译时检查确保所有查询的类型安全
- 📚 规范统一:强制执行一致的代码标准和命名约定
- 🧪 测试友好:开箱即用的Mock支持,测试编写效率大幅提升
- 🛠 灵活演进:复杂场景仍可手写实现,兼顾简单与复杂需求
在Rust强大的元编程能力支持下,我们能够构建出这样既智能又实用的开发工具。这不仅是技术的进步,更是开发体验的革新——让我们能够更专注于业务逻辑本身,而不是重复的机械性工作。
评论区
写评论还没有评论