Rust/SeaORM:唯一约束冲突的重试逻辑实现问题排查
背景与问题
我正在用Rust结合SeaORM实现create_patient函数,患者的patient_ic字段有唯一性约束,需求是出现唯一约束冲突时最多重试5次并生成新的patient_ic。
我编写了检测重复键错误的辅助函数:
pub fn check_if_is_duplicate_key_from_data_base<T>( attempts: &mut usize, result: Result<T, DbErr>, ) -> Option<Result<T, CustomError>> { match result { Ok(value) => Some(Ok(value)), Err(DbErr::Exec(e)) => { if e.to_string().contains("duplicate key value violates unique constraint") || e.to_string().contains("23505") { *attempts += 1; None // 表示需要重试 } else { Some(Err(CustomError::from(DbErr::Exec(e)))) // 不可重试的错误 } } Err(e) => Some(Err(CustomError::from(e))), // 其他错误 } }
create_patient函数实现:
// ... (导入和虚拟结构体已省略) pub async fn create_patient( &self, patient_data: Option<PatientRequestBody>, ) -> Result<PatientWithPerson, CustomError> { let mut payload = patient_data.ok_or_else(|| CustomError::new(400, "Missing patient data".to_string()))?; let now_utc = Utc::now(); let mut attempts = 0; const MAX_ATTEMPTS: usize = 5; // 示例虚拟Person数据 let person = person::Model { id: Uuid::new_v4(), first_name: "Dummy".to_string(), last_name: "Person".to_string(), date_of_birth: Utc::now().naive_utc(), gender: "Other".to_string(), phone: None, email: None, address: None, nationality: None, marital_status: None, photo_url: None, created_at: Utc::now(), updated_at: Utc::now() }; // 初始检查是否存在已提供的patient_ic if let Some(ref ic) = payload.patient_ic { if let Ok(Some(existing_patient)) = self.find_by_field("patient_ic", ic).await { return Ok(PatientWithPerson { patient: existing_patient, person }); } } loop { if attempts >= MAX_ATTEMPTS { return Err(CustomError::new(500, "Failed to generate a unique patient IC after multiple attempts.".to_string())); } if payload.patient_ic.is_none() || attempts > 0 { payload.patient_ic = Some(Self::generate_random_patient_ic()); } let active_model = patient::ActiveModel { id: Set(person.id), patient_ic: Set(payload.patient_ic.clone()), created_at: Set(now_utc), updated_at: Set(now_utc), hospital_id: Set(payload.hospital_id), emergency_contact: Set(payload.emergency_contact.clone()), blood_type: Set(payload.blood_type), allergies: Set(payload.allergies.clone()), medical_history: Set(payload.medical_history.clone()), }; let insert_result = active_model.insert(&self.conn).await; match insert_result { Ok(patient_model) => return Ok(PatientWithPerson { patient: patient_model, person }), Err(db_err) => { if let Some(final_outcome) = Self::check_if_is_duplicate_key_from_data_base(&mut attempts, Err(db_err)) { return final_outcome.map_err(|e| e); // 传播不可重试的错误 } else { // 是重复键错误,继续循环重试 } } } } }
当前问题:当出现patient_ic重复错误(DbErr::Exec包含"23505"或"duplicate key")时,重试循环未正确生成新的patient_ic并重试插入,反而常出现"Patient not found"或初始数据库错误,循环未按预期执行。
疑问
check_if_is_duplicate_key_from_data_base返回的Option<Result<T, CustomError>>在create_patient中是否正确用于触发重试(None)或传播错误(Some(Err))?- 在SeaORM/Rust中,除字符串匹配或特定PostgreSQL错误码外,是否有更可靠的方式检测唯一约束冲突?
create_patient的循环中是否存在微妙的控制流问题导致重试失败?
解答
1. 辅助函数的返回值使用是否正确?
逻辑方向是对的,但存在计数时机的瑕疵:
- 返回
None触发重试、Some(Err)传播错误的逻辑是正确的,但辅助函数里提前递增attempts会导致重试次数计数不准。比如第一次失败后attempts直接变为1,后续循环的次数判断会提前触发终止,实际重试次数达不到预期的5次。
建议把*attempts +=1从辅助函数移到create_patient的重试分支中,确保计数和重试动作一一对应。
2. 更可靠的唯一约束冲突检测方式
可以直接匹配数据库驱动的标准错误类型,避免字符串匹配的脆弱性:
以PostgreSQL为例,引入驱动的错误状态码枚举,直接匹配唯一约束冲突的标准状态:
use postgres::error::SqlState; pub fn is_unique_violation(err: &DbErr) -> bool { match err { DbErr::Exec(e) => { e.downcast_ref::<postgres::Error>() .map(|pg_err| pg_err.code() == Some(&SqlState::UNIQUE_VIOLATION)) .unwrap_or(false) } _ => false, } }
这种方式直接对应SQL标准的23505状态码,不会因为数据库本地化错误信息、版本更新导致匹配失效。
3. 循环中的控制流问题
存在三个关键问题:
问题1:尝试次数递增时机错误
当前辅助函数在检测到错误时就递增attempts,导致循环开头的次数判断提前生效。比如第5次失败后attempts变为5,下一次循环直接返回错误,实际只重试了4次。
问题2:person.id重复使用
每次循环都复用同一个person.id插入患者表,如果患者表的id是主键/唯一键,第一次插入失败后,后续重试会触发id重复的约束错误,被误判为非重试错误返回,导致出现"Patient not found"类问题。
问题3:初始检查的竞态条件
初始检查patient_ic存在性后,其他进程可能在插入前抢先插入同个IC,触发约束错误进入重试,但当前逻辑没问题,属于正常的分布式场景,只需确保重试逻辑正确即可。
修正方案:
pub async fn create_patient( &self, patient_data: Option<PatientRequestBody>, ) -> Result<PatientWithPerson, CustomError> { let mut payload = patient_data.ok_or_else(|| CustomError::new(400, "Missing patient data".to_string()))?; let now_utc = Utc::now(); let mut attempts = 0; const MAX_ATTEMPTS: usize = 5; // 初始检查已提供的patient_ic if let Some(ref ic) = payload.patient_ic { if let Ok(Some(existing_patient)) = self.find_by_field("patient_ic", ic).await { let person = person::Model { id: Uuid::new_v4(), first_name: "Dummy".to_string(), last_name: "Person".to_string(), date_of_birth: Utc::now().naive_utc(), gender: "Other".to_string(), phone: None, email: None, address: None, nationality: None, marital_status: None, photo_url: None, created_at: Utc::now(), updated_at: Utc::now() }; return Ok(PatientWithPerson { patient: existing_patient, person }); } } loop { if attempts >= MAX_ATTEMPTS { return Err(CustomError::new(500, "Failed to generate a unique patient IC after multiple attempts.".to_string())); } // 生成新IC:首次无IC或重试时更新 if payload.patient_ic.is_none() || attempts > 0 { payload.patient_ic = Some(Self::generate_random_patient_ic()); } // 每次重试生成新的person实例(如果patient.id是主键) let person = person::Model { id: Uuid::new_v4(), first_name: "Dummy".to_string(), last_name: "Person".to_string(), date_of_birth: Utc::now().naive_utc(), gender: "Other".to_string(), phone: None, email: None, address: None, nationality: None, marital_status: None, photo_url: None, created_at: Utc::now(), updated_at: Utc::now() }; let active_model = patient::ActiveModel { id: Set(person.id), patient_ic: Set(payload.patient_ic.clone().unwrap()), created_at: Set(now_utc), updated_at: Set(now_utc), hospital_id: Set(payload.hospital_id), emergency_contact: Set(payload.emergency_contact.clone()), blood_type: Set(payload.blood_type), allergies: Set(payload.allergies.clone()), medical_history: Set(payload.medical_history.clone()), }; match active_model.insert(&self.conn).await { Ok(patient_model) => return Ok(PatientWithPerson { patient: patient_model, person }), Err(db_err) => { if is_unique_violation(&db_err) { attempts += 1; // 仅在确定是重复错误时递增次数 } else { return Err(CustomError::from(db_err)); } } } } }
内容的提问来源于stack exchange,提问作者NsdHSO

