Rails 4: Difference between validates presence on id or association - ruby-on-rails

If I have a 'belongs_to' association in a model, I'd like to know the notional difference between validating an association:
class Topping < ActiveRecord::Base
belongs_to :pancake
validates :pancake, presence: true
...
and validating the associated model id:
class Topping < ActiveRecord::Base
belongs_to :pancake
validates :pancake_id, presence: true
...
Motivation:
Some code which assigned a topping to a pancake stopped working at some time in the past. Changing the validation from association to id 'fixed' the problem, but I'd like to know the deeper reason why.
(FYI, when stepping into the code the pancake was valid and in the database and the topping responded to both .pancake and .pancake_id appropriately. Both the push operator (pancake.toppings << topping) and manual assignment and save (topping.pancake = pancake; topping.save) failed with a pancake missing validation error.)

Investigating further, I found that the 'presence' validator resolves to 'add_on_blank':
http://apidock.com/rails/ActiveModel/Errors/add_on_blank
def add_on_blank(attributes, options = {})
Array(attributes).each do |attribute|
value = #base.send(:read_attribute_for_validation, attribute)
add(attribute, :blank, options) if value.blank?
end
end
This does what it says: adds a validation error if the property in question is blank?
This means it's simply an existence check. So if I validate an id, that id has to exist. That means:
topping.pancake = Pancake.new
topping.valid?
would return false. However:
topping.pancake_id = -12
topping.valid?
would return true. On the other hand, if I validate the object the exact opposite would be true. Unless -12 is a valid index, in which case ActiveRecord would automatically load it from the database on receipt of the 'pancake' message.
Moving on to my issue, further investigation showed that blank? delegates to empty?, and indeed someone had defined empty? on the pancake, returning true if there are no toppings.
Culprit found, and something about Rails learned.

Related

How to tell which of the associated models fails my ActiveRecord validation?

