I'm running Rails 5.1.4, and I have a model that looks like this:
class Quota < ActiveRecord::Base
belongs_to :domain, optional: true
belongs_to :project, optional: true
end
A quota should belong to a domain OR project, but not both (hence setting optional: true
).
However, I can't seem to figure out how to make rails throw errors if an invalid project or domain ID is provided.
Here's what happens:
q = Quota.create!(domain_id: nil, project_id: 'invalid_id')
q.project_id # -> nil
Even if I explicitly pass a project_id, it just magically clears it if it doesn't match a valid project.
I tried adding a custom validation method, but by the time the validation method is called, it has already been set to nil. It doesn't even use the project_id=
method either; I checked.
Is there a way to get Rails to raise an error if the ID is invalid instead of setting it to nil? (while still allowing a nil value)
belongs_to associations are now automatically required: true So if the association is optional, that must be declared, or validation will fail without the presence of the associated model record.
The only difference between hasOne and belongsTo is where the foreign key column is located. Let's say you have two entities: User and an Account. In short hasOne and belongsTo are inverses of one another - if one record belongTo the other, the other hasOne of the first.
If you set the :optional option to true, then the presence of the associated object won't be validated. By default, this option is set to false . otherwise it will be required associated object.
So remember folks, validates is for Rails validators (and custom validator classes ending with Validator if that's what you're into), and validate is for your custom validator methods.
Here is one possible solution
class Quota < ApplicationRecord
belongs_to :domain, optional: true
belongs_to :project, optional: true
validate :present_domain_or_project?
validates :domain, presence: true, unless: Proc.new { |q| q.project_id.present? }
validates :project, presence: true, unless: Proc.new { |q| q.domain_id.present? }
private
def present_domain_or_project?
if domain_id.present? && project_id.present?
errors.add(:base, "Specify a domain or a project, not both")
end
end
end
In the first block, we define the associations and specify optional: true
so we overpass the new Rails 5 behavior of validating the presence of the association.
belongs_to :domain, optional: true
belongs_to :project, optional: true
Then, the first thing we do is just simply eliminating the scenario of both the association attributes (project_id
and domain_id
) are set. This way we avoid hitting the DB twice, in reality, we would only need to hit the DB once.
validate :present_domain_or_project?
...
private
def present_domain_or_project?
if domain_id.present? && project_id.present?
errors.add(:base, "Specify a domain or a project, not both")
end
end
The last part is to check if one of the association is present(valid) in the absence of the other
validates :domain, presence: true, unless: Proc.new { |q| q.project_id.present? }
validates :project, presence: true, unless: Proc.new { |q| q.domain_id.present? }
Regarding:
Is there a way to get Rails to raise an error if the ID is invalid instead of setting it to nil? (while still allowing a nil value)
When using the create! method, Rails raises a RecordInvalid error if validations fail. The exception should be caught and handled appropriately.
begin
q = Quota.create!(domain_id: nil, project_id: 'invalid_id')
rescue ActiveRecord::RecordInvalid => invalid
p invalid.record
p invalid.record.errors
end
The invalid
object should contain the failing model attributes along with the validation errors. Just note that after this block, the value of q
is nil since the attributes were not valid and no object is instantiated. This is normal, predefined behavior in Rails.
Another approach is to use the combination of new
and save
methods. Using the new
method, an object can be instantiated without being saved and a call to save
will trigger validation and commit the record to the database if valid.
q = Quota.new(domain_id: nil, project_id: 'invalid_id')
if q.save
# quota model passes validations and is saved in DB
else
# quota model fails validations and it not saved in DB
p q
p q.errors
end
Here the object instance - q
will hold the attribute values and the validation errors if any.
The best solution I could come up with is this:
class Quota < ActiveRecord::Base
belongs_to :domain, optional: true
belongs_to :project, optional: true
validate :validate_associations
def project_id=(val)
Project.find(val) unless val.nil?
super
end
def domain_id=(val)
Domain.find(val) unless val.nil?
super
end
private
def validate_associations
errors.add(:base, 'Specify a domain or a project, not both') if domain && project
errors.add(:base, 'Must specify a domain or a project') if domain.nil? && project.nil?
end
end
Thanks for helping iron things out @vane-trajkov. I found I really needed to do use the find
method when setting the domain_id or project_id, because Rails was happy to set it to an invalid ID. Using project=
and domain=
work fine as-is since they pretty much ensure the ID has already been set to a valid value.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With