How to simplify the soft delete process with Ruby on Rails? - ruby-on-rails
I want to have a model where I need to soft delete a record and not show them in the find or any other conditions while searching.
I want to retain the model without deleting the record. How to go about this?
Just use a concern in rails 4
Example here
module SoftDeletable
extend ActiveSupport::Concern
included do
default_scope { where(is_deleted: false) }
scope :only_deleted, -> { unscope(where: :is_deleted).where(is_deleted: true) }
end
def delete
update_column :is_deleted, true if has_attribute? :is_deleted
end
def destroy;
callbacks_result = transaction do
run_callbacks(:destroy) do
delete
end
end
callbacks_result ? self : false
end
def self.included(klazz)
klazz.extend Callbacks
end
module Callbacks
def self.extended(klazz)
klazz.define_callbacks :restore
klazz.define_singleton_method("before_restore") do |*args, &block|
set_callback(:restore, :before, *args, &block)
end
klazz.define_singleton_method("around_restore") do |*args, &block|
set_callback(:restore, :around, *args, &block)
end
klazz.define_singleton_method("after_restore") do |*args, &block|
set_callback(:restore, :after, *args, &block)
end
end
end
def restore!(opts = {})
self.class.transaction do
run_callbacks(:restore) do
update_column :is_deleted, false
restore_associated_records if opts[:recursive]
end
end
self
end
alias :restore :restore!
def restore_associated_records
destroyed_associations = self.class.reflect_on_all_associations.select do |association|
association.options[:dependent] == :destroy
end
destroyed_associations.each do |association|
association_data = send(association.name)
unless association_data.nil?
if association_data.is_deleted?
if association.collection?
association_data.only_deleted.each { |record| record.restore(recursive: true) }
else
association_data.restore(recursive: true)
end
end
end
if association_data.nil? && association.macro.to_s == 'has_one'
association_class_name = association.options[:class_name].present? ? association.options[:class_name] : association.name.to_s.camelize
association_foreign_key = association.options[:foreign_key].present? ? association.options[:foreign_key] : "#{self.class.name.to_s.underscore}_id"
Object.const_get(association_class_name).only_deleted.where(association_foreign_key, self.id).first.try(:restore, recursive: true)
end
end
clear_association_cache if destroyed_associations.present?
end
end
Deletable
A rails concern to add soft deletes.
Very simple and flexible way to customise/ change
(You can change the delete column to be a timestamp and change the methods to call ActiveRecord touch ).
Best where you want to control code not have gems for simple tasks.
Usage
In your Tables add a boolean column is_deletable
class AddDeletedAtToUsers < ActiveRecord::Migration
def change
add_column :users, :is_deleted, :boolean
end
end
In your models
class User < ActiveRecord::Base
has_many :user_details, dependent: :destroy
include SoftDeletable
end
Methods and callbacks available:
User.only_deleted
User.first.destroy
User.first.restore
User.first.restore(recursive: true)
Note:
Focus Using update_column or touch if you decide to use a timestamp column.
Edited
If you are using rails <= 3.x (this example also use a DateTime field instead boolean), there are some differences:
module SoftDeletable
extend ActiveSupport::Concern
included do
default_scope { where(deleted_at: nil }
# In Rails <= 3.x to use only_deleted, do something like 'data = Model.unscoped.only_deleted'
scope :only_deleted, -> { unscoped.where(table_name+'.deleted_at IS NOT NULL') }
end
def delete
update_column :deleted_at, DateTime.now if has_attribute? :deleted_at
end
# ... ... ...
# ... OTHERS IMPLEMENTATIONS ...
# ... ... ...
def restore!(opts = {})
self.class.transaction do
run_callbacks(:restore) do
# Remove default_scope. "UPDATE ... WHERE (deleted_at IS NULL)"
self.class.send(:unscoped) do
update_column :deleted_at, nil
restore_associated_records if opts[:recursive]
end
end
end
self
end
alias :restore :restore!
def restore_associated_records
destroyed_associations = self.class.reflect_on_all_associations.select do |association|
association.options[:dependent] == :destroy
end
destroyed_associations.each do |association|
association_data = send(association.name)
unless association_data.nil?
if association_data.deleted_at?
if association.collection?
association_data.only_deleted.each { |record| record.restore(recursive: true) }
else
association_data.restore(recursive: true)
end
end
end
if association_data.nil? && association.macro.to_s == 'has_one'
association_class_name = association.options[:class_name].present? ? association.options[:class_name] : association.name.to_s.camelize
association_foreign_key = association.options[:foreign_key].present? ? association.options[:foreign_key] : "#{self.class.name.to_s.underscore}_id"
Object.const_get(association_class_name).only_deleted.where(association_foreign_key, self.id).first.try(:restore, recursive: true)
end
end
clear_association_cache if destroyed_associations.present?
end
end
Usage
In your Tables add a DateTime column deleted_at
class AddDeletedAtToUsers < ActiveRecord::Migration
def change
add_column :users, :deleted_at, :datetime
end
end
Try this gem : https://github.com/technoweenie/acts_as_paranoid - ActiveRecord plugin allowing you to hide and restore records without actually deleting them
Just add a boolean field called deleted or something to that effect. When you soft delete the record just set that field to true.
When doing a find just add that as a condition (or make a scope for it).
The default_scope functionality in ActiveRecord 3 makes this easy, but personally, I favor the wide variety of standard solutions that can be dropped into the project. acts_as_archive in particular is the best fit for most of my projects, since it moves infrequently-accessed deleted records to a separate table, allowing the base table to stay small and in the database server's RAM.
Depending on your needs, you may also want to consider versioning instead of soft deletion.
Add a date field to your model - deleted_at.
Override the delete (or destroy) method on your model to set the deleted_at value. You can also create it as a new method. Something like soft_delete.
Add a restore/undelete method to your model to set the deleted_at value back to null.
Optional: create an alias method for the original delete (or destroy) method. Name it something like hard_delete.
You can define a module like this
module ActiveRecordScope
def self.included(base)
base.scope :not_deleted, -> { base.where(deleted: false) }
base.send(:default_scope) { base.not_deleted }
base.scope :only_deleted, -> { base.unscope(where: :deleted).where(deleted: true) }
def delete
update deleted: true
end
def recover
update deleted: false
end
end
end
Then in your class, you can write something like:
class User < ActiveRecord::Base
include ActiveRecordScope
end
So you have both soft delete and recover.
You call user.delete to soft delete an user. Then you can call user.recover to set the deleted back to false again, and recover it.
Have a look at rails3_acts_as_paranoid.
A simple plugin which hides records instead of deleting them, being
able to recover them.
...
This plugin was inspired by acts_as_paranoid and acts_as_active.
Usage:
class Paranoiac < ActiveRecord::Base
acts_as_paranoid
scope :pretty, where(:pretty => true)
end
Paranoiac.create(:pretty => true)
Paranoiac.pretty.count #=> 1
Paranoiac.only_deleted.count #=> 0
Paranoiac.pretty.only_deleted.count #=> 0
Paranoiac.first.destroy
Paranoiac.pretty.count #=> 0
Paranoiac.only_deleted.count #=> 1
Paranoiac.pretty.only_deleted.count #=> 1
If you use Rails4, Try this gem : https://github.com/alfa-jpn/kakurenbo
An association function of Kakurenbo is better than other gems.
i wont use a default scope cause if i want to get all the records, i need to ignore it using "with_exclusive_scope" which in turn is messy. the would go by adding a 'deleted' boolean field which is set when the record is deleted. Also, would have added scopes to get the data as per the condition.
checkout Overriding a Rails default_scope and Rails: Why is with_exclusive_scope protected? Any good practice on how to use it?
Add one column say status in your table and on deletion of records update the value of column status to inactive.
and while fetching the records, add condition status != "inactive" in the query.
For Rails 4 don't use acts_as_paranoid (buggy for Rails 4), use paranoia. All you have to do is add a deleted_at timestamp column and include acts_as_paranoia in the model.
From there, just call destroy on the object and all other ActiveRecord relations and most other methods(like :count) automatically exclude the soft_deleted records.
Related
Audit functionality for rails models
I'm trying to implement an audit functionality for some of my rails models and store it on an external "event" database (we chose BigQuery). Each event should be very basic: before_json, after_json, diff, action, object_id So, I started building this concern that I am planning on adding to my models: module Auditable extend ActiveSupport::Concern included do before_destroy {audit(:destroy)} after_validation on: :update do audit(:update) end after_validation on: :create do audit(:create) end def audit(action) EventSender.send(before_json, self.to_json, diff, action, self.id) end end end The only thing I dont know how to implement is getting the before state of the object so I can populate the relevant fields and the diff between the two states. Any ideas on how I can do it?
I solved it the following way: module Auditable require 'active_record/diff' extend ActiveSupport::Concern included do include ActiveRecord::Diff before_destroy {audit(:destroy, before: before_state)} after_validation on: :update do audit(:update, before: before_state, after: self) end after_create do audit(:create, after: self) end def audit(action, before: {}, after: {}) diff = case action when :update before.diff(after) when :create after when :destroy before end BigQueryClient.new.insert( self.class.to_s.downcase, { before: before.to_json, after: after.to_json, diff: diff.to_json, action: action, "#{self.class.to_s.downcase.to_sym}_id": self.id }, dataset_name: "audit" ) end private def before_state self.class.find(self.id) end end end Notice that I'm using an external gem called "activerecord-diff" to calculate the diff between the before and after.
detect if only one attribute is updated in Rails 4 on update_attributes
I am making a blogging app. I need to have two different methods based on how many attributes have been changed. Essentially, if ONLY the publication_date changes I do one thing...even the publication_date and ANYTHING ELSE changes, I do another thing. posts_controller.rb def special_update if #detect change of #post.publication_date only #do something elsif # #post changes besides publication_date elsif #no changes end end
One way to approach this is in your model using methods provided by ActiveModel::Dirty, which is available to all your Rails Models. In particular the changed method is helpful: model.changed # returns an array of all attributes changed. In your Post model, you could use an after_update or before_update callback method to do your dirty work. class Post < ActiveRecord::Base before_update :clever_method private def clever_method if self.changed == ['publication_date'] # do something else # do something else end end end
craig.kaminsky's answer is good, but if you prefer to mess with your controller instead of your model, you can do that as well: def special_update # the usual strong params thing param_list = [:title, :body] new_post_params = params.require(:post).permit(*param_list) # old post attributes post_params = #post.attributes.select{|k,v| param_list.include(k.to_sym)} diff = (post_params.to_a - new_post_params.to_a).map(&:first) if diff == ['publication_date'] #do something elsif diff.empty? # no changes else # other changes end end
Or simply compare parameter with existing value if params[:my_model][:publication_date] != #my_model.publication_date params[:my_model][:publication_date] = Time.now end
How can I only allow updating of nil attributes for a Rails ActiveRecord object?
I have an ActiveRecord object with multiple attributes that are allowed to be nil on creation and can later be updated by the user through a form. However, once an attribute is changed from nil to non-nil, that attribute may not be updated again. How should I go about setting up this behavior?
create_table :funky do |t| t.integer :fireflies end class Funky < ActiveRecord::Base def fireflies=(ff) raise "Uh uh.. it was already set" unless self.fireflies.blank? write_attribute(:fireflies, ff) end end Editing post as user requested that many fields be edited [:one, :two, :three].each do |s| define_method "#{s}=" do |v| raise "Uh uh.. it was already set" unless self.send(s).blank? write_attribute(s, v) end end
Well, I think you should definitely allow users to change their information at any time, but anyway, if you want to add the restriction to the controller instead of the model you could do this: def update your_model = YourModel.find(params[:id]) # find the attributes with nil values: nil_attributes = your_model.attributes.select {|k,v| v.nil?}.keys.map(&:to_sym) # attributes that you allow to edit: allowed_attributes = [:title, :description, :size] # tell rails which are the allowed modifications: allowed_params = params.require(:your_model).permit(*(allowed_attributes - nil_attributes)) # save the changes: your_model.update_attributes(allowed_params) # ... end
Active Record Clear instance of model after_find callback
I have defined a callback after_find for checking some settings based on the retrieved instance of the model. If the settings aren't fulfilled I don't want the instance to be return from the find method. Is that possible? an example the controller looks like: class UtilsController < ApplicationController def show #util = Util.find(params[:id]) end end the model: class Util < ActiveRecord::Base after_find :valid_util_setting def valid_util_setting # calculate_availability? complex calculation # that can not be part of the sql statement or a scope unless self.setting.calculate_availability?(User.current.session) #if not available => clear the record for view else #nothing to do here end end end
Instead of trying to clear the record, you could just raise an exception? E.g. unless self.setting.calculate_availability?(User.current.session) raise ActiveRecord::RecordNotFound else ...
I'm afraid you can't clear found record in this callback Maybe you should find in scope with all your options from the beginning? I.e. #util = Util.scoped.find(params[:id])
I found a solution def valid_util_setting Object.const_get(self.class.name).new().attributes.symbolize_keys!.each do |k,v| begin self.assign_attributes({k => v})#, :without_protection => true) rescue ActiveModel::MassAssignmentSecurity::Error => e; end end end With this I'm able to create an almost empty object
How can I avoid running ActiveRecord callbacks?
I have some models that have after_save callbacks. Usually that's fine, but in some situations, like when creating development data, I want to save the models without having the callbacks run. Is there a simple way to do that? Something akin to... Person#save( :run_callbacks => false ) or Person#save_without_callbacks I looked in the Rails docs and didn't find anything. However in my experience the Rails docs don't always tell the whole story. UPDATE I found a blog post that explains how you can remove callbacks from a model like this: Foo.after_save.clear I couldn't find where that method is documented but it seems to work.
Use update_column (Rails >= v3.1) or update_columns (Rails >= 4.0) to skip callbacks and validations. Also with these methods, updated_at is not updated. #Rails >= v3.1 only #person.update_column(:some_attribute, 'value') #Rails >= v4.0 only #person.update_columns(attributes) http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_column #2: Skipping callbacks that also works while creating an object class Person < ActiveRecord::Base attr_accessor :skip_some_callbacks before_validation :do_something after_validation :do_something_else skip_callback :validation, :before, :do_something, if: :skip_some_callbacks skip_callback :validation, :after, :do_something_else, if: :skip_some_callbacks end person = Person.new(person_params) person.skip_some_callbacks = true person.save UPDATE (2020) Apparently Rails has always supported :if and :unless options, so above code can be simplified as: class Person < ActiveRecord::Base attr_accessor :skip_some_callbacks before_validation :do_something, unless: :skip_some_callbacks after_validation :do_something_else, unless: :skip_some_callbacks end person = Person.new(person_params) person.skip_some_callbacks = true person.save
This solution is Rails 2 only. I just investigated this and I think I have a solution. There are two ActiveRecord private methods that you can use: update_without_callbacks create_without_callbacks You're going to have to use send to call these methods. examples: p = Person.new(:name => 'foo') p.send(:create_without_callbacks) p = Person.find(1) p.send(:update_without_callbacks) This is definitely something that you'll only really want to use in the console or while doing some random tests. Hope this helps!
Updated: #Vikrant Chaudhary's solution seems better: #Rails >= v3.1 only #person.update_column(:some_attribute, 'value') #Rails >= v4.0 only #person.update_columns(attributes) My original answer : see this link: How to skip ActiveRecord callbacks? in Rails3, assume we have a class definition: class User < ActiveRecord::Base after_save :generate_nick_name end Approach1: User.send(:create_without_callbacks) User.send(:update_without_callbacks) Approach2: When you want to skip them in your rspec files or whatever, try this: User.skip_callback(:save, :after, :generate_nick_name) User.create!() NOTE: once this is done, if you are not in rspec environment, you should reset the callbacks: User.set_callback(:save, :after, :generate_nick_name) works fine for me on rails 3.0.5
If the goal is to simply insert a record without callbacks or validations, and you would like to do it without resorting to additional gems, adding conditional checks, using RAW SQL, or futzing with your exiting code in any way, consider using a "shadow object" pointing to your existing db table. Like so: class ImportedPerson < ActiveRecord::Base self.table_name = 'people' end This works with every version of Rails, is threadsafe, and completely eliminates all validations and callbacks with no modifications to your existing code. You can just toss that class declaration in right before your actual import, and you should be good to go. Just remember to use your new class to insert the object, like: ImportedPerson.new( person_attributes )
rails 3: MyModel.send("_#{symbol}_callbacks") # list MyModel.reset_callbacks symbol # reset
You could try something like this in your Person model: after_save :something_cool, :unless => :skip_callbacks def skip_callbacks ENV[RAILS_ENV] == 'development' # or something more complicated end EDIT: after_save is not a symbol, but that's at least the 1,000th time I've tried to make it one.
You can use update_columns: User.first.update_columns({:name => "sebastian", :age => 25}) Updates the given attributes of an object, without calling save, hence skipping validations and callbacks.
The only way to prevent all after_save callbacks is to have the first one return false. Perhaps you could try something like (untested): class MyModel < ActiveRecord::Base attr_accessor :skip_after_save def after_save return false if #skip_after_save ... blah blah ... end end ... m = MyModel.new # ... etc etc m.skip_after_save = true m.save
Looks like one way to handle this in Rails 2.3 (since update_without_callbacks is gone, etc.), would be to use update_all, which is one of the methods that skips callbacks as per section 12 of the Rails Guide to validations and callbacks. Also, note that if you are doing something in your after_ callback, that does a calculation based on many association (i.e. a has_many assoc, where you also do accepts_nested_attributes_for), you will need to reload the association, in case as part of the save, one of its members was deleted.
The most up-voted answer might seem confusing in some cases. You can use just a simple if check if you would like to skip a callback, like this: after_save :set_title, if: -> { !new_record? && self.name_changed? }
with Rails 6 you can now use the insert methods from the documentation: Inserts multiple records into the database in a single SQL INSERT statement. It does not instantiate any models nor does it trigger Active Record callbacks or validations. Though passed values go through Active Record's type casting and serialization.
https://gist.github.com/576546 just dump this monkey-patch into config/initializers/skip_callbacks.rb then Project.skip_callbacks { #project.save } or the like. all credit to the author
A solution that should work across all versions of Rails without the use of a gem or plugin is simply to issue update statements directly. eg ActiveRecord::Base.connection.execute "update table set foo = bar where id = #{self.id}" This may (or may not) be an option depending on how complex your update is. This works well for eg updating flags on a record from within an after_save callback (without retriggering the callback).
When I need full control over the callback, I create another attribute that is used as a switch. Simple and effective: Model: class MyModel < ActiveRecord::Base before_save :do_stuff, unless: :skip_do_stuff_callback attr_accessor :skip_do_stuff_callback def do_stuff puts 'do stuff callback' end end Test: m = MyModel.new() # Fire callbacks m.save # Without firing callbacks m.skip_do_stuff_callback = true m.save # Fire callbacks again m.skip_do_stuff_callback = false m.save
I needed a solution for Rails 4, so I came up with this: app/models/concerns/save_without_callbacks.rb module SaveWithoutCallbacks def self.included(base) base.const_set(:WithoutCallbacks, Class.new(ActiveRecord::Base) do self.table_name = base.table_name end ) end def save_without_callbacks new_record? ? create_without_callbacks : update_without_callbacks end def create_without_callbacks plain_model = self.class.const_get(:WithoutCallbacks) plain_record = plain_model.create(self.attributes) self.id = plain_record.id self.created_at = Time.zone.now self.updated_at = Time.zone.now #new_record = false true end def update_without_callbacks update_attributes = attributes.except(self.class.primary_key) update_attributes['created_at'] = Time.zone.now update_attributes['updated_at'] = Time.zone.now update_columns update_attributes end end in any model: include SaveWithoutCallbacks then you can: record.save_without_callbacks or Model::WithoutCallbacks.create(attributes)
# for rails 3 if !ActiveRecord::Base.private_method_defined? :update_without_callbacks def update_without_callbacks attributes_with_values = arel_attributes_values(false, false, attribute_names) return false if attributes_with_values.empty? self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).arel.update(attributes_with_values) end end
None of these points to without_callbacks plugin that just does what you need ... class MyModel < ActiveRecord::Base before_save :do_something_before_save def after_save raise RuntimeError, "after_save called" end def do_something_before_save raise RuntimeError, "do_something_before_save called" end end o = MyModel.new MyModel.without_callbacks(:before_save, :after_save) do o.save # no exceptions raised end http://github.com/cjbottaro/without_callbacks works with Rails 2.x
I wrote a plugin that implements update_without_callbacks in Rails 3: http://github.com/dball/skip_activerecord_callbacks The right solution, I think, is to rewrite your models to avoid callbacks in the first place, but if that's impractical in the near term, this plugin may help.
If you are using Rails 2. You could use SQL query for updating your column without running callbacks and validations. YourModel.connection.execute("UPDATE your_models SET your_models.column_name=#{value} WHERE your_models.id=#{ym.id}") I think it should work in any rails versions.
For creating test data in Rails you use this hack: record = Something.new(attrs) ActiveRecord::Persistence.instance_method(:create_record).bind(record).call https://coderwall.com/p/y3yp2q/edit
You can use sneaky-save gem: https://rubygems.org/gems/sneaky-save. Note this cannot help in saving associations along without validations. It throws error 'created_at cannot be null' as it directly inserts the sql query unlike a model. To implement this, we need to update all auto generated columns of db.
For custom callbacks, use an attr_accessor and an unless in the callback. Define your model as follows: class Person << ActiveRecord::Base attr_accessor :skip_after_save_callbacks after_save :do_something, unless: :skip_after_save_callbacks end And then if you need to save the record without hitting the after_save callbacks you defined, set the skip_after_save_callbacks virtual attribute to true. person.skip_after_save_callbacks #=> nil person.save # By default, this *will* call `do_something` after saving. person.skip_after_save_callbacks = true person.save # This *will not* call `do_something` after saving. person.skip_after_save_callbacks = nil # Always good to return this value back to its default so you don't accidentally skip callbacks.
Why would you want to be able to do this in development? Surely this will mean you are building your application with invalid data and as such it will behave strangely and not as you expect in production. If you want to populate your dev db with data a better approach would be to build a rake task that used the faker gem to build valid data and import it into the db creating as many or few records as you desire, but if you are heel bent on it and have a good reason I guess that update_without_callbacks and create_without_callbacks will work fine, but when you are trying to bend rails to your will, ask yourself you have a good reason and if what you are doing is really a good idea.
One option is to have a separate model for such manipulations, using the same table: class NoCallbacksModel < ActiveRecord::Base set_table_name 'table_name_of_model_that_has_callbacks' include CommonModelMethods # if there are : : end (Same approach might make things easier for bypassing validations) Stephan
Another way would be to use validation hooks instead of callbacks. For example: class Person < ActiveRecord::Base validate_on_create :do_something def do_something "something clever goes here" end end That way you can get the do_something by default, but you can easily override it with: #person = Person.new #person.save(false)
Something that should work with all versions of ActiveRecord without depending on options or activerecord methods that may or may not exist. module PlainModel def self.included(base) plainclass = Class.new(ActiveRecord::Base) do self.table_name = base.table_name end base.const_set(:Plain, plainclass) end end # usage class User < ActiveRecord::Base include PlainModel validates_presence_of :email end User.create(email: "") # fail due to validation User::Plain.create(email: "") # success. no validation, no callbacks user = User::Plain.find(1) user.email = "" user.save TLDR: use a "different activerecord model" over the same table
I faced the same problem when I wanted to run a Rake Task but without running the callbacks for every record I was saving. This worked for me (Rails 5), and it must work for almost every version of Rails: class MyModel < ApplicationRecord attr_accessor :skip_callbacks before_create :callback1 before_update :callback2 before_destroy :callback3 private def callback1 return true if #skip_callbacks puts "Runs callback1" # Your code end def callback2 return true if #skip_callbacks puts "Runs callback2" # Your code end # Same for callback3 and so on.... end The way it works is that it just returns true in the first line of the method it skip_callbacks is true, so it doesn't run the rest of the code in the method. To skip callbacks you just need to set skip_callbacks to true before saving, creating, destroying: rec = MyModel.new() # Or Mymodel.find() rec.skip_callbacks = true rec.save
Not the cleanest way, but you could wrap the callback code in a condition that checks the Rails environment. if Rails.env == 'production' ...