This behavior has been around for awhile (since at least 3.2.x; I didn't 
test further back). I'm a bit surprised this didn't trip my up before, but 
I'm sure it did and I just took the quick way out.

To set some groundwork here is what the Active Record Validations Guide 
<http://guides.rubyonrails.org/active_record_validations.html#presence> 
presence validation (emphasis mine):
 

> If you want to be sure that an association is present, you'll need to test 
> whether the associated object itself is present, and not the foreign key 
> used to map the association. 


> class LineItem < ActiveRecord::Base
>  belongs_to :order
>  validates :order, presence: true
> end
>
> *In order to validate associated records whose presence is required, you 
> must specify the :inverse_of option for the association*: 


> class Order < ActiveRecord::Base
>   has_many :line_items, inverse_of: :order
> end
>
> If you validate the presence of an object associated via a has_one or 
> has_many relationship, it will check that the object is neither blank? nor 
> marked_for_destruction?.


However, using inverse_of doesn't work. Note that this section of docs 
states that only has_one and has_many relationships will check only blank? 
and marked_for_destruction?.

Compare this with the API docs for the same validation 
<http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_presence_of>
:

Validates that the specified attributes are not blank (as defined by 
> Object#blank?), and, if the attribute is an association, that the 
> associated object is not marked for destruction. Happens by default on save.


That gives the indication that it doesn't seem to matter if you use 
inverse_of or not. Nor, will the presence validator ever validate the 
association. However, this causes some very odd behavior. Namely, it's 
possibly to save a record which, when pulled immediately back out of the 
database is not valid. This is because a null value has been saved in the 
place of the association's reference id.

Here's code which demonstrates this:

# Activate the gem you are reporting the issue against.
gem 'activerecord', '4.1.6.rc2'
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|
    t.string :name
  end


  create_table :comments do |t|
    t.integer :post_id
  end
end

class Post < ActiveRecord::Base
  has_many :comments, inverse_of: :post
  validates_presence_of :name
end

class Comment < ActiveRecord::Base
  belongs_to :post, inverse_of: :comments
  validates_presence_of :post
end

class BugTest < Minitest::Test
  def setup
    Post.delete_all
    Comment.delete_all
  end

  def test_association_persists_valid_objects
    post = Post.new(name: "Demoing confusing behavior")
    assert post.valid?
    refute post.persisted?

    comment = Comment.create(post: post)
    assert comment.valid?
    assert comment.persisted?
    assert post.persisted?
    assert_equal 1, Post.count
    assert_equal 1, Comment.count
  end


  def test_valid_present_belongs_to_association
    post = Post.new
    refute post.valid?, "Post was valid but should be invalid"

    Comment.create post: post
    assert_equal 0, Comment.count, "Comment was saved with invalid Post"
  end


  def test_showing_why_presence_should_mean_valid
    comment = Comment.create(post: Post.new)

    if (persisted_comment = Comment.first)
      assert_equal comment, persisted_comment, "Assigned and persisted 
comments are different"
      assert persisted_comment.valid?, "Persisted comment isn't valid"
    else
      refute comment.valid?, "Assigned comment is valid"
    end
  end
end


Searches online seem to just take this as by design. Yet, it doesn't really 
make sense. The suggestions to "workaround" this are to either set the 
presence validation on the reference id field or adding an association 
validation.

Setting the presence validation on the reference id field has the downside 
that it doesn't verify that the associated model object exists (without 
foreign keys defined in the database). The alternative is to use both 
presence and associated validations:

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


Am I completely missing something with this behavior?

-- 
You received this message because you are subscribed to the Google Groups "Ruby 
on Rails: Core" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to rubyonrails-core+unsubscr...@googlegroups.com.
To post to this group, send email to rubyonrails-core@googlegroups.com.
Visit this group at http://groups.google.com/group/rubyonrails-core.
For more options, visit https://groups.google.com/d/optout.

Reply via email to