构建数据库repository层——从tauri-plugin-sql重构到seaorm #2

构建数据库repository层——从tauri-plugin-sql重构到seaorm #2

huoshen80
2025-10-21 / 0 评论 / 4 阅读 / 正在检测是否收录...

使用dto灵活转换数据结构

在构建数据库的repository层时,灵活的数据结构转换是非常重要的。通过使用DTO(数据传输对象),可以在前后端之间传递数据时,保持数据结构的清晰和一致性。entity实体结构用于数据库操作,而DTO则用于与前端交互。

对于前端需要插入游戏而传递的数据,不存在后端需要的自增主键id,entity结构中包含id字段,而且要求不为空,如果直接使用entity结构进行插入操作,则需要前端添加这个并无意义的id字段。为此我准备了一个GameInsertDto结构,只包含前端传递的必要字段:

/// 用于插入游戏的数据结构(不包含 id, created_at, updated_at)
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct InsertGameData {
    pub bgm_id: Option<String>,
    pub vndb_id: Option<String>,
    pub id_type: String,
    pub date: Option<String>,
    pub localpath: Option<String>,
    pub savepath: Option<String>,
    pub autosave: Option<i32>,
    pub clear: Option<i32>,
    pub custom_name: Option<String>,
    pub custom_cover: Option<String>,
}

普通的dto结构体无法直接用于seaorm的数据库操作,因此需要实现一个转换方法:

/// Trait:将 DTO 转换为 ActiveModel
pub trait IntoActiveModel<T> {
    fn into_active_model(self, game_id: i32) -> T;
}

impl IntoActiveModel<bgm_data::ActiveModel> for BgmDataInput {
    fn into_active_model(self, game_id: i32) -> bgm_data::ActiveModel {
        bgm_data::ActiveModel {
            game_id: Set(game_id),
            image: Set(self.image),
            name: Set(self.name),
            name_cn: Set(self.name_cn),
            aliases: Set(self.aliases),
            summary: Set(self.summary),
            tags: Set(self.tags),
            rank: Set(self.rank),
            score: Set(self.score),
            developer: Set(self.developer),
        }
    }
}

更新数据需要删除某个列时(设为null),正常情况下的反序列化会将缺失的字段和显式的null值都解析为None,导致无法区分这两种情况。为了解决这个问题,可以自定义一个double_option函数,用于分辨从前端传来的未定义字段和显式的null值:

fn double_option<'de, D, T>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
where
    D: Deserializer<'de>,
    T: Deserialize<'de>,
{
    Ok(Some(Option::deserialize(deserializer)?))
}

#[serde(default, deserialize_with = "double_option")]
pub bgm_id: Option<Option<String>>
// 各行数据......

构建repository层

重构到seaorm的优势将在这里体现出来:repository层中大部分的数据库操作逻辑,不再需要使用sql语句,而是通过orm方法进行增删改查;因为需要将原来的总games表进行拆分,有关数据库的操作从简单的一对一映射,变成了一对多映射,而seaorm可以轻松定义实体间的关系(如 One-to-One, One-to-Many等),使一对多映射的实现变得简单。

插入示例:

/// 批量插入游戏数据(包含关联数据)
    pub async fn insert_with_related(
        db: &DatabaseConnection,
        game: InsertGameData,
        bgm: Option<BgmDataInput>,
        vndb: Option<VndbDataInput>,
        other: Option<OtherDataInput>,
    ) -> Result<i32, DbErr> {
        let txn = db.begin().await?;

        // 构建 ActiveModel 并插入游戏基础数据
        let now = chrono::Utc::now().timestamp() as i32;
        let game_active = games::ActiveModel {
            id: NotSet,
            bgm_id: Set(game.bgm_id),
            vndb_id: Set(game.vndb_id),
            id_type: Set(game.id_type),
            date: Set(game.date),
            localpath: Set(game.localpath),
            savepath: Set(game.savepath),
            autosave: Set(game.autosave),
            clear: Set(game.clear),
            custom_name: Set(game.custom_name),
            custom_cover: Set(game.custom_cover),
            created_at: Set(Some(now)),
            updated_at: Set(Some(now)),
        };

        let game_model = game_active.insert(&txn).await?;
        let game_id = game_model.id;

        // 使用辅助函数插入关联数据
        Self::insert_bgm_data(&txn, game_id, bgm).await?;
        Self::insert_vndb_data(&txn, game_id, vndb).await?;
        Self::insert_other_data(&txn, game_id, other).await?;

        txn.commit().await?;
        Ok(game_id)
    }

查询示例:

/// 根据 ID 查询完整游戏数据(包含关联数据)
    pub async fn find_full_by_id(
        db: &DatabaseConnection,
        id: i32,
    ) -> Result<Option<FullGameData>, DbErr> {
        let game = match Games::find_by_id(id).one(db).await? {
            Some(g) => g,
            None => return Ok(None),
        };

        let bgm = BgmData::find_by_id(id).one(db).await?;
        let vndb = VndbData::find_by_id(id).one(db).await?;
        let other = OtherData::find_by_id(id).one(db).await?;

        Ok(Some(FullGameData {
            game,
            bgm_data: bgm,
            vndb_data: vndb,
            other_data: other,
        }))
    }

这篇文章没图,那就放个野生的里想奈
/滑稽
里想奈

1

评论 (0)

取消