Rails创建Attempt记录报item_id非空约束PG::NotNullViolation错误
问题概述
通过控制器创建Attempt记录时抛出数据库非空约束报错:ActiveRecord::NotNullViolation Exception: PG::NotNullViolation: ERROR: null value in column "item_id" violates not-null constraint
已排查过公开issue中提到的移除activerecord-suppress_range_error gem的解决方案,当前项目未引入该依赖,该方案不适用。
环境信息
- Rails版本:
6.1.3 - Ruby版本:
2.7.5 - PaperTrail gem版本:
11.0
相关代码
控制器代码(AttemptsController)
# frozen_string_literal: true class AttemptsController < ApplicationController before_action :set_training before_action :set_attempt, only: [:update] def new @attempt = Attempt.new @questions = @training.questions skip_authorization end def create @attempt = @training.attempts.new(attempt_params) @attempt.option_id << attempt_params[:option_id] byebug @attempt.save skip_authorization end private def attempt_params params.require(:attempt).permit(:question_id, :user_id, :option_id) end def set_attempt @attempt = Attempt.find(params[:id]) end def set_training @training = Training.find(params[:training_id]) end end
数据库迁移代码(CreateAttempts)
# frozen_string_literal: true class CreateAttempts < ActiveRecord::Migration[6.1] def change create_table :attempts, type: :uuid do |t| t.references :user, type: :uuid, index: true, foreign_key: true, null: false t.references :training, type: :uuid, index: true, foreign_key: true, null: false t.string :option_id, array: true, default: [], null: false t.string :question_id, null: false t.timestamps end end end
根因定位
报错提到的item_id字段不属于attempts业务表,是PaperTrail版本审计功能生成的versions表的必填字段,作用是关联被记录版本的业务对象(也就是当前要创建的Attempt记录)。出现非空约束报错的核心原因有三个:
attempts表使用UUID类型主键,但建表迁移没有配置主键的自动生成规则,新实例化的Attempt对象在保存前id属性为nil,PaperTrail构建版本记录时拿不到有效主键值,给item_id传了null触发约束。- 现有代码存在逻辑问题:
option_id是数组类型字段,但strong parameters把它当成普通字符串参数放行,还额外做了<<追加操作,很容易出现类型错误;user_id直接从前端参数取,存在参数伪造风险,也容易漏传触发业务表自身的非空约束;create动作里遗留了byebug调试语句,开发环境下会直接卡在断点。 - 如果当初生成PaperTrail的versions表时,把
item_id设成了默认的bigint类型,和UUID主键类型不匹配,也会触发同类报错。
修复步骤
- 补全数据库扩展和UUID主键配置
首先新建迁移启用pgcrypto扩展(用于PostgreSQL生成UUID):
再新建迁移给attempts表主键加默认生成规则:class EnablePgcryptoExtension < ActiveRecord::Migration[6.1] def change enable_extension 'pgcrypto' end end
依次执行class AddDefaultUuidValueToAttemptsId < ActiveRecord::Migration[6.1] def change change_column :attempts, :id, :uuid, default: "gen_random_uuid()", null: false end endrails db:migrate跑完两个迁移。 - 修复控制器参数和赋值逻辑
删掉create动作里的byebug调试语句,修正数组类型参数的permit写法,去掉冗余的追加操作,user_id直接从当前登录用户读取,不要走前端参数,修改后的核心代码如下:def create @attempt = @training.attempts.new(attempt_params) @attempt.user = current_user # 替换成项目内获取当前登录用户的实际方法 if @attempt.save # 替换成保存成功后的实际跳转逻辑 redirect_to training_path(@training), notice: "提交成功" else @questions = @training.questions render :new end skip_authorization end private def attempt_params # 数组类型参数需要按数组格式配置permit规则 params.require(:attempt).permit(:question_id, option_id: []) end - 校验PaperTrail versions表字段类型
找到当初生成PaperTrail versions表的迁移文件,确认item_id字段类型为:uuid,如果是默认的:bigint类型,新建迁移修改字段类型:
跑完迁移后重启服务即可验证修复效果。class ChangeVersionsItemIdTypeToUuid < ActiveRecord::Migration[6.1] def change change_column :versions, :item_id, :uuid # 如果item_id和item_type加了联合索引,记得同步重建索引 end end
内容的提问来源于stack exchange,提问作者maxagno3
相关产品推荐
相关产品推荐

