You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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记录)。出现非空约束报错的核心原因有三个:

  1. attempts表使用UUID类型主键,但建表迁移没有配置主键的自动生成规则,新实例化的Attempt对象在保存前id属性为nil,PaperTrail构建版本记录时拿不到有效主键值,给item_id传了null触发约束。
  2. 现有代码存在逻辑问题:option_id是数组类型字段,但strong parameters把它当成普通字符串参数放行,还额外做了<<追加操作,很容易出现类型错误;user_id直接从前端参数取,存在参数伪造风险,也容易漏传触发业务表自身的非空约束;create动作里遗留了byebug调试语句,开发环境下会直接卡在断点。
  3. 如果当初生成PaperTrail的versions表时,把item_id设成了默认的bigint类型,和UUID主键类型不匹配,也会触发同类报错。
修复步骤
  • 补全数据库扩展和UUID主键配置
    首先新建迁移启用pgcrypto扩展(用于PostgreSQL生成UUID):
    class EnablePgcryptoExtension < ActiveRecord::Migration[6.1]
      def change
        enable_extension 'pgcrypto'
      end
    end
    
    再新建迁移给attempts表主键加默认生成规则:
    class AddDefaultUuidValueToAttemptsId < ActiveRecord::Migration[6.1]
      def change
        change_column :attempts, :id, :uuid, default: "gen_random_uuid()", null: false
      end
    end
    
    依次执行rails 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 03:57:36