使用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,
}))
}
这篇文章没图,那就放个野生的里想奈
/滑稽
评论 (0)