In Rails, how can you update a child record with values from its parent before either the child or parent are saved?
I'm using Rails' nested attributes to create a parent record with many children. The parent record has a user_id attribute to track its owner, and for purposes outside of the scope of this question, the child record also needs to store this user_id attribute.
Currently, I'm using a before_save callback on the child model:
before_save :set_user_id
def set_user_id
if user = self.parent.try(:user)
self[:user_id] = user.id
end
end
But because I also need to make sure that the child is unique for a given parent/user combo, I have to do this:
validates :parent,
:uniqueness => {:scope => :user_id},
:unless => Proc.new{|child| child.user_id.blank?}
The problem is that unless parameter. The child's user_id attr is blank until after validation, so the validation effectively isn't run on child creation. This causes a problem if users double-submit the form; the result is that duplicate, invalid subscriptions end up in the database. I've tried to fix this by changing this:
before_save :set_user_id
to
before_validate :set_user_id
but it appears the parent's user_id is not accessible before validation.
Has anybody encountered such a problem?
If possible, can you just put a hidden field in the form with the user_id in it? You'll probably need to do some validation and potential rollback in an after_save, but seems like it might be easy.
Another option might be to just set the user_id for all the relevant objects in the params hash in the controller before calling the save/create on the parent object.
Related
In a nested form, I want the user to have the ability to create or modify all of a Parent's Childs at one time. So let's so the params that are passed are like this:
{"childs_attributes" => [{attribute:1}, {attribute:2}, {attribute:3}...]}
I would like a validation that says for any one Parent, the attributes of all of its Childs must be unique. In other words, in the above example, that's OK because you'd get:
Parent.childs.pluck(:attribute).uniq.length == Parent.childs.pluck(:attribute).length
However, if the params passed were like below, it'd be a violation of the validation rule:
{"childs_attributes" => [{attribute:1}, {attribute:2}, {attribute:3}...]}
So far the only solution I've come up with to do this validation is in the Controller... which I know is bad practice because we want to push this to the model.
The problem is that if in the model I have something like the below:
class Parent
validate :unique_attribute_on_child
def unique_attribute_on_child
attribute_list = self.childs.pluck(:attribute)
if attribute_list.uniq.length != attribute_list.length
errors[:base] << "Parent contains Child(s) with same Attribute"
end
end
end
That won't work because self.childs.pluck(:attribute) won't return the attribute passed in the current update, since the current update won't have saved yet.
I guess I could do something like an after_save but that feels really convoluted since it's going back and reversing db commits (not to mention, the code as written below [non tested, just an example] likely leads to a circular loop if I'm not careful, since Parent validate associated children)
after_save :unique_attribute_on_child
def unique_attribute_on_child
attribute_list = self.childs.pluck(:attribute)
if attribute_list.uniq.length != attribute_list.length
self.childs.each { |c| c.update_attributes(attribute:nil) }
errors[:base] << "Parent contains Child(s) with same Attribute"
end
end
end
Other ideas?
My first impulse is to suggest that Rails uses smart pluralization and to try using children instead of childs, but I think this was just the example.
I now recommend that you change your strategy. Call the validation on the children like so:
class Child < ActiveRecord::Base
belongs_to :parent
...
validates :child_attribute, uniqueness: { scope: :parent }
...
end
I have a situation (parsing) where I need to begin with child records, add their logical parent, and then save while validating both the child and the parent.
Is there a standard way to handle this with Rails?
Given the following relationships:
Parent has_many Child, validates :child, presence: true
Child belongs_to Parent
I'm seeing something like the following:
p = Parent.new # with a missing required attribute
c = Child.new
c.parent = p # or c.parents << p, this isn't important
p.valid? # false
c.valid? # true
c.save # true
So it's letting me save the child while the newly-associated parent object has errors.
Is there a conventional way to solve this? I believe this works properly when the objects are swapped, as expected given their relationship.
However, I can't begin with the parent in this case because it may exist already, and I do a separate find_or_create for this. It's more logical to begin with the child records in this case.
Google/SO searches didn't help.
Take a look at validates_associated here.
(Please note to not use it on both ends)
You can add a custom validator in your child model like the following:
validate parent_model_validate
And, then add the parent_model_validate method in the same (child) model:
def parent_model_validate
unless self.parent.valid?
errors.add(:child_model, 'Parent is not valid')
end
end
This validator will make sure that, if the parent model is not valid, then the child model will not be saved.
I have a model:
class A < ActiveRecord::Base
has_many :B
end
And I want to reset or update A's B association, but only save it later:
a = A.find(...)
# a.bs == [B<...>, B<...>]
a.bs = []
#or
a.bs = [B.new, B.new]
# do some validation stuff on `a` and `a.bs`
So there might be some case where I will call a.save later or maybe not. In the case I don't call a.save I would like that a.bs stay to its original value, but as soon as I call a.bs = [], the old associations is destroyed and now A.find(...).bs == []. Is there any simple way to set a record association without persisting it in the database right away? I looked at Rails source and didn't find anything that could help me there.
Thanks!
Edit:
I should add that this is for an existing application and there are some architecture constraint that doesn't allow us to use the the regular ActiveRecord updating and validation tools. The way it works we have a set of Updater class that take params and assign the checkout object the value from params. There are then a set of Validater class that validate the checkout object for each given params. Fianlly, if everything is good, we save the model.
In this case, I'm looking to update the association in an Updater, validate them in the Validator and finally, persist it if everything check out.
In summary, this would look like:
def update
apply_updaters(object, params)
# do some stuff with the updated object
if(validate(object))
object.save(validate: false)
end
Since there are a lot of stuff going on between appy_updaters and object.save, Transaction are not really an option. This is why I'm really looking to update the association without persisting right away, just like we would do with any other attribute.
So far, the closest solution I've got to is rewriting the association cache (target). This look something like:
# In the updater
A.bs.target.clear
params[:bs].each{|b| A.bs.build(b)}
# A.bs now contains the parameters object without doing any update in the database
When come the time to save, we need to persist cache:
new_object = A.bs.target
A.bs(true).replace(new_object)
This work, but this feel kind of hack-ish and can easily break or have some undesired side-effect. An alternative I'm thinking about is to add a method A#new_bs= that cache the assigned object and A#bs that return the cached object if available.
Good question.
I can advice to use attributes assignment instead of collection manipulation. All validations will be performed as regular - after save or another 'persistent' method. You can write your own method (in model or in separated validator) which will validate collection.
You can delete and add elements to collection through attributes - deletion is performed by additional attribute _destroy which may be 'true' or 'false' (http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html), addition - through setting up parent model to accept attributes.
As example set up model A:
class A < ActiveRecord::Base
has_many :b
accepts_nested_attributes_for :b, :allow_destroy => true
validates_associated :b # to validate each element
validate :b_is_correct # to validate whole collection
def b_is_correct
self.bs.each { |b| ... } # validate collection
end
end
In controller use plain attributes for model updating (e.g update!(a_aparams)). These methods will behave like flat attribute updating. And don't forget to permit attributes for nested collection.
class AController < ApplicationController
def update
#a = A.find(...)
#a.update(a_attributes) # triggers validation, if error occurs - no changes will be persisted and a.errors will be populated
end
def a_attributes
params.require(:a).permit([:attr_of_a, :b_attributes => [:attr_of_b, :_destroy]])
end
end
On form we used gem nested_form (https://github.com/ryanb/nested_form), I recommend it. But on server side this approach uses attribute _destroy as mentioned before.
I finally found out about the mark_for_destruction method. My final solution therefor look like:
a.bs.each(&:mark_for_destruction)
params[:bs].each{|b| a.bs.build(b)}
And then I can filter out the marked_for_destruction? entry in the following processing and validation.
Thanks #AlkH that made me look into how accepts_nested_attributes_for was working and handling delayed destruction of association.
How do I change an AcriveRecord from marked to be saved to make sure it does not get saved, from within the model itself?
Considering I can have a method run by a hook in activerecord, such as: before_save
for (hypothetical) example:
before_save :ignore_new_delete_exisiting_if_blank(self.attribute)
def ignore_new_delete_exisiting_if_blank(attribute)
self.do_not_save_me! if attribute.blank?
#what is that magic "do_not_save_me" method?
#Is there such thing, or something to achieve the same thing?
end
Update
My particular use case requires that no errors be thrown and other models to continue to be saved, even if this one will not. I should explain:
I am using model inheritance, and I am having an issue with figuring out how to let save the parent model, but if the child model instances are blank, (no values exist in certain attributes) they should not be persisted; however, the parent should still be persisted. This scenario does not let me make use of validations on the child model as that would block the parent from being persisted as well...
Your method should just return false to make it does not save.
Or you set the errors, which will allow to be more descriptive.
For example:
def ignore_new_delete_exisiting_if_blank_attribute
if attribute.blank?
errors.add(:base, "Not allowed to save if attribute is blank.")
end
end
Note that you cannot send parameters to a before_save. If you just want to make sure a record is not saved when an attribute is not present, you should use
validates_presence_of :attribute
[UPDATE]
When saving a parent model with children, you have to do something like accepts_nested_attributes_for, and in that call, you can specify which attributes must be given or when a child-record is ignored.
For example
accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? }
will not save a post if the title is blank.
Hope this helps.
The "magic" is that when you return false from the method, the record won't be saved.
In your case:
def ignore_new_delete_exisiting_if_blank(attribute)
attribute.present?
end
I am currently using nested model mass assignment on one of my models. It works a treat, however, I'd like to be able to ensure that any nested models that are created "belong_to" the same user.
I've managed to implement this by using alias method chaining with:
def contact_attributes_with_user_id=(attributes)
self.contact_attributes_without_user_id = attributes.merge( "user_id" => user_id )
end
alias_method_chain :contact_attributes=, :user_id
Now this works fine, but it means I can no longer have attribute protection on user_id for the contact - which could easily catch someone out in the future.
Can anyone come up with a better way?
What if you add a before_save hook to your Contact model, like this:
belongs_to :parent
validates_presence_of :parent_id
before_save :assign_user_id
private
def assign_user_id
self.user_id = parent.user_id
end
This way your Contacts' user_ids will follow the parent model's and you don't have to worry about assigning at all (you can get rid of the alias_method_chain).