Rails中first_or_create仅加载已有记录不执行更新,原因何在?
你碰到的这个问题其实是first_or_create方法的一个常见误区——它的块代码只在创建新记录的时候才会执行!当数据库里已经存在匹配user_id和item_id的记录时,这个方法只会直接返回找到的那条记录,完全不会触发你写的s.amount += amount逻辑,这就是为什么你只看到了Blob Load的日志,没有任何更新操作。
为什么会这样?
first_or_create的工作流程是固定的:
- 第一步:执行你定义的
WHERE查询,尝试找到匹配的记录 - 第二步:如果找到记录,直接返回这条记录,跳过块代码
- 第三步:如果没找到,才会初始化一个新的
Blob实例,把块里的逻辑应用到这个实例上,然后保存到数据库
所以你的代码里,只有第一次创建记录时,块里的累加逻辑才会生效;后续再调用时,只会加载已有的记录,不会做任何更新。
解决办法
根据你的需求,这里有几种靠谱的实现方式:
1. 先获取记录,再统一更新(简单直观)
这种方式最容易理解,不管是新创建的还是已有的记录,都执行累加并保存:
blob = Blob.where(user_id: user.id, item_id: item.id).first_or_create blob.amount += amount blob.save!
如果想更简洁,可以合并成一行,而且用数据库层面的加法避免并发竞态问题:
Blob.where(user_id: user.id, item_id: item.id).first_or_create.update!(amount: Blob.arel_table[:amount] + amount)
2. 用upsert实现原子性插入/更新(高效推荐)
如果你使用的是Rails 6及以上版本,推荐用upsert方法,它能在一次数据库操作里完成“找到就更新,找不到就创建”的逻辑,效率更高:
Blob.upsert( { user_id: user.id, item_id: item.id }, unique_by: [:user_id, :item_id], update: { amount: Blob.arel_table[:amount] + amount } )
这里需要确保数据库里user_id和item_id的组合是唯一约束,这样upsert才能正确识别要更新的记录。
3. 用first_or_initialize手动控制流程
如果你想更精细地控制创建和更新的逻辑,可以用first_or_initialize,它会返回已有的记录或者新初始化的实例,然后你自己决定是否保存:
blob = Blob.where(user_id: user.id, item_id: item.id).first_or_initialize blob.amount += amount blob.save!
这个方法和第一种思路类似,但first_or_initialize不会自动保存新实例,需要你手动调用save!。
总结
记住这个核心点:first_or_create的块仅用于初始化新记录,不要指望它能更新已有的记录。想要更新现有记录,必须单独处理更新逻辑,或者用upsert这种原子性的方法来实现。
内容的提问来源于stack exchange,提问作者calyxofheld

