Rails 4 cascade save association with validation on belongs_to - ruby-on-rails

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

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] )

Why does adding a bang make create work?

Rails convention of adding a bang to the end of a method makes it throw an exception if it fails. But my experience isn't matching that behavior.
My controller for a many-to-many relationship (an audit trail). The relationship object cannot be created, only updated by posting events to the audit trail object. (Which means you create by updating...)
I have a User object and a Foo object I'll call the relationship Bar.
bar=Bar.where(:user_id=>params[:user_id]).where(:foo_id=>params[:foo_id]).first
if bar
authorize! :update, bar
else
user=User.find(params[:user_id])
authorize! :bar_create, user
foo=Foo.find(params[:foo_id])
bar=Bar.create!(:user_id=>user.id, :foo_id=>foo.id)
end
The create method does not work. I debugged, and bar.save worked fine, but the entire point of create is to avoid having to make that second call to save. I experimented, and discovered that create! works just fine.
Edit:
As I continued on, I discovered that create! did not, in fact, always save. No errors in the underlying object, just mysteriously not saved.
I've had to do a create call followed by a save call, which... honestly, I just don't understand.
Edit: Per request, adding model code -- simplified to the relevant statements by removing unnecessary methods, validation calls, and the like. (While writing this, I noticed that I haven't yet added the has_many :through calls, but... doesn't seem like those should be relevant to the issue at hand.
class User < ActiveRecord::Base
has_secure_password
has_many :progresses
end
class Bar < ActiveRecord::Base
belongs_to :user
belongs_to :foo
has_many :bar_events
validates :user, :presence=>true
validates :foo, :presence=>true
scope :user_id, -> (user_id){ where user_id: user_id}
scope :foo_id, -> (foo_id){ where foo_id: foo_id}
end
class Foo < ActiveRecord::Base
end
There is a validation error or more in one of the associations in Bar foo or user. Try inspecting those objects:
bar=Bar.create!(:user_id=>user.id, :foo_id=>foo.id)
puts bar.errors.inspect, bar.user.errors.inspect, bar.foo.errors.inspect
That will print the errors of all those objects to the terminal running rails server. The only reason create would not save is due to validation errors in itself or nested associations.

Rails Active Record Nested Attributes Validation which are in the same request

I have two models house and booking.Everything is okey over booking_date validation. But when I try to update or create multi booking in the same request. Validation can't check the invalid booking in the same request params.
Let give an example assume that booking table is empty.
params = { :house => {
:title => 'joe', :booking_attributes => [
{ :start_date => '2012-01-01', :finish_date => '2012-01-30 },
{ :start_date => '2012-01-15', :finish_date => '2012-02-15 }
]
}}
Second booking also save but its start_date is between first booking interval. When I save them one by one validation works.
class House < ActiveRecord::Base
attr_accessible :title, :booking_attributes
has_many :booking
accepts_nested_attributes_for :booking, reject_if: :all_blank, allow_destroy: true
end
class Booking < ActiveRecord::Base
belongs_to :house
attr_accessible :start_date, :finish_date
validate :booking_date
def booking_date
# Validate start_date
if Booking.where('start_date <= ? AND finish_date >= ? AND house_id = ?',
self.start_date, self.start_date, self.house_id).exists?
errors.add(:start_date, 'There is an other booking for this interval')
end
# Validate finish_date
if Booking.where('start_date <= ? AND finish_date >= ? AND house_id = ?',
self.finish_date, self.finish_date, self.house_id).exists?
errors.add(:finish_date, 'There is an other booking for this interval')
end
end
end
I google nearly 2 hours and could not find anything. What is the best approach to solve this problem?
Some resources
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
http://railscasts.com/episodes/196-nested-model-form-part-1
This was only a quick 15-minutes research on my part, so I may be wrong, but I believe here's the root cause of your problem:
What accepts_nested_attributes_for does under the hood, it calls 'build' for new Booking objects (nothing is validated at this point, objects are created in memory, not stored to db) and registers validation and save hooks to be called when the parent object (House) is saved. So, in my understanding, all validations are first called for all created objects (by calling 'valid?' for each of them. Then, again if I get it right, they are saved using insert_record(record,false) which leads to save(:validate => false), so validations are not called for the 2nd time.
You can look at the sources inside these pages: http://apidock.com/rails/v3.2.8/ActiveRecord/AutosaveAssociation/save_collection_association,
http://apidock.com/rails/ActiveRecord/Associations/HasAndBelongsToManyAssociation/insert_record
You validations call Booking.where(...) to find the overlapping dates-ranges. At this point the newly created Booking objects are still only in memory, not saved to the db (remember, we are just calling valid? for each of them in the loop, saves will be done later). Thus Booking.where(...) which runs a query against a DB doesn't find them there and returns nothing. Thus they all pass valid? stage and then saved.
In a nutshell, the records created together in such a way will not be cross-validated against each other (only against the previously existing records in the database). Hence the problem you see.
Thus either save them one-by-one, or check for such date-overlapping cases among the simultaneously created Bookings yourself before saving.

accepts_nested_attributes_for :reject_if to trigger another method

I've got a multi-level nested form using formtastic_cocoon (jquery version of formtastic).
I am trying to do some validation in the sense of
if value is_numeric do
insert into database
else do
database lookup on text
insert id as association
end
I was hoping tha the accepts_nested_attributes_for would have an :if option, but apparently there is only the :reject_if.
Is there a way to create a validation like I describe as part of the accepts_nested_attributes_for??
-----------------------Updated as per Zubin's Response ---------------------------
I believe Zubin is on the right track with a method, but I can't seem to get it working just right. The method I am using is
def lookup_prereq=(lookup_prereq)
return if lookup_prereq.blank?
case lookup_prereq
when lookup_prereq.is_a?(Numeric) == true
self.task_id = lookup_prereq
else
self.task = Task.find_by_title(lookup_prereq)
end
end
When I trigger this function, the self.task_id is being put in the database as '0' rather than the Task.id.
I'm wondering if I'm missing something else.
I'm not completely sure that the method is actually being called. Shouldn't I need to say
lookup_prereq(attr[:prereq_id)
at some point?
-------------------further edit -----------------------
I think from what I can find that the method is called only if it is named with the same name as the value for the database, therefore I've changed the method to
def completed_task=(completed_task)
Unfortunately this is still resulting in 0 as the value in the database.
Sounds like you need a method in your nested model to handle that, eg:
class Post < ActiveRecord::Base
has_many :comments
accepts_nested_attributes_for :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :author
def lookup_author=(lookup_author)
return if lookup_author.blank?
case lookup_author
when /^\d+$/
self.author_id = lookup_author
else
self.author = Author.find_by_name(lookup_author)
end
end
end

Calling valid? on new records, but ignore validity of built associations

I have three tables: tasks, departments, and department_tasks. I need to call "valid?" on new task objects, but I want to ignore the validity of any "built" department_tasks. We are doing bulk uploads, and so we load everything or nothing.
As we loop through the Excel file we are reading in, we build the new "Task" according to the values in each row. With each row, there may be an associated department for the task; if there is, we "build" the associated department_task object like so:
new_task.department_tasks.build(:department_id => d.id)
At the end of the loop, we test the validity of the new "task" object by calling "valid?"
new_task.valid?
If the task is valid, it goes in the "good" pile; if it's bad, it goes on the "bad" pile.
The problem is, we haven't saved the task and therefore it has no :id. Without an id, the "built" department_task is invalid (:department_id and :task_id must both be present).
I need to know how I can call "valid?" or test validity of the "new_task" object without the validation cascading down to the "task_department" associated object which cannot be valid before task is saved.
You can skip individual validations using :if or :unless
validates_presence_of :department_id,
:unless => lambda { |record| record.new_record? }
If I understand correctly, you have something like this:
class Task < ActiveRecord::Base
has_many :department_tasks
has_many :departments, :through => :department_tasks
validates_associated :department_tasks
end
class DepartmentTask < ActiveRecord::Base
belongs_to :task
belongs_to :department
validates_presence_of :department_id, :task_id
end
When Task is new the associated validation in DepartmentTask fails because task_id is nil. Correct?
I don't see an easy way around this. The most obvious solution is to just remove the validates_presence_of for task_id. If the only way that you create DepartmentTasks is to build them through the Task model, the presence_of validation seems unnecessary, since Rails will always add the task_id when Task is saved.
Another option is to wrap it in a transaction, create the new task (so it has an ID), then build and validate DepartmentTask, and rollback if invalid.
You should assign an object to your AR queries to check it's validity, also use .new(:department_id => d.id)
myrecord = new_task.department_tasks.new(:department_id => d.id)
if myrecord.save!
"good pile"
else
"bad pile"
end

Resources