I have two associated classes like this:
class Purchase < ActiveRecord::Base
has_many :actions
before_create do |p|
self.actions.build
end
end
class Action < ActiveRecord::Base
belongs_to :purchase
before_save do |a|
false
end
end
The block in the Action class prevents it from saving. I was thinking doing Purchase.create will fail because it cannot save the child object. But while it does not save the Action, it commits the Purchase. How can i prevent the parent object to be saved when there is an error in the child object?
It turns out you have to rollback the transaction explicitly, errors from the child objects does not propagate. So i ended up with:
class Purchase < ActiveRecord::Base
has_many :actions
after_create do |p|
a = Action.new(purchase: p)
if !a.save
raise ActiveRecord::Rollback
end
end
end
class Action < ActiveRecord::Base
belongs_to :purchase
before_save do |a|
false
end
end
Take note that i also changed the before_create callback to after_create. Otherwise, since belongs_to also causes the parent to be saved, you will get a SystemStackError: stack level too deep.
I ran into this problem when dealing with race conditions where the child objects would pass a uniqueness validation, but then fail the database constraint (when trying to save the parent object), leading to childless (invalid) parent objects in the database.
A slightly more general solution to the one suggested by #lunr:
class Purchase < ActiveRecord::Base
has_many :actions
after_save do
actions.each do |action|
raise ActiveRecord::Rollback unless action.save
end
end
end
class Action < ActiveRecord::Base
belongs_to :purchase
before_save do |a|
false
end
end
Try to use this code in Purchase class:
validate :all_children_are_valid
def all_children_are_valid
self.actions.each do |action|
unless action.valid?
self.errors.add(:actions, "aren't valid")
break
end
end
end
Or use validates_associated in Purchase class:
validates_associated :actions
If in your business logic you can't save purchase without any action, then add a presence validator on actions inside purchases
validates :actions, length: {minimum: 1}, presence: true
Related
class MyTask < ApplicationRecord
has_many :jobs, as: :ownerable, dependent: :destroy
accepts_nested_attributes_for :jobs, allow_destroy: true
before_save :set_some_data
end
class Job < ApplicationRecord
belongs_to :ownerable, polymorphic: true, optional: true
end
In the :set_some_data method, we actually take the values from all the jobs belonging to the MyTask object and perform some calculations and save the result in a column (actually, just a self.column_name = calculated_value, not actually calling save).
The problem is that the UPDATE on the column happens before any jobs marked for destruction ie with "_destroy" => 1 in the params. And so obviously, it includes data from the deleted jobs, which is incorrect.
I am currently doing the following - change callback to:
after_save :set_some_data
def set_some_data
#Do stuff
# WARNING: Don't use any method that will trigger an after_save callback. Infinite loop otherwise.
self.update_columns(column_name: calculated_value)
end
It does what I want. But is this a good solution? Can you suggest some better alternatives?
you can do this with with after_destroy and put the method in job.rb this will make sure when child deleted(job) it will call the parent to update the value
class Job < ApplicationRecord
belongs_to :ownerable, polymorphic: true, optional: true
after_destroy :update_parent
def update_parent
# check your parent model
self.ownerable.update_columns(column_name: calculated_value)
end
end
for more detail callback you can check this and
There are models with has has_many through association:
class Event < ActiveRecord::Base
has_many :event_categories
has_many :categories, through: :event_categories
validates :categories, presence: true
end
class EventCategory < ActiveRecord::Base
belongs_to :event
belongs_to :category
validates_presence_of :event, :category
end
class Category < ActiveRecord::Base
has_many :event_categories
has_many :events, through: :event_categories
end
The issue is with assigning event.categories = [] - it immediately deletes rows from event_categories. Thus, previous associations are irreversibly destroyed and an event becomes invalid.
How to validate a presence of records in case of has_many, through:?
UPD: please carefully read sentence marked in bold before answering.
Rails 4.2.1
You have to create a custom validation, like so:
validate :has_categories
def has_categories
unless categories.size > 0
errors.add(:base, "There are no categories")
end
end
This shows you the general idea, you can adapt this to your needs.
UPDATE
This post has come up once more, and I found a way to fill in the blanks.
The validations can remain as above. All I have to add to that, is the case of direct assignment of an empty set of categories. So, how do I do that?
The idea is simple: override the setter method to not accept the empty array:
def categories=(value)
if value.empty?
puts "Categories cannot be blank"
else
super(value)
end
end
This will work for every assignment, except when assigning an empty set. Then, simply nothing will happen. No error will be recorded and no action will be performed.
If you want to also add an error message, you will have to improvise. Add an attribute to the class which will be populated when the bell rings.
So, to cut a long story short, this model worked for me:
class Event < ActiveRecord::Base
has_many :event_categories
has_many :categories, through: :event_categories
attr_accessor :categories_validator # The bell
validates :categories, presence: true
validate :check_for_categories_validator # Has the bell rung?
def categories=(value)
if value.empty?
self.categories_validator = true # Ring that bell!!!
else
super(value) # No bell, just do what you have to do
end
end
private
def check_for_categories_validator
self.errors.add(:categories, "can't be blank") if self.categories_validator == true
end
end
Having added this last validation, the instance will be invalid if you do:
event.categories = []
Although, no action will have been fulfilled (the update is skipped).
use validates_associated, official documentaion is Here
If you are using RSpec as your testing framework, take a look at Shoulda Matcher. Here is an example:
describe Event do
it { should have_many(:categories).through(:event_categories) }
end
Rails 4.2
I have a parent class that accepts nested attributes for its children.
class Parent < ActiveRecord::Base
has_many :kids
accepts_nested_attributes_for :kids, allow_destroy: true
end
class Kid < ActiveRecord::Base
belongs_to :parent
def destroy
if some_count > 0
self.hidden = true
else
self.destroy
end
end
end
I sometimes want to set the hidden flag on the child instead of deleting it. I am doing this via accepts_nested_attributes_for. I need this decision to be set on the server side, I can't have users deciding whether to destroy or hide.
But not destroying raises ActiveRecord::RecordNotDestroyed - Failed to destroy the record:
What's the right way to do this?
The error is thrown because you got into infinite loop (you are calling destroy method within destroy method). Use super instead. Also you need to save hidden column change to the database. In this case it should be safe to use update_column (no validation and no callbacks are triggered, no other columns are saved to the database)
class Kid < ActiveRecord::Base
belongs_to :parent
def destroy
if some_count > 0
update_column(:hidden, true)
else
super
end
end
end
To answer other question you need to explain what some_count is. :)
I am using rails 2.3.17 and have this relationship setup
class Item < ActiveRecord::Base
belongs_to :order
end
class Order < ActiveRecord::Base
has_many :items, :dependent => :delete_all
end
Now i need to do a validation on an item, by accessing order object attributes, how can I do this?
When I write
validate :checkXYZ
def checkXYZ
Rails.logger.debug self.order // I AM GETTING NIL
end
but when I write
before_save :checkXYZ
def checkXYZ
Rails.logger.debug self.order // I AM ORDER OBJECT
end
This is my controller logic
#order = #otherObj.orders.create(params[:order])
item = #order.items.create(params[:item])
I need to get the order object in validate of item class, how can I do that?
In before_validate section, the parent(order) is not yet connected to the item object. Hence it'll definitely show nil.
But after the validation is passed & in before_save stage, the order & item are connected hence you are able to access the parent order of the selected item.
You can have below approach to validate your object.
class Order < ActiveRecord::Base
has_many :items, dependent: :delete_all
end
class Item < ActiveRecord::Base
before_save :something_missing?
belongs_to :order
private
def something_missing?
your_order = self.order
if (add_your_condition_which_is_violated)
errors[:base] << "Your error message"
return false
end
# When you are returning false here, the record won't be saved.
# And the respective error message you can use to show in the view.
end
end
To do this reliably when creating or updating an order you should call the validation on the parent object's (order's) model like this:
class Order < ActiveRecord::Base
has_many :items, :dependent => :delete_all
validate :checkXYZ
private
def checkXYZ
Rails.logger.debug self // Here you will have the Order object
for i in items do
if (vehicle == 7 and i.distance <= 500) then // vehicle is an attribute of order
errors.add(:error, "You're driving by car, distance must be larger than 500")
end
end
end
end
I'm having trouble with Active Record callbacks in a model which contains accepts_nested_attributes
I call build_associated_partiesusing an after_create callback, but these values are not being saved and I get <nil> errors. I've also tried using before_create & after_initialize callbacks without success.
What is causing the callbacks to fail?
connection.rb
class Connection < ActiveRecord::Base
attr_accessible :reason, :established, :connector, :connectee1,
:connectee2, :connectee1_attributes,
:connectee2_attributes, :connector_attributes
belongs_to :connector, class_name: "User"
belongs_to :connectee1, class_name: "User"
belongs_to :connectee2, class_name: "User"
accepts_nested_attributes_for :connector, :connectee1, :connectee2
belongs_to :permission
after_create :build_associated_parties
# builds connectee's, connector, permission objects
def build_associated_parties
build_connector
build_connectee1
build_connectee2
build_permission
end
connection_controller.rb
class ConnectionsController < ApplicationController
def new
#connection = Connection.new
end
def create
#connection = Connection.new params[:connection]
if #connection.save
flash[:notice] = "Connection created successfully!"
redirect_to #connection
else
render :new
end
end
end
However, if I instead build these attributes inside the controller as shown below, I don't get the error. This is nice, but it seems to go against keeping business logic code out of the controller.
class ConnectionsController < ApplicationController
def new
#connection = Connection.new
#connection.build_connectee1
#connection.build_connectee2
#connection.build_connector
end
end
How can I accomplish the same functionality with code in the model? Are there advantages to keeping it in the model?
You called your method build_associated_parties after connection is created, so how these methods:
build_connector
build_connectee1
build_connectee2
build_permission
know what params it will use? So they don't know what values are passed into method then they will get error. In controller, they didn't have error because they used values of params[:connection].
On your form, if you already have fields for connector, connectee1, connectee2, you should put code which initialize object in your new controller. When you save #connection, it's saved those object too. I think these codes aren't need to put into model. Your model only should put other logic code, like search or calculation...
after_create is a big no. Use after_initialize in your model and use self inside your build_associated_parties method. See if that works.
Moved logic out of controller and back into model. However, the build_* code was overwriting the values I was passing into the nested attributes.
By adding unless {attribute} to these build_ methods, I was able to properly pass in the values.
class Connection < ActiveRecord::Base
attr_accessible :reason, :established, :connector, :connectee1, :connectee2,
:connectee1_attributes, :connectee2_attributes, :connector_attributes
belongs_to :connector, class_name: "User"
belongs_to :connectee1, class_name: "User"
belongs_to :connectee2, class_name: "User"
accepts_nested_attributes_for :connector, :connectee1, :connectee2
belongs_to :permission
after_initialize :build_associated_parties
validates :reason, :presence => true
validates_length_of :reason, :maximum => 160
#builds connectee's, connector, permission objects
def build_associated_parties
build_connector unless connector
build_connectee1 unless connectee1
build_connectee2 unless connectee2
end