has_one relationship validation in rails - ruby-on-rails

Since has_one doesn't provide a before_add callback to allow validation,
how do I prevent rails from destroying the old association even when the
new one does'nt pass validation?
susan :has_one :shirt
shirt :belongs_to :susan
susan.shirt = a_nice_shirt
this destroys whatever association was present beforehand,
even if the new shirt is never realy associated because it didn't pass
validation, leaving a shirtless susan behind (well, acutally leaving a shirt
behind that doesn't belong to anyone..).
susan.build_shirt
does the same thing
Is there a good reason for the missing before_add callback that I overlooked?

I'm not sure why that callback isn't there, but you can always just add an Observer to the model, and verify the new association in a before_save. I'll assume "susan" is a User Model instance, and the shirt has to be red to pass validation.
class UserObserver< ActiveRecord::Observer
def before_save(user)
return false if user.shirt.color != "red"
end
end
If you return false in the observer, the object won't save. Of course, your current instance of "susan" will still have the invalid association. I'm not positive, but if you change the before_save_ in the observer to something like this:
class UserObserver< ActiveRecord::Observer
def before_save(user)
if user.shirt.color != "red"
user.reload
false
end
end
Might refresh the instance of your User. I've never tried this though.

In Rails validation is usually done when you try to save the object to the database (using save or save!), not when it's modified. If for any reason you would like to restore the old value when the validation fails you can either reload the object or use the new dirty attributes feature.

Look at this ticket:
http://dev.rubyonrails.org/ticket/10518
It appears that the functionality described there is still present if you have :dependent => :destroy on your association.
Personally, I think this is a bug in rails.

Related

Updating association without saving it

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 alter an object attribute accurately based on an association count?

I got a sample class:
class Human < ActiveRecord::Base
has_and_belongs_to_many :diseases
before_save :check_health
def check_health
if(diseases.count == 0)
self.healthy = true
end
end
end
I want to change the fields of the object before saving it.
The problem:
self.diseases returns the old value of the diseases association.
The new value of :diseases can be accessed via association_cache[:diseases], but I don't know how to use association_cache. I don't understand which objects are marked for deletion, which objects are new, and which have been updated.
Why not reload the relation object to get an accurate count? Try this:
def check_health
healthy = true unless diseases.reload.any?
end
At first I think the logic in #check_health is totally correct. No need to change it.
The reason you met the problem may be you used it on non-persisted objects, both human and disease.
To solve it, I think the correct approach is to change the before_save callback to after_save callback. This will make more sense. Also it looks you only need this method for later use after saving, so why not do that?

How do I change an ActiveRecord from marked to be saved to make sure it does not get saved, from within the model itself?

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

How to intercept accepts_nested_attributes_for?

I have a Rails application, with two models: SalesTransactions and PurchaseOrders.
In the PurchaseOrders model, new entries are registered using 'purchase_order_number' as the key field. I use the create method of the model to search if that 'purchase_order_number' has been previously registered, and if so, reuse that record and use its id in the SalesTransaction record. If that name wasn't already registered, I go ahead and perform the create, and then use the new PurchaseOrder record id in the SalesTransaction (the foreign_id linking to the associated PO).
Note that I don't have the existing PurchaseOrder record id until I've done a look-up in the create method (so this is not a question of 'how do I update a record using 'accepts_nested_attributes_for'?', I can do that once I have the id).
In some situations, my application records a new SalesTransaction, and creates a new PurchaseOrder at the same time. It uses accepts_nested_attributes_for to create the PurchaseOrder record.
The problem appears to be that when using 'accepts_nested_attributes_for', create is not called and so my model does not have the opportunity to intercept the create, and look-up if the 'purchase_order_number' has already been registered and handle that case.
I'd appreciate suggestions as to how to intercept 'accepts_nested_attributes_for' creations to allow some pre-processing (i.e. look up if the PurchaseOrder record with that number already exists, and if so, use it).
Not all Sales have a PurchaseOrder, so the PurchaseOrder record is optional within a SalesTransaction.
(I've seen a kludge involving :reject_if, but that does not allow me to add the existing record id as the foreign_id within the parent record.)
Thanks.
You could use validate and save callbacks to do what you need.
Assuming the setup:
class SalesTransaction < ActiveRecord::Base
belongs_to :purchase_order, :foreign_key => "po_purchase_order_no",
:primary_key => "purchase_order_no"
accepts_nested_attributes_for :purchase_order
end
class PurchaseOrder < ActiveRecord::Base
has_many :sales_transactions, :foreign_key => "po_purchase_order_no",
:primary_key => "purchase_order_no"
before_validation :check_for_exisitng_po # maybe only on create?
accepts_nested_attributes_for :sales_transactions
private
def check_for_exisitng_po
existing_po = PurchaseOrder.find_by_purchase_order_no(self.purchase_order_no)
if existing_po
self.id = existing_po.id
self.reload # don't like this, also will overwrite incoming attrs
#new_record = false # tell AR this is not a new record
end
true
end
end
This should give back full use of accepts_nested_attributes_for again.
gist w/tests
Two ideas: Have you taken a look at association callbacks? Perhaps you can "intercept" accepts_nested_attributes_for at this level, using :before_add to check if it is already in the DB before creating a new record.
The other idea is to post-process instead. In an after_save/update you could look up all of the records with the name (that ought to be unique), and if there's more than one then merge them.
I was going to write a before_save function, but you say this:
It uses accepts_nested_attributes_for to create the PurchaseOrder record.
So in the SalesTransaction process flow, why look it up at all? You should just get the next one available... there shouldn't be a reason to search for something that didn't exist until NOW.
OK, I've left this question out there for a while, and offered a bounty, but I've not got the answer I'm looking for (though I certainly appreciate folk trying to help).
I'm concluding that I wasn't missing some trick and, at the time of writing, there isn't a neat solution, only work-arounds.
As such, I'm going to rewrite my App to avoid using accept_nested_attributes_for, and post the SalesTransaction and the PurchaseOrder records separately, so the create code can be applied in both cases.
A shame, as accept_nested... is pretty cool otherwise, but it's not complete enough in this case.
I still love Rails ;-)

Validating a Rails model post-save?

I have a model with a couple of accepts_nested_attributes_for. There is a requirement that I have at least one of each of the nested attributes when saving and moving on.
However, validation occurs pre-save, so when I'm removing an item and moving on, it let's it through.
How can I validate that when I've saved, I have at least one item of each nested type there?
There's a bug with accepts_nested_attributes_for. Meaning you have to be a little more devious when it comes to validations in the parent model.
You could use an :after_save callback in each of your nested models to check if it's the last one. But if there's many nested associations where you want to ensure at least one, this isn't very DRY.
This is however a valid workaround for the linked bug:
class Whatever < ActiveRecord::Base
:has_many => :association_a
:has_many => :association_b
def ensure_minimum_associations
bad_associations = [:association_a, :association_b].
select{|assoc| self.send(assoc).all?{|a| a.marked_for_destruction?}}
unless bad_associations.empty?
bad_associations.each do |association|
errors.add_to_base "Each #{self.class.name.downcase} must retain at least one #{association}"
end
return false
end
end
end
You can always call valid? on a model and it will run the validation.
I believe you're looking for validates_associated

Resources