
前言
大家好,我是ElephAI项目的开发者。今天想和大家聊聊我们项目中一个很实用的宏——#[repository]。
你有没有遇到过这样的情况:每创建一个数据库表,就要写一套相似的Repository代码?list_by_ids、create、update...这些方法写了一遍又一遍,枯燥又容易出错。
今天要介绍的这个#[repository]宏,就是来解决这个问题的。只需要几行代码,就能自动生成完整的数据库操作方法。
先看看效果
先看看用法和不用法的对比。
不用宏的传统写法:
pubstruct AgentChatRepository;impl AgentChatRepository {pub async fn list_by_ids( db: &impl ConnectionTrait, ids: Vec<u64> ) ->Result<Vec<Model>, DbErr> { Entity::find() .filter(Column::Id.is_in(ids)) .filter(Column::DeletedAt.is_null()) .all(db) .await }pub async fn create( db: &impl ConnectionTrait, data: ActiveModel ) ->Result<InsertResult<ActiveModel>, DbErr> { Entity::insert(data).exec(db).await }pub async fn update( db: &impl ConnectionTrait, data: ActiveModel ) ->Result<Model, DbErr> { Entity::update(data).exec(db).await }}用宏的写法:
use eleph_macros::repository;#[repository]pubstructAgentChatRepository;implAgentChatRepository {}就这么简单!一个空结构体加上#[repository]属性,就自动拥有了完整的数据库操作能力。
依赖与前置知识
编写这个宏主要用到四个crate。
Cargo.toml依赖声明:
[dependencies]proc-macro2.workspace = truequote.workspace = truesyn.workspace = trueheck.workspace = truerepository.rs完整import:
use proc_macro2::{Span, TokenStream};use quote::quote;use syn::{Data, DeriveInput, Error, Fields, Ident, Result};各个import的作用:
proc_macro2::Span └── 代码位置信息,用于生成错误的spanproc_macro2::TokenStream └── 整个代码块的token序列quote::quote └── 生成代码的宏,将Rust代码转换为TokenStreamsyn::Data └── 类型的数据部分(枚举/结构体/联合体)syn::DeriveInput └── #[derive(...)] 包裹的类型,包含名称、属性、数据等信息syn::Fields └── 字段类型(命名/元组/无)syn::Fields::Unit └── 单元结构体(没有字段的结构体)syn::Ident └── 标识符,如变量名、函数名、结构体名syn::Error └── 错误类型,用于报告宏使用错误syn::Result └── 结果类型,通常是 Result<TokenStream, Error>宏的实现原理
下面让我们一步步拆解#[repository]宏的实现。
第一步:宏的定义与入口
在lib.rs中,repository宏被定义为一个属性宏:
#[proc_macro_attribute]pubfnrepository(_attr: TokenStream, input: TokenStream) -> TokenStream {letinput = parse_macro_input!(input as DeriveInput); repository::parse(input) .unwrap_or_else(syn::Error::into_compile_error) .into()}宏接收两个参数:
_attr:宏的属性参数,当前版本未使用 input:应用宏的结构体定义
第二步:输入验证
宏首先检查输入的结构体是否为单元结构体(没有字段):
pubfn parse(input: DeriveInput) ->Result<TokenStream> {let mut is_unit = false;if let Data::Struct(ds) = input.data {ifletFields::Unit = ds.fields { is_unit = true; } }if !is_unit {return Err(Error::new( input.ident.span(),format!("the `{}` must be unit struct!", &input.ident), )); }// ...}这是因为Repository只需要作为方法容器,不需要存储任何状态。
第三步:名称转换
宏需要将Repository名称转换为对应的Model名称:
const SUFFIX: &'static str = "Repository";fn struct_to_model(ident: &Ident, suffix: &str) -> Ident {let name = ident.to_string();let without_suffix = if name.ends_with(suffix) { &name[0..name.len() - suffix.len()] } else { &name };let mut snake_case = String::new();for (i, c) in without_suffix.chars().enumerate() {if c.is_uppercase() {if i != 0 { snake_case.push('_'); } snake_case.push(c.to_ascii_lowercase()); } else { snake_case.push(c); } } Ident::new(&snake_case, Span::call_site())}这个函数做了两件事:
移除结构体名称末尾的"Repository"后缀 将驼峰命名转换为蛇形命名
例如:AgentChatRepository → agent_chat
第四步:生成代码
这是最核心的部分,使用quote库生成完整的Repository代码:
letmodule_ident = struct_to_model(&input.ident, SUFFIX);let input_ident = input.ident;let input_attrs = input.attrs;let token = quote! {use std::sync::Arc;use sea_orm::{ConnectionTrait, DbErr, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, InsertResult, UpdateResult, sea_query::OnConflict, prelude::Expr};use crate::models::#module_ident::{Column, Entity, Model, ActiveModel}; #(#input_attrs)*pub struct #input_ident;impl #input_ident {pub async fn list_by_ids(db:&implConnectionTrait, ids:Vec<u64>) ->Result<Vec<Model>, DbErr> { Entity::find() .filter(Column::Id.is_in(ids)) .filter(Column::DeletedAt.is_null()) .all(db) .await }pub async fn create(db:&implConnectionTrait, data:ActiveModel) ->Result<InsertResult<ActiveModel>, DbErr> { Entity::insert(data) .exec(db) .await }pub async fn update(db:&implConnectionTrait, data:ActiveModel) ->Result<Model, DbErr> { Entity::update(data) .exec(db) .await }// ... }};Ok(TokenStream::from(token))生成的代码包含:
必要的导入语句 结构体声明 标准的数据库操作方法
与SeaORM框架的集成
repository宏与SeaORM框架紧密集成,利用了SeaORM的核心概念:
ConnectionTrait:数据库连接抽象 Entity:数据库表实体 Model:数据库记录模型 ActiveModel:用于创建或更新记录的模型 DbErr:SeaORM的错误类型
扩展与定制
虽然宏会自动生成基础方法,但你仍然可以添加自定义方法:
#[repository]pub struct AgentChatRepository;impl AgentChatRepository {// 自定义方法:根据用户ID查询聊天记录pub async fn get_by_user_id( db: &implConnectionTrait, user_id: u64 ) ->Result<Vec<Model>, DbErr> { Entity::find() .filter(Column::UserId.eq(user_id)) .filter(Column::DeletedAt.is_null()) .all(db) .await }}宏生成的方法和你自定义的方法可以完美共存。
优势总结
使用#[repository]宏有以下几个明显的优势:
- 减少样板代码
:不再需要为每个仓库重复编写相同的CRUD方法 - 统一代码风格
:所有仓库自动遵循相同的实现模式 - 提高开发效率
:可以专注于业务逻辑,而不是基础的数据库操作 - 降低错误风险
:标准化的实现减少了手动编写时可能引入的错误
设计思考
为什么选择单元结构体?
单元结构体作为方法容器,不需要存储状态,符合Repository模式的设计理念。
为什么使用命名约定?
通过约定Repository名称与Model名称的对应关系,减少了配置,提高了开发效率。开发者只需要遵循命名规范,宏就能自动找到对应的模型。
为什么支持扩展?
允许添加自定义方法,保持了宏的灵活性,满足不同业务场景的需求。
未来可改进的方向
目前版本的repository宏还有一些可以改进的地方:
支持更多基础方法:如 get_by_id、delete_by_id等提供配置选项:允许自定义生成的方法 支持软删除配置:可配置是否启用软删除 添加事务支持:简化事务操作 支持关联查询:处理复杂的数据库关系
工作流程回顾
最后来总结一下repository宏的工作流程:
接收单元结构体定义 验证输入的正确性 转换名称,找到对应的Model 生成标准的数据库操作方法 返回生成的代码
结语
#[repository]宏是Rust宏编程的一个优秀实践。它通过自动化代码生成,极大地简化了数据库操作的实现,让开发者能够更加专注于业务逻辑。
这种"用代码生成代码"的方式,正是Rust语言强大表达能力的体现。如果你在开发Rust项目,不妨试试这种宏编程的方式,相信会给你带来不少惊喜!
夜雨聆风