Rails 5.2升级后新实例ActiveRecord关联未自动保存问题
问题:Rails 5.0升级到5.2后,关联对象创建顺序变更导致非空约束错误
升级背景
- Rails版本:从5.0.7.2升级到5.2.8.1
- Ruby版本:从2.5.0升级到2.7.7
原工作代码(Rails 5.0中正常运行)
def up Test.find_by(name: 'Rewarded_Ad_Goal').try(:destroy) choice_1565 = Choice.new(weight: 1, value: '0_Control') choice_1566 = Choice.new(weight: 1, value: '1_Alt') test = Test.create!(name: 'Rewarded_Ad_Goal', choices: [choice_1565, choice_1566], aasm_state: 'stopped') end
Rails 5.2中触发的错误
irb(main):004:0> test = Ingot::Test.create!(name: 'Rewarded_Ad_Goal', choices: [choice_1565, choice_1566], aasm_state: 'stopped') (0.3ms) BEGIN Ingot::Test Exists (0.4ms) SELECT 1 AS one FROM "ingot_tests" WHERE "ingot_tests"."name" = $1 LIMIT $2 [["name", "Rewarded_Ad_Goal"], ["LIMIT", 1]] Ingot::Test Exists (0.4ms) SELECT 1 AS one FROM "ingot_tests" WHERE "ingot_tests"."name" = $1 LIMIT $2 [["name", "Rewarded_Ad_Goal"], ["LIMIT", 1]] SQL (1.8ms) INSERT INTO "ingot_tests" ("aasm_state", "name", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["aasm_state", "stopped"], ["name", "Rewarded_Ad_Goal"], ["created_at", "2023-04-10 19:17:33.630832"], ["updated_at", "2023-04-10 19:17:33.630832"]] (0.3ms) ROLLBACK Traceback (most recent call last): 1: from (irb):4 ActiveRecord::NotNullViolation (PG::NotNullViolation: ERROR: null value in column "default_choice_id" violates not-null constraint) DETAIL: Failing row contains (311, null, null, stopped, null, Rewarded_Ad_Goal, 2023-04-10 19:17:33.630832, 2023-04-10 19:17:33.630832). : INSERT INTO "ingot_tests" ("aasm_state", "name", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"
相关模型定义
test.rb
class Test < ActiveRecord::Base include AASM include Contracts belongs_to :default_choice, class_name: 'Ingot::Choice' has_many :assignments, class_name: 'Ingot::Assignment', dependent: :delete_all has_many :choices, class_name: 'Ingot::Choice', dependent: :destroy, autosave: true has_many :filters, class_name: 'Ingot::Filter', dependent: :destroy accepts_nested_attributes_for :default_choice, allow_destroy: true, reject_if: :all_blank accepts_nested_attributes_for :choices, allow_destroy: true, reject_if: :all_blank accepts_nested_attributes_for :filters, allow_destroy: true, reject_if: :all_blank validates_length_of :choices, minimum: 2, message: 'are missing. You must specify at least two alternatives.' validates_presence_of :default_choice validates_presence_of :name validates_uniqueness_of :name before_validation :set_default_choice, unless: :default_choice def set_default_choice # confirmed this gets called puts "Setting default choice to #{choices.first.id}" # <== nil puts "Setting default choice to #{choices.first.weight}" puts "Setting default choice to #{choices.first.value}" puts "Setting default choice to #{choices.first.test_id}" # <== nil self.default_choice ||= choices.first end end
choice.rb
class Choice < ActiveRecord::Base belongs_to :test, class_name: 'Ingot::Test' has_many :assignments, class_name: 'Ingot::Assignment', dependent: :destroy validates_numericality_of :weight validates_presence_of :value validates_presence_of :weight before_validation :set_default_weight def test super || Test.where(default_choice_id: id).first end private def set_default_weight self.weight ||= 1 end end
问题分析与解决方案
核心原因:Rails 5.1+的关联保存顺序变更
在Rails 5.0中,创建父对象并关联子对象时,Rails会先保存子对象(获取id),再保存父对象,因此before_validation回调能拿到子对象的id。但从Rails 5.1开始,关联对象的保存顺序发生了关键变化:父对象会先完成验证并尝试保存,之后才处理子对象的保存。这就导致set_default_choice回调触发时,choices还只是内存中的未持久化对象,没有id和test_id,无法满足default_choice_id的非空约束。
解决办法
调整回调逻辑,手动提前保存子对象:
修改set_default_choice方法,先保存所有子对象再关联默认选项:def set_default_choice choices.each(&:save!) if choices.any? # 先持久化子对象 self.default_choice ||= choices.first end注意:需确保子对象的验证规则都能通过,否则会抛出保存异常。
改用嵌套属性创建关联对象:
使用choices_attributes替代直接传入子对象实例,Rails会自动处理正确的保存顺序,确保default_choice能关联到已持久化的子对象:test = Test.create!( name: 'Rewarded_Ad_Goal', aasm_state: 'stopped', choices_attributes: [ { weight: 1, value: '0_Control' }, { weight: 1, value: '1_Alt' } ] )临时调整数据库约束(不推荐长期使用):
若允许临时修改数据库,可先将default_choice_id改为允许null,创建对象后再设置默认选项,最后改回非空约束。此方式需要额外迁移步骤,仅适合临时过渡。
内容的提问来源于stack exchange,提问作者PragmaticProgrammer
相关产品推荐
相关产品推荐

