乐于分享
好东西不私藏

Rust 开发 AI 智能体:#[repository]宏的设计原理

Rust 开发 AI 智能体:#[repository]宏的设计原理

前言

大家好,我是ElephAI项目的开发者。今天想和大家聊聊我们项目中一个很实用的宏——#[repository]

你有没有遇到过这样的情况:每创建一个数据库表,就要写一套相似的Repository代码?list_by_idscreateupdate...这些方法写了一遍又一遍,枯燥又容易出错。

今天要介绍的这个#[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 = true

repository.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())}

这个函数做了两件事:

  1. 移除结构体名称末尾的"Repository"后缀
  2. 将驼峰命名转换为蛇形命名

例如: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]宏有以下几个明显的优势:

  1. 减少样板代码
    :不再需要为每个仓库重复编写相同的CRUD方法
  2. 统一代码风格
    :所有仓库自动遵循相同的实现模式
  3. 提高开发效率
    :可以专注于业务逻辑,而不是基础的数据库操作
  4. 降低错误风险
    :标准化的实现减少了手动编写时可能引入的错误

设计思考

为什么选择单元结构体?

单元结构体作为方法容器,不需要存储状态,符合Repository模式的设计理念。

为什么使用命名约定?

通过约定Repository名称与Model名称的对应关系,减少了配置,提高了开发效率。开发者只需要遵循命名规范,宏就能自动找到对应的模型。

为什么支持扩展?

允许添加自定义方法,保持了宏的灵活性,满足不同业务场景的需求。

未来可改进的方向

目前版本的repository宏还有一些可以改进的地方:

  • 支持更多基础方法:如get_by_iddelete_by_id
  • 提供配置选项:允许自定义生成的方法
  • 支持软删除配置:可配置是否启用软删除
  • 添加事务支持:简化事务操作
  • 支持关联查询:处理复杂的数据库关系

工作流程回顾

最后来总结一下repository宏的工作流程:

  1. 接收单元结构体定义
  2. 验证输入的正确性
  3. 转换名称,找到对应的Model
  4. 生成标准的数据库操作方法
  5. 返回生成的代码

结语

#[repository]宏是Rust宏编程的一个优秀实践。它通过自动化代码生成,极大地简化了数据库操作的实现,让开发者能够更加专注于业务逻辑。

这种"用代码生成代码"的方式,正是Rust语言强大表达能力的体现。如果你在开发Rust项目,不妨试试这种宏编程的方式,相信会给你带来不少惊喜!