在 Rust 中实现 Repository 仓储模式

前言

单位上有个 Rust 项目,orm 选型很长时间都没定下来,故先设计了抽象的仓储层方便写业务逻辑。

设计抽象接口

抽象只读接口,仅读取使用,目前需求仅用查询 id、查询全部和按名称搜索,当然理应设计上分页。

//! read_only_repository.rs
/// 只读仓储,对仅限读取的仓储进行抽象
#[async_trait::async_trait]
pub trait IReadOnlyRepository<T>
where
    T: std::marker::Send,
{
    /// 根据 id 获取唯一对象
    async fn get_by_id(&self, id: &str) -> anyhow::Result<T>;
    /// 获取所有对象
    async fn get_all(&self) -> anyhow::Result<Vec<T>>;
    /// 根据名称搜索
    async fn search_by_name(&self, &str) -> anyhow::Result<Vec<T>>;
}

抽象可变接口,目前仅考虑了插入、修改、删除以及事务提交。

//! mutable_repository.rs
/// 可变仓储,对修改数据的仓储进行抽象
#[async_trait::async_trait]
pub trait IMutableRepository<T>
where
    T: std::marker::Send,
{
    /// 更新数据
    async fn update(&self, entity: T) -> anyhow::Result<T>;
    /// 插入数据
    async fn insert(&self, entity: T) -> anyhow::Result<T>;
    /// 删除数据
    async fn delete(&self, entity: T) -> anyhow::Result<bool>;
    /// 使用 uuid 删除数据,`entity` 是用于指示当前实现类型的泛型模板,防止 Rust 产生方法重载的问题,
    /// 但对于大多数数据库可尝试使用以下代码:
    /// ``` no_run
    /// // 建立一个空的枚举用于指示类型
    /// let n: Option<TYPE> = None;
    /// self.delete_by_id(entity.id.as_str(), n).await?;
    /// ```
    async fn delete_by_id(&self, uuid: &str, entity: Option<T>) -> anyhow::Result<bool>;
    /// 提交变更,在带有事务的数据库将提交事务,否则该方法应该仅返回 `Ok(true)`
    ///
    async fn save_changed(&self) -> anyhow::Result<bool>;
}

租约仓储,为了支持非关系型数据库用的,或许会用到租约(生存时间)。

//! lease_repository.rs
/// 租约仓储,对带有租约的仓储进行抽象
#[async_trait::async_trait]
pub trait ILeaseRepository<T>
where
    T: std::marker::Send,
{
    /// 更新数据并更新租约
    async fn update_with_lease(&self, key: &str, entity: T, ttl: i64) -> anyhow::Result<T>;
    /// 插入数据并设定租约
    async fn insert_with_lease(&self, key: &str, entity: T, ttl: i64) -> anyhow::Result<T>;
    /// 延长特定数据的租约
    async fn keep_alive(&self, key: &str) -> anyhow::Result<bool>;
}

最终整合的接口。

//! mod.rs
/// 对使用数据库仓储的抽象,带有可读仓储和可写仓储
#[async_trait::async_trait]
pub trait IDBRepository<T>: IReadOnlyRepository<T> + IMutableRepository<T>
where
    T: std::marker::Send,
{
}

/// 对使用带有租约的数据库进行抽象,带有租约仓储、可读仓储和可写仓储
#[async_trait::async_trait]
pub trait ILeaseDBRepository<T>: IDBRepository<T> + ILeaseRepository<T>
where
    T: std::marker::Send,
{
}

简单实现

泛型具体用起来有一定的生命周期的问题,解决问题的方法也并不难,加控制生命周期的标记。但我目前的实现方案为使用 marco 自动为每个实体类型生成代码。在这里我个人本地暂且先用了 etcd 数据库作为基础实现。
可变仓储的实现:

    /// 针对 Etcd 数据库实现只读仓储 `repository::IMutableRepository`
    ///
    /// struct 要求带有字段 `client: std::sync::Arc<etcd_client::Client>`
    #[macro_export]
    macro_rules! impl_etcd_mutable_repository {
        ($base_struct: ty, $domain: ty) => {
            #[async_trait::async_trait]
            impl IMutableRepository<$domain> for $base_struct {
                async fn update(&self, entity: $domain) -> anyhow::Result<$domain> {
                    let mut kv_client = self.client.kv_client();
                    let key = format!("test_{}_{}", stringify!($domain), entity.id);
                    kv_client
                        .put(
                            key,
                            Into::<Vec<u8>>::into(serde_json::to_vec(&entity).unwrap()),
                            None,
                        )
                        .await?;
                    Ok(entity)
                }
                async fn insert(&self, entity: $domain) -> anyhow::Result<$domain> {
                    self.update(entity).await
                }
                async fn delete(&self, entity: $domain) -> anyhow::Result<bool> {
                    let n: Option<$domain> = None;
                    self.delete_by_id(entity.id.as_str(), n).await
                }
                async fn delete_by_id(
                    &self,
                    uuid: &str,
                    entity: Option<$domain>,
                ) -> anyhow::Result<bool> {
                    let mut kv_client = self.client.kv_client();
                    let key = format!("test_{}_{}", stringify!($domain), uuid);
                    match kv_client.delete(key, None).await {
                        Ok(x) => Ok(true),
                        Err(e) => anyhow::bail!(e),
                    }
                }
                async fn save_changed(&self) -> anyhow::Result<bool> {
                    Ok(true)
                }
            }
        };
    }

具体应用:

use crate::repository::*;
pub struct EtcdRepository {
    client: std::sync::Arc<etcd_client::Client>,
}
impl EtcdRepository {
    pub fn new(client: std::sync::Arc<etcd_client::Client>) -> Self {
        Self { client }
    }
}
impl_etcd_mutable_repository!(
    EtcdRepository,
    crate::models::UserInfo
);

调用

use crate::models::*;
use crate::repository::IMutableDBRepository;

pub struct UserInfoService {
    user_info_repository: std::sync::Arc<dyn IMutableRepository<UserInfo> + Send + Sync>,
}

impl HeartbeatService {
    pub fn new(
        user_info_repository: std::sync::Arc<dyn IMutableRepository<UserInfo> + Send + Sync>,
    ) -> Self {
        return Self { user_info_repository };
    }
}

#[async_trait::async_trait]
pub trait IPluginManagementService {
    async fn list_user_infos(&self) -> Result<Vec<UserInfo>>;
}

#[async_trait::async_trait]
impl IPluginManagementService for PluginManagementService {
    async fn list_user_infos(&self) -> Result<Vec<UserInfo>> {
        self.user_info_repository.get_all().await
    }
}

参考

  1. Rustでクリーンアーキテクチャを組む時DIするサンプルコード
  2. Crate etcd_client
posted @ 2022-04-27 15:10  Aoba_xu  阅读(199)  评论(0编辑  收藏  举报