Rails soft delete child record - ruby-on-rails

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. :)

Related

before_destroy for model that destroys dependent records

I have a situation like this:
class Shop < ActiveRecord::Base
has_many :services, dependent: :destroy
end
class Service < ActiveRecord::Base
has_many :model_weeks, dependent: :destroy
end
class ModelWeek < ActiveRecord::Base
before_destroy :prevent_destroy, if: :default?
private
def prevent_destroy
false
end
end
When I try to destroy a shop, I get ActiveRecord::RecordNotDestroyed: Failed to destroy the record because it starts destroying the associated records first, and it gets prevented by the callback in the ModelWeek.
I could easily unset default for ModelWeek when destroying a shop, only if I could catch it. before_destroy in the Shop model does not get triggered prior to when the above mentioned exception is raised.
So, is there a way to catch this in Shop model, or if not, is it possible to "know" in the ModelWeek that destruction was triggered by a parent? I investigated parsing the caller, but it offers nothing useful, and it would be messy anyway...
i solved this problem by using a funny hack,
you just need to put the before_destroy line
before the association line
and it will run before_destroy before deleting the associations
Since rails has destruction chain from related children to parent, it really makes sense, right? To make it easy, we can overwrite the destroy method in ModelWeek like this:
class ModelWeek < ActiveRecord::Base
# before_destroy :prevent_destroy, if: :default?
def destroy
unless default?
super
end
end
end
After some research and testing, this is what I came up with:
This is the order in which the methods are being called (do_before_destroy are any methods specified in the before_destroy callback):
Shop.destroy
Service.destroy
ModelWeek.destroy
ModelWeek.do_before_destroy
Service.do_before_destroy
Shop.do_before_destroy
So, I can deal with anything preventing the destruction of a child (ModelWeek) in the destroy method of a parent (Shop):
# Shop
def destroy
# default is a scope
ModelWeek.default.where(service_id: self.services.pluck(:id)).each do |m|
m.unset_default
end
super
end
After that nothing prevents destruction of the child and the chain continues unprevented.
UPDATE
There is even better, cleaner solution, without a need for overriding the destroy of the parent and doing any queries:
class Shop < ActiveRecord::Base
has_many :services, dependent: :destroy, before_remove: :unset_default_week
private
def unset_default_week(service)
service.model_weeks.default.unset_default
end
end
The modern solution is to use a before_destroy callback in the parent model with prepend: true option, making the callback trigger before the destruction callback that's defined under the hood by dependent: :destroy on association.
class Service < ActiveRecord::Base
has_many :model_weeks, dependent: :destroy
before_destroy :handle_model_weeks, prepend: true
private
def handle_model_weeks
# special handling here
end
end
See also a Rails thread on this.

Prevent parent object to be saved when saving child object fails

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

validate the presence of at least one association object in rails 3.2

I've one small problem, I can't get solved. I want to validate that there is at least one associated model. Like in the following
class User < ActiveRecord::Base
has_many :things
validates_presence_of :things
end
class Thing < ActiveRecord::Base
belongs_to :user
end
This works fine when I update my model via #update_attributes, but when I simply set #user.things = [], I am able to get invalid data in the database. My workaroud to solve this is to overwrite the setter method
def things=(val)
begin
if val.blank?
errors.add(:things, "not valid")
raise SomeError
end
super
rescue SomeError
false
end
end
But somehow this doesn't feel right. Isn't there a way to archive the same result via validations and/or callbacks, preferably so that #things= return false (and not val) and so that #user.things is not changed (I mean the cached #user.things, #user.things(true) should work fine anyway).
You can create a custom validator that will check the presence of things.
Instead of
validates_presence_of :things
You could do
validate :user_has_things
def user_has_things
if self.things.size == 0
errors.add("user has no thingies")
end
end

How can I invoke the after_save callback when using 'counter_cache'?

I have a model that has counter_cache enabled for an association:
class Post
belongs_to :author, :counter_cache => true
end
class Author
has_many :posts
end
I am also using a cache fragment for each 'author' and I want to expire that cache whenever #author.posts_count is updated since that value is showing in the UI. The problem is that the internals of counter_cache (increment_counter and decrement_counter) don't appear to invoke the callbacks on Author, so there's no way for me to know when it happens except to expire the cache from within a Post observer (or cache sweeper) which just doesn't seem as clean.
Any ideas?
I had a similar requirement to do something on a counter update, in my case I needed to do something if the counter_cache count exceeded a certain value, my solution was to override the update_counters method like so:
class Post < ApplicationRecord
belongs_to :author, :counter_cache => true
end
class Author < ApplicationRecord
has_many :posts
def self.update_counters(id, counters)
author = Author.find(id)
author.do_something! if author.posts_count + counters['posts_count'] >= some_value
super(id, counters) # continue on with the normal update_counters flow.
end
end
See update_counters documentation for more info.
I couldn't get it to work either. In the end, I gave up and wrote my own cache_counter-like method and call it from the after_save callback.
I ended up keeping the cache_counter as it was, but then forcing the cache expiry through the Post's after_create callback, like this:
class Post
belongs_to :author, :counter_cache => true
after_create :force_author_cache_expiry
def force_author_cache_expiry
author.force_cache_expiry!
end
end
class Author
has_many :posts
def force_cache_expiry!
notify :force_expire_cache
end
end
then force_expire_cache(author) is a method in my AuthorSweeper class that expires the cache fragment.
Well, I was having the same problem and ended up in your post, but I discovered that, since the "after_" and "before_" callbacks are public methods, you can do the following:
class Author < ActiveRecord::Base
has_many :posts
Post.after_create do
# Do whatever you want, but...
self.class == Post # Beware of this
end
end
I don't know how much standard is to do this, but the methods are public, so I guess is ok.
If you want to keep cache and models separated you can use Sweepers.
I also have requirement to watch counter's change. after digging rails source code, counter_column is changed via direct SQL update. In other words, it will not trigger any callback(in your case, it will not trigger any callback in Author model when Post update).
from rails source code, counter_column was also changed by after_update callback.
My approach is give rails's way up, update counter_column by myself:
class Post
belongs_to :author
after_update :update_author_posts_counter
def update_author_posts_counter
# need to update for both previous author and new author
# find_by will not raise exception if there isn't any record
author_was = Author.find_by(id: author_id_was)
if author_was
author_was.update_posts_count!
end
if author
author.update_posts_count!
end
end
end
class Author
has_many :posts
after_update :expires_cache, if: :posts_count_changed?
def expires_cache
# do whatever you want
end
def update_posts_count!
update(posts_count: posts.count)
end
end

