在Rails的Rescue块中捕获外键错误对应的product_id
你遇到的这个场景很常见——批量插入时偶尔触发外键约束错误,想要自动过滤掉无效的product_id后重试。其实我们可以通过解析PostgreSQL返回的错误详情来直接提取出那个不存在的product_id,不用手动排查。
核心思路
PostgreSQL抛出的外键错误消息里会明确指出哪个product_id不存在,格式大概是:
PG::ForeignKeyViolation: ERROR: insert or update on table "group_products" violates foreign key constraint "fk_rails_a3bed1e3ad"
DETAIL: Key (product_id)=(15495) is not present in table "products"
我们可以用正则表达式从错误消息中捕获括号里的数字(也就是无效的product_id)。注意要从e.cause里拿原始的PG错误消息,因为ActiveRecord::InvalidForeignKey只是一个包装类。
完整实现代码
arr = [ {group_id: 51, product_id: 34345}, {group_id: 45, product_id: 22133}, {group_id: 90, product_id: 10045}, {group_id: 2, product_id: 15495}, {group_id: 23, product_id: 25085} ] # 加个重试计数器,防止无限循环 retry_count = 0 max_retries = arr.size # 最多重试次数等于数组元素数量 begin retry_count += 1 GroupProduct.insert_all(arr, unique_by: %i[ group_id product_id ]) rescue ActiveRecord::InvalidForeignKey => e if retry_count <= max_retries # 获取原始PG错误的详情信息 error_detail = e.cause.message # 用正则匹配出无效的product_id if match = error_detail.match(/Key \(product_id\)=\((\d+)\) is not present in table/) invalid_product_id = match[1].to_i # 过滤掉无效的条目 arr = arr.reject { |item| item[:product_id] == invalid_product_id } # 重试插入 retry else # 如果匹配不到product_id相关的错误,说明是其他外键问题,重新抛出错误 raise end else # 超过最大重试次数,抛出明确错误 raise "批量插入group_products时,超过最大重试次数(#{max_retries}次)" end end
关键细节说明
为什么用
e.cause?ActiveRecord::InvalidForeignKey是ActiveRecord对PostgreSQL原生错误的包装,e.cause才是真正的PG::ForeignKeyViolation对象,它的message包含了我们需要的详细错误信息。正则表达式的灵活性
我写的正则/Key \(product_id\)=\((\d+)\) is not present in table/会忽略后面的表名,不管外键指向的是products还是其他表,都能匹配到product_id。如果你的错误消息格式有细微差别,可以调整正则的匹配规则。防止无限重试
加入retry_count和max_retries是为了避免因为未知错误导致无限循环——最坏情况下,我们最多重试数组元素个数次,每次过滤一个无效条目,最后数组为空时就会停止。
内容的提问来源于stack exchange,提问作者Puneet Pandey

