Room数据库保存JobPW时遇竞态条件,Workers关联表数据缺失
一、挂起函数中实现顺序等待的正确方式
挂起函数本身支持顺序执行自动等待,不需要额外拆分成多个协程。Room的suspend方法本身是线程安全的,在同一个协程内按顺序调用,会自动等待前一个操作完成后再执行下一个,完全满足你的需求。
你之前用launch嵌套的协程拆分方式是错误的:不仅无法保证执行顺序,还会出现返回值提前返回的问题(比如saveJobToDatabase函数直接返回jobId=0,因为协程还没执行完)。正确的写法是把saveJobToDatabase改为suspend函数,内部顺序调用Dao方法:
suspend fun saveJobToDatabase(job: JobPW): Long { val jobId = jobsDao.insertJob(job.job) jobsDao.insertWorkers(job.workers) job.workers.forEach { jobsDao.insertJobWorkerCrossRef(JobWorkerCrossRef(jobId, it.worker_id)) } jobsDao.insertPostersWithWorker(job.posters) return jobId }
二、交叉关联表插入失败的可能原因及解决
1. Worker插入后ID不匹配(最可能的原因)
如果你的Worker类主键是自增生成(比如@PrimaryKey(autoGenerate = true)),调用insertWorkers后,Room会为新插入的Worker分配新的worker_id,但你原job.workers中的Worker对象还是保留着插入前的旧ID(比如默认值0)。此时用旧ID插入交叉表,会导致关联关系无效,查询时自然取不到workers数据。
解决方法:
修改Dao的insertWorkers方法,让它返回插入后的Worker ID列表:
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertWorkers(workers: List<Worker>): List<Long>
然后在插入交叉表时,使用返回的真实ID:
val jobId = insertJob(job.job) // 获取插入后的Worker ID列表 val workerIds = insertWorkers(job.workers) // 配对原Worker和插入后的ID,插入交叉表 job.workers.zip(workerIds).forEach { (_, workerId) -> insertJobWorkerCrossRef(JobWorkerCrossRef(jobId, workerId)) }
2. Room事务注解失效
Room的@Transaction注解在**旧版本(低于2.2.0)**中对suspend函数支持有问题,会导致事务不生效,多个插入操作不在同一个事务内执行,可能出现Job插入未提交时就插入交叉表的情况。
解决方法:
- 升级Room到2.2.0及以上版本;
- 确保Dao中的
insertJobPW函数正确标记@Transaction,且是suspend函数。
3. 查询时未使用事务
查询JobPW时,@Relation注解需要在事务中执行才能正确关联主表和关联表数据。如果查询函数没有标记@Transaction,可能出现主表数据已存在,但关联表数据未被查询到的情况。
解决方法:
修改查询函数,添加@Transaction注解:
@Transaction @Query("SELECT * FROM Job WHERE job_id = :jobId") suspend fun getJobPW(jobId: Long): JobPW
4. 交叉表字段类型不匹配
检查JobWorkerCrossRef中的job_id和worker_id类型,是否与Job的job_id、Worker的worker_id完全一致(比如都是Long类型)。类型不匹配会导致插入的交叉表数据无法与主表关联。
解决方法:
统一所有关联字段的数据类型,确保交叉表的外键与主表主键类型一致。
5. Worker插入冲突处理不当
如果insertWorkers使用了OnConflictStrategy.IGNORE,当Worker已存在时会跳过插入,但你仍用原对象的ID插入交叉表。如果原ID与数据库中已存在的Worker ID不一致,会导致关联失效。
解决方法:
- 若允许替换已存在的Worker,将
insertWorkers的冲突策略改为OnConflictStrategy.REPLACE; - 若需要保留已存在的Worker,先查询数据库中已存在的Worker ID,再用真实ID插入交叉表。
内容的提问来源于stack exchange,提问作者user1743524