How do I 'validate' on destroy in rails

On destruction of a restful resource, I want to guarantee a few things before I allow a destroy operation to continue? Basically, I want the ability to stop the destroy operation if I note that doing so would place the database in a invalid state? There are no validation callbacks on a destroy operation, so how does one "validate" whether a destroy operation should be accepted?
You can raise an exception which you then catch. Rails wraps deletes in a transaction, which helps matters.
For example:
class Booking < ActiveRecord::Base
has_many :booking_payments
....
def destroy
raise "Cannot delete booking with payments" unless booking_payments.count == 0
# ... ok, go ahead and destroy
super
end
end
Alternatively you can use the before_destroy callback. This callback is normally used to destroy dependent records, but you can throw an exception or add an error instead.
def before_destroy
return true if booking_payments.count == 0
errors.add :base, "Cannot delete booking with payments"
# or errors.add_to_base in Rails 2
false
# Rails 5
throw(:abort)
end
myBooking.destroy will now return false, and myBooking.errors will be populated on return.
just a note:
For rails 3
class Booking < ActiveRecord::Base
before_destroy :booking_with_payments?
private
def booking_with_payments?
errors.add(:base, "Cannot delete booking with payments") unless booking_payments.count == 0
errors.blank? #return false, to not destroy the element, otherwise, it will delete.
end
It is what I did with Rails 5:
before_destroy do
cannot_delete_with_qrcodes
throw(:abort) if errors.present?
end
def cannot_delete_with_qrcodes
errors.add(:base, 'Cannot delete shop with qrcodes') if qrcodes.any?
end
State of affairs as of Rails 6:
This works:
before_destroy :ensure_something, prepend: true do
throw(:abort) if errors.present?
end
private
def ensure_something
errors.add(:field, "This isn't a good idea..") if something_bad
end
validate :validate_test, on: :destroy doesn't work: https://github.com/rails/rails/issues/32376
Since Rails 5 throw(:abort) is required to cancel execution: https://makandracards.com/makandra/20301-cancelling-the-activerecord-callback-chain
prepend: true is required so that dependent: :destroy doesn't run before the validations are executed: https://github.com/rails/rails/issues/3458
You can fish this together from other answers and comments, but I found none of them to be complete.
As a sidenote, many used a has_many relation as an example where they want to make sure not to delete any records if it would create orphaned records. This can be solved much more easily:
has_many :entities, dependent: :restrict_with_error
The ActiveRecord associations has_many and has_one allows for a dependent option that will make sure related table rows are deleted on delete, but this is usually to keep your database clean rather than preventing it from being invalid.
You can wrap the destroy action in an "if" statement in the controller:
def destroy # in controller context
if (model.valid_destroy?)
model.destroy # if in model context, use `super`
end
end
Where valid_destroy? is a method on your model class that returns true if the conditions for destroying a record are met.
Having a method like this will also let you prevent the display of the delete option to the user - which will improve the user experience as the user won't be able to perform an illegal operation.
I ended up using code from here to create a can_destroy override on activerecord:
https://gist.github.com/andhapp/1761098
class ActiveRecord::Base
def can_destroy?
self.class.reflect_on_all_associations.all? do |assoc|
assoc.options[:dependent] != :restrict || (assoc.macro == :has_one && self.send(assoc.name).nil?) || (assoc.macro == :has_many && self.send(assoc.name).empty?)
end
end
end
This has the added benefit of making it trivial to hide/show a delete button on the ui
You can also use the before_destroy callback to raise an exception.
I have these classes or models
class Enterprise < AR::Base
has_many :products
before_destroy :enterprise_with_products?
private
def empresas_with_portafolios?
self.portafolios.empty?
end
end
class Product < AR::Base
belongs_to :enterprises
end
Now when you delete an enterprise this process validates if there are products associated with enterprises
Note: You have to write this in the top of the class in order to validate it first.
Use ActiveRecord context validation in Rails 5.
class ApplicationRecord < ActiveRecord::Base
before_destroy do
throw :abort if invalid?(:destroy)
end
end
class Ticket < ApplicationRecord
validate :validate_expires_on, on: :destroy
def validate_expires_on
errors.add :expires_on if expires_on > Time.now
end
end
I was hoping this would be supported so I opened a rails issue to get it added:
https://github.com/rails/rails/issues/32376

Resources