When working with ActiveRecord models, there are occasions where using either a foreign key or an object to denote an association is desirable. It may be the only available information about the foreign record is the key, or it may be that only a “new” (i.e. not persisted) object has been created. To ensure the association is present, you need to validate the presence of both the association (i.e. the Object) and the foreign key.
Assuming we have a User
model with a has_many
association to a Post
model,
the solution might look something like this:
class Post < ActiveRecord::Base
belongs_to :user, :inverse_of => :posts
validates :user, :presence => {:if => proc{|o| o.user_id.blank? }}
validates :user_id, :presence => {:if => proc{|o| o.user.blank? }}
end
Here, we’re validating the existence of the User
association: first by
checking the presence of the object if the foreign key is blank; second by
checking the presence of the foreign key if the User
association is blank.
This isn’t a great solution, because it results in two error messages if the validation fails.
A better solution is to create a validator. To do that in Rails, the validator
file must be created under lib/validators
, and the filename must be the snake case version of the class name (i.e.
ExistenceValidator => existence_validator.rb).
class ExistenceValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value.blank? && record.send("#{attribute}_id".to_sym).blank?
record.errors[attribute] << I18n.t("errors.messages.existence")
end
end
end
lib/validators/existence_validator.rb
As you can see, we subclass ActiveModel::EachValidator
and override the
validate_each
method.
Inside the method, we add to the errors
list if either the value of what we
are testing is #blank?
or if the foreign key is #blank?
Just add the desired
error message to your config/locales/en.yml
file (adjust for language).
en:
errors:
messages:
existence: "must exist as an object or foreign key"
config/locales/en.yml
The above solution may be a little tricky if you’re not too familiar with metaprogramming in Ruby, so we’ll look at it a little more closely here.
record.send("#{attribute}_id".to_sym).blank?
In Ruby, the #send
method takes a symbol
as its first argument, and it
“sends” that symbol as a message to an object, (i.e. it calls a method of the
same name as the symbol).
We are defining the message to be sent with the "#{attribute}_id".to_sym
bit –
in our use case, it will end up being user_id
. That message gets sent to the
model instance the validation is defined within and determined if it is
blank?
.
When fully translated, it looks like this:
record.user_id.blank?
Now that we’ve defined our new validation, let’s use it. We can remove the two
presence
validators we had before and replace them with the more succinct
existence
validator.
class Post < ActiveRecord::Base
belongs_to :user, :inverse_of => :posts
validates :user, :existence => true
end
Post model using new validator
The solution is pretty good, but it assumes the foreign key is the same name as
the association, only with an appended _id
(e.g. user
and user_id
).
For the occasion when you need to get around this, you’ll want to create a sort of “in class” validator for your odd association.
class Post < ActiveRecord::Base
belongs_to :author, :class_name => User, :inverse_of => :posts
validate :must_have_author
private
def must_have_author
if author.blank? && user_id.blank?
self.errors.add(:author, I18n.t("errors.messages.existence"))
end
end
end
Post model using specific validator
At this point you should have a solution for 90% of what you’re going to run into with regard to validating associations. If you see something I missed, or if there’s a way I can improve anything above, please leave a comment and I’ll update the post accordingly.