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 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:
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?