As I understand it, the build method can be used to build up a collection of associated records before saving. Then, when calling save, all the child records will be validated and saved, and if there are validation errors the parent record will have an error reflecting this. First question is, is this correct?
But my main question is, assuming the above is valid, is it possible to do the same thing with updates, not creates? In other words, is there a way to update several records in a collection associated with a parent record, then save the parent record and have all the updates take place at once (with an error in the parent if there are validation errors in the children)?
Edit: So to summarize, I'm wondering the right way to handle a case where a parent record and several associated child records need to be updated and saved all at once, with any errors aborting the whole save process.
Firstly +1 to #andrea for Transactions - cool stuff I didn't know
But easiest way here to go is to use accepts_nested_attributes_for method for model.
Lets make an example. We have got two models: Post title:string and Comment body:string post:references
lets look into models:
class Post < ActiveRecord::Base
has_many :comments
validates :title, :presence => true
accepts_nested_attributes_for :comments # this is our hero
end
class Comment < ActiveRecord::Base
belongs_to :post
validates :body, :presence => true
end
You see: we have got some validations here. So let's go to rails console to do some tests:
post = Post.new
post.save
#=> false
post.errors
#=> #<OrderedHash {:title=>["can't be blank"]}>
post.title = "My post title"
# now the interesting: adding association
# one comment is ok and second with __empty__ body
post.comments_attributes = [{:body => "My cooment"}, {:body => nil}]
post.save
#=> false
post.errors
#=> #<OrderedHash {:"comments.body"=>["can't be blank"]}>
# Cool! everything works fine
# let's now cleean our comments and add some new valid
post.comments.destroy_all
post.comments_attributes = [{:body => "first comment"}, {:body => "second comment"}]
post.save
#=> true
Great! All works fine.
Now lets do the same things with update:
post = Post.last
post.comments.count # We have got already two comments with ID:1 and ID:2
#=> 2
# Lets change first comment's body
post.comments_attributes = [{:id => 1, :body => "Changed body"}] # second comment isn't changed
post.save
#=> true
# Now let's check validation
post.comments_attributes => [{:id => 1, :body => nil}]
post.save
#=> false
post.errors
#=> #<OrderedHash {:"comments.body"=>["can't be blank"]}>
This works!
SO how can you use it. In your models the same way, and in views like common forms but with fields_for tag for association. Also you can use very deep nesting for your association with validations nd it will work perfect.
Try using validates_associated :some_child_records in your Patient class.
If you only want this to occur on updates, just use the :on option like validates_associated :some_child_records, :on => :update
More info here:
http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_associated
Related
I have a class student with has_many tests. The test class has a student_id, marks, name. Here the test name should be unique. The test is a nested attribute for student. So the parameters are this way:
:student => {:first_name => "abc",
:email => "dfsdf#sfdsdsd.bbb",
:tests_attributes => { "0" => {:name => "bgc", :marks => "470"}}}
I have a problem with update. If I update_attributes with the tests_attributes, it throws a validation error saying the name for test is not unique. I am actually addressing the same record here. How do I overcome this?
Without seeing your models (& validations), it's going to be quite difficult to diagnose your error directly.
--
Nested Attributes
We've done something like this, and found that your nested data is passed to the child model as if it were receiving a new object (without being nested). This means if you've got validates uniqueness for that model, it should be okay:
#app/models/test.rb
Class Test < ActiveRecord::Base
belongs_to :student
validates :name, uniqueness: true
end
Reason I write this is because there's a method called inverse_of, which basically allows you to access the parent model data in your child model
--
Update
I think the problem will likely lie with your use of update_attributes. Problem being you're trying to update both the student and the test attributes at one time.
I'm not sure exactly why this would be a problem, but I'd test this:
#app/controllers/students_controller.rb
class StudentsController < ApplicationController
def update
#student = Student.find params[:id]
#student.test.update(name: params[:test_name], marks: params[:marks])
end
end
I think if you can explain your methodology a little more, it will be much more helpful. I.E are you trying to update student or test? If you're updating student & adding a new test, how are you updating the studet?
Thanks for the reply guys. I ended up finding the answer myself. I did have a uniqueness validation for name.
I had a situation where initially I wouldn't know the student but have only his details. So I would have to create this hash and pass it to update. The trick to not trying to create a new record for the same name in test is to pass the actual record's ID along with it. This solved the problem
Nested Attributes
I think the problem with nested_attributes. For update need to pass nested_attributes with ID.
Ex.
:student => {:first_name => "abc",
:email => "dfsdf#sfdsdsd.bbb",
:tests_attributes => { "0" => {id: 1, :name => "bgc", :marks => "470"}}}
I have tried below-given example it is worked for me:
Update
#app/controllers/students_controller.rb
class StudentsController < ApplicationController
def update
#student = Student.find params[:id]
#student.update_attributes(student_params)
end
private
def student_params
params.require(:student).permit(:first_name, :email,
tests_attributes: [:id, :name, :marks])
end
end
I have the following mongoid model, with a scoped validation to prevent multiple votes on one bill. Each vote belongs to a user and a group:
class Vote
include Mongoid::Document
field :value, :type => Symbol # can be :aye, :nay, :abstain
field :type, :type => Symbol # TODO can delete?
belongs_to :user
belongs_to :polco_group
embedded_in :bill
validates_uniqueness_of :value, :scope => [:polco_group_id, :user_id, :type]
end
The user has the following method to add a vote to a bill:
def vote_on(bill, value)
if my_groups = self.polco_groups # test to make sure the user is a member of a group
my_groups.each do |g|
# TODO see if already voted
bill.votes.create(:value => value, :user_id => self.id, :polco_group_id => g.id, :type => g.type)
end
else
raise "no polco_groups for this user" # #{self.full_name}"
end
end
and a Bill class which embeds many :votes. This is designed to allow a user to associate their vote with different groups ("Ruby Coders", "Women", etc.) and is working well, except the database currently allows a user to vote multiple times on one bill. How can I get the following to work?
u = User.last
b = Bill.last
u.vote_on(b,:nay)
u.vote_on(b,:nay) -> should return a validation error
Most probably validators on Vote are not getting fired. You can confirm that by adding a validates function and outputting something or raising an exception in it.
class Vote
validate :dummy_validator_to_confirmation
def dummy_validator_to_confirmation
raise "What the hell, it is being called, then why my validations are not working?"
end
end
If after creating above validations User#vote_on doesn't raises exception, it confirms that callbacks are not fired for Vote via vote_on method. You need to change your code to fire callbacks on Vote. Probably changing it to resemble following would help:
def vote_on(bill, value)
if my_groups = self.polco_groups # test to make sure the user is a member of a group
my_groups.each do |g|
# TODO see if already voted
vote = bill.votes.new(:value => value, :user_id => self.id, :polco_group_id => g.id, :type => g.type)
vote.save
end
else
raise "no polco_groups for this user" # #{self.full_name}"
end
end
There is an open issue on mongoid github issue tracker to allow cascade callbacks to embedded documents. Right now callbacks are only fired on document on which persistence actions are taking place on.
If I have a model Department with columns user_id and group_id
When the action tries to save an entry into this model that already exists, i.e. 1 (user_id), 22 (group_id) already exists, at that time I want to raise a violation. What is the way to do this in rails?
Following is the code I am using to save right now:
if #department.save
flash[:notice] = "Successfully created department."
redirect_to #department
else
render :action => 'new'
end
Department model
class Department < ActiveRecord::Base
belongs_to :group
belongs_to :user
end
But I guess what you want is to validate that there's only one department with pair user_id = 1, group_id = 22. This could be achieved with:
validates_uniqueness_of :user_id, :scope => [:group_id]
edit
Now, I might have misunderstood you and maybe all you want is validates_uniqueness_of check. Let me know if I miss something.
Active Records has new_record? method to determine whether object is saved already (whether record in database exists for it).
I copy this demo from rails tutorial:
>> p = Person.new(:name => "John Doe")
=> #<Person id: nil, name: "John Doe", created_at: nil, :updated_at: nil>
>> p.new_record?
=> true
>> p.save
=> true
>> p.new_record?
=> false
You can also use built-in rails validations, something like
class Department < ActiveRecord::Base
validate :my_validation_method, :on => update
def my_validation_method
errors.add_to_base("You can't update existing objects")
end
end
You can find more information on rails validations in the tutorial I linked above.
I have a model named Tickets that being saved to the database even when
invalid. This is stopping me from using validations to help prevent
duplicate data being saved to the DB. In script/console
>> Ticket.last.valid?
=> False
>> Ticket.first.valid?
=> False
If I try to see what errors are associated with this invalid object
>> Ticket.last.errors.each{|attr,msg| puts "#{attr} - #{msg}\n" }
=> {}
So does anyone know how it's possible to save an invalid object to the
database, and how can I find what is making the object invalid?
Ticket.rb (model)
class Ticket < ActiveRecord::Base
belongs_to :whymail
belongs_to :forms
attr_accessible :to_email, :to_email, :from_email, :subject, :body
validates_uniqueness_of :to_email, :scope => [:body, :from_email]
validates_presence_of :to_email
validates_presence_of :from_email
validates_presence_of :subject
validates_presence_of :body
def after_create
if self.valid?
whymail = Whymail.find(:first, :include => :user, :conditions => ['(email = ?)', self.to_email.upcase ] )
if !whymail.nil?
self.whymail_id = whymail.id
self.save
MyMailer.deliver_forward(whymail.user.email, self.from_email, self.to_email, self.subject, self.body)
end
end
end
end
One part of this question was answered, second was not. Can anyone see problems with this model that may allow it to save even though it is invalid??
It is possible to skip validations. How are you saving it? Is it part of a nested form?
In any case, you should look at the errors like this:
>>t = Ticket.last
>>t.valid?
>>t.errors.each{|attr,msg| puts "#{attr} - #{msg}\n" }
The way you have it above, you are getting a new object with the second Ticket.last call and validation hasn't been run on that one, so you can't see what the errors are.
Try something like:
t = Ticket.last
t.save
puts t.errors.full_messages.inspect
The errors object won't be populated until you try to save the activerecord object.
I am running into an issue when trying to apply an :if condition to a validates_associated validation. The if condition works for validates_presence_of, but not for validates_associated, and the form (with two models in it) works correctly, except that the validation still returns an error, regardless of whether the if condition is true or false.
validates_associated :departures, :if => :cruise?
validates_presence_of :ship_name, :if => :cruise?
def cruise?
item_marker == 1
end
# I even tested it using this, and it still returned a validated_associated error
def cruise?
false
end
#form item
<%= departure_form.date_select :date, :index => (departure.new_record? ? '' :
departure.id), :start_year =>Time.now.year, :order => [:month, :day, :year ],
:prompt=>true %>
I am using a date select for the :departures field, with a prompt for the for default values (i.e. the first selected option for each field with have a value=""). I believe this is what is causing the problem. I could remove the prompt and just blank out the departure dates for non_cruises in the controller, but that seems sloppy. does anyone have a suggestions? Note, this code uses portions of Ryan Bates's "Handle Multiple Models in One Form" Recipe.
I figured out what was wrong. My new departures builder function was creating departure objects for all items, whether they were cruises or not. I added an if cruises? statement to the code below and now it works properly.
has_many :departures, :dependent => :destroy
def new_departure_attributes=(departure_attributes)
departure_attributes.each do |attributes|
if cruises? #new if statement
departures.build(attributes)
end
end
end
#the following two methods are only used for update actions
#(the error happens in new/create as well)
def existing_departure_attributes=(departure_attributes)
departures.reject(&:new_record?).each do |departure|
attributes = departure_attributes[departure.id.to_s]
if attributes
departure.attributes = attributes
else
departures.delete(departure)
end
end
end
def save_departures
departures.each do |departure|
departure.save(false)
end
end