Say I have the following models:
class Race < ApplicationRecord
has_many :horses
end
class Horse < ApplicationRecord
belongs_to :race
validates :name, presence: true
end
Now with my REST API I'm creating a Race object and associate multiple horses. One of the Horses fails validation, which adds the error.
Adding an error means adding entries to errors.details and errors.messages, where errors is a field of the Race model. Both these fields are hashes, with horses.name as a key and details of the error(s) and error message(s) as values, respectively.
I'm looking for a way to find, which of the associated Horse models failed the validation so that I can provide a comprehensive error message. A reference, id, or even an index, would be enough.
race = Race.create race_params
race.errors.messages
=> {'horses.name' => ['Can't be blank']}
race.horces[0].errors.messages
=> {'name' => ['Can't be blank']}
to get records with error, simply filter race.horses
with_error = race.horses.select{|h| h.errors.messages.present?}
index = race.horses.index( with_error[0] )

Rails 4 cascade save association with validation on belongs_to

I'm having troubles autosaving objects which are created this way. Consider having following models:
class SearchTerm < ActiveRecord::Base
has_many :search_term_occurrences, dependent: :destroy
end
class SearchTermOccurrence < ActiveRecord::Base
belongs_to :search_term
validates :search_term, presence: true # Problematic validation
validates_associated :search_term # See Gotchas in answer
end
So when I try to do the following:
term = SearchTerm.new term: 'something'
term.search_term_occurrences << [SearchTermOccurrence.new, SearchTermOccurrence.new]
term.save!
I get the following:
ActiveRecord::RecordInvalid: Validation failed: Search term occurrences is invalid, Search term occurrences is invalid
But when I omit the validation on belongs_to search_term. Everything is saved properly.
My question is: How to save parent object and its association (newly created) whilst having validations on child objects without saving associated objects one by one and then saving parent object within transaction? I want Rails to handle the transactional logic.
So after some playing with Rails console I've found an answer. The problem was the order of saving.
I chose different approach of saving: Instead of going from top to bottom (SearchTerm has_many -> SearchTermOccurrences) I tried to save it from the bottom up (SearchTermOccurrence belongs_to -> SearchTerm). So instead of doing this:
term = SearchTerm.new term: 'something'
term.search_term_occurrences << [SearchTermOccurrence.new, SearchTermOccurrence.new]
term.save!
I did this:
occurrence = SearchTermOccurrence.new
occurence.search_term = SearchTerm.new(term: 'search query')
occurence.save!
This suddenly worked. So I'm guessing that Rails can only validate parent records in belongs_to association while newly created but cannot validate has_many child records.
EDIT 1
Gotchas:
If you are choosing this cascade save approach, make sure you add validates_associated for belongs_to record otherwise you might end up with inconsistent database, since validates :search_term, presence: true can pass even though SearchTerm itself is invalid. I've added validates_associated to SearchTermOccurrence.
you should save the term instance first before adding SearchTermOccurrence objects into it.
term = SearchTerm.new term: 'something'
term.save!
term.search_term_occurrences << [SearchTermOccurrence.new, SearchTermOccurrence.new]
because SearchTermOccurrence have validation of it's parent SearchTerm present true so SearchTerm.new will not create any ID of SearchTerm into database until it is saved.
try above code

ActiveRecord validates inclusion in list - list isn't updated after new associated model created

I have a Company model and an Employer model. Employer belongs_to :company and Company has_many :employers. Within my Employer model I have the following validation:
validates :company_id, inclusion: {in: Company.pluck(:id).prepend(nil)}
I'm running into a problem where the above validation fails. Here is an example setup in a controller action that will cause the validation to fail:
company = Company.new(company_params)
# company_params contains nested attributes for employers
company.employers.each do |employer|
employer.password = SecureRandom.hex
end
company.employers.first.role = 'Admin' if client.employers.count == 1
company.save!
admin = company.employers.where(role: 'Admin').order(created_at: :asc).last
admin.update(some_attr: 'some_val')
On the last line in the example code snippet, admin.update will fail because the validation is checking to see if company_id is included in the list, which it is not, since the list was generated before company was saved.
Obviously there are ways around this such as grabbing the value of company.id and then using it to define admin later, but that seems like a roundabout solution. What I'd like to know is if there is a better way to solve this problem.
Update
Apparently the possible workaround I suggested doesn't even work.
new_company = Company.find(company.id)
admin = new_company.employers.where(role: 'Admin').order(created_at: :asc).last
admin.update
# Fails validation as before
I'm not sure I understand your question completely, but there is an issue in this part of the code:
validates :company_id, inclusion: {in: Company.pluck(:id).prepend(nil)}
The validation is configured on the class-level, so it won't work well with updates on that model (won't be re-evaluated on subsequent validations).
The docs state that you can use a block for inclusion in, so you could try to do that as well:
validates :company_id, inclusion: {in: ->() { Company.pluck(:id).prepend(nil) }}
Some people would recommend that you not even do this validation, but instead, have a database constraint on that column.
I believe you are misusing the inclusion validator here. If you want to validate that an associated model exists, instead of its id column having a value, you can do this in two ways. In ActivRecord, you can use a presence validator.
validates :company, presence: true
You should also use a foreign key constraint on the database level. This prevents a model from being saved if there is no corresponding record in the associated table.
add_foreign_key :employers, :companies
If it gets past ActiveRecord, the database will throw an error if there is no company record with the given company_id.

ActiveRecord, validates_uniqueness_of :name not catching non-uniquness if I have a capitalize method

I have a simple capitalize method so that when user submits a new band in the band page it returns it with the first letter capitalized.
Inside my Band class I also have a validates_uniqueness_of :band_name to see if there is already a band with the same entry. See code below:
class Band < ActiveRecord::Base
has_and_belongs_to_many :venues
validates :band_name, :presence => true
before_save :title_case
validates_uniqueness_of :band_name
private
def title_case
self.band_name.capitalize!
end
end
So if I type in someband, it creates it and displays it as Someband. If I type someband again, ActiveRecord sees it as unique and I'll get another Someband. The only way it works is if I type Someband. How would I remedy this situation?
I think what you want to do is this
validates_uniqueness_of :band_name, :case_sensitive :false, allow_blank: false
Take a look at http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html
:case_sensitive - Looks for an exact match. Ignored by non-text
columns (true by default).
The reason your code doesn't work is because validations happen before the before_save callbacks are triggered. Check out the list of ActiveRecord::Callbacks for the order in which things are called.
MZaragoza's answer is a great option for making your validation work regardless of what casing your users might enter. It will prevent things like "someband" and "SomeBand" from being added. I recommend including that as part of your solution.
Another option very similar to the code you already have is to switch to using the before_validation callback:
before_validation :title_case
I highly recommend using the before_validation callbacks instead of before_save callbacks whenever data changes that may be relevant to your validation rules, regardless of what other changes you make. That ensures that you are checking that actual state of the model that you plan to save to the database.
You can use attribute setter instead of before_save callback to capitalize your value without postponing.
def band_name=(value)
self['band_name'] = value && value.mb_chars.capitalize
end

Rails conditional validation in model

I have a Rails 3.2.18 app where I'm trying to do some conditional validation on a model.
In the call model there are two fields :location_id (which is an association to a list of pre-defined locations) and :location_other (which is a text field where someone could type in a string or in this case an address).
What I want to be able to do is use validations when creating a call to where either the :location_id or :location_other is validated to be present.
I've read through the Rails validations guide and am a little confused. Was hoping someone could shed some light on how to do this easily with a conditional.
I believe this is what you're looking for:
class Call < ActiveRecord::Base
validate :location_id_or_other
def location_id_or_other
if location_id.blank? && location_other.blank?
errors.add(:location_other, 'needs to be present if location_id is not present')
end
end
end
location_id_or_other is a custom validation method that checks if location_id and location_other are blank. If they both are, then it adds a validation error. If the presence of location_id and location_other is an exclusive or, i.e. only one of the two can be present, not either, and not both, then you can make the following change to the if block in the method.
if location_id.blank? == location_other.blank?
errors.add(:location_other, "must be present if location_id isn't, but can't be present if location_id is")
end
Alternate Solution
class Call < ActiveRecord::Base
validates :location_id, presence: true, unless: :location_other
validates :location_other, presence: true, unless: :location_id
end
This solution (only) works if the presence of location_id and location_other is an exclusive or.
Check out the Rails Validation Guide for more information.

Resources