rails: Building a has_one association fails on save if target has a validation on foreign_key

Hello,

A gist for the busy can be found here

Assuming the following:

class Post < ActiveRecord::Base
  has_one :comment
end

class Comment < ActiveRecord::Base
  validates_presence_of :post_id
  belongs_to :post
end

This script:

    post = Post.create!
    post.comment = Comment.create!({post_id: post.id})
    post.build_comment

will fail because build is trying to delete the original comment, but that one can’t be deleted because there is a validation on post_id.

This is because prior to deleting, the foreign key of the target association is set to nil and a save operation is performed on the target. I am not sure why this is done. Per my use case, I no longer need the associated target prior to building a new one, and therefore a destroy call should succeed without the need to save, ideally, but not setting the foreign_key to nil will also work.

Furthermore, this could be (accidentally, I should say) mitigated by passing a dependent: :destroy to the Post model. In other words, this:

class Post < ActiveRecord::Base
  has_one :comment, dependent: :destroy
end

will make the build operation successful. There is nothing in the semantics of dependent: :destroy that should affect a target association without affecting the owner, but in this case it does.

The issue is with this particular method, which is called prior to a saving the target association (in our case, the comment instance).

I can submit a PR if this is a clear issue for everyone.

About this issue

  • Original URL
  • State: open
  • Created 10 years ago
  • Reactions: 5
  • Comments: 17 (13 by maintainers)

Most upvoted comments

This is still reproducible and raises the following error:

Failed to remove the existing associated comment. The record failed to save after its foreign key was set to nil.

begin
  require "bundler/inline"
rescue LoadError => e
  $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler"
  raise e
end

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  gem "rails", github: "rails/rails", branch: "main"
  gem "sqlite3"
end

require "active_record"
require "minitest/autorun"
require "logger"

# Ensure backward compatibility with minitest 4.
Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :posts do |t|
  end

  create_table :comments do |t|
    t.references :post
  end
end

class Post < ActiveRecord::Base
  has_one :comment
end

class Comment < ActiveRecord::Base
  validates_presence_of :post_id
  belongs_to :post
end

class BugTest < Minitest::Test
  def test_association_stuff
    post = Post.create!
    post.comment = Comment.create!(post_id: post.id)
    post.build_comment
  end
end

I ran into this too and it had me wondering what was going on. Surely if it’s a has_one association and the associated class has a uniqueness validator on the foreign key then logically a second attempt at creating the association should just fail the foreign key uniqueness validation instead of nullifying the foreign key?

@why-el Please try the following. This should make it work

class Post < ActiveRecord::Base
  has_one :comment, inverse_of: post
end

class Comment < ActiveRecord::Base
  belongs_to :post, inverse_of: comment
  validates :post, presence: true
end