rails before_destroy partial delete - ruby-on-rails-3

I'm trying to figure out a way to partially delete/destroy dependent models in rails.
Code looks something like this:
class User < ActiveRecord::Base
has_many :subscriptions
has_many :photos, :dependent => :destroy
has_many :badges, :dependent => :destroy
before_destroy :partial_destroy
def partial_destroy
self.photos.destroy_all
self.badges.destroy_all
return false if self.subscriptions.any?
end
...
Essentially, I want to destroy the photos and badges, but if the user has any subscriptions, I want to keep those, and also keep the user from being destroyed.
I tried with .each { |obj| obj.destroy } and using delete and delete_all, but it seems to not matter.
It looks like rails is performing some kind of a rollback whenever the before_destroy returns false. Is there a way to destroy part of the dependents but not others?

This is old so I expect you've forgotten it, but I stumbled across it.
I'm not surprised delete and delete_all didn't work, since those bypass callbacks.
You're exactly right that Rails performs a rollback if any before_ callback returns false. Because Rails wraps the entire callback chain in a transaction, you're not going to be able to perform database calls (like destroys) inside the chain. What I would recommend is putting a conditional in the callback:
If the user has subscriptions, kick off a background job which will do this partial delete later (outside the callback transaction), and return false from the callback.
If they don't have subscriptions, you don't start the background job, return true from the callback, and destroy your model as usual.

I ended up doing the following:
override destroy on the User model (see below)
not actually deleting the User, but rather destroying the dependants that are not needed, and blanking any fields on the User model itself, e.g. email.
I created a UserDeleter class that takes the user and performs all clearing operations, just to keep things cleaner / having some kind of single-responsibility
overriding destroy
def destroy
run_callbacks(:destroy) do
UserDeleter.new(self).delete
end
end
deleting dependants and clearing data on User
class UserDeleter
def initialize(user)
#user = user
end
def delete
delete_photos
delete_badges
clear_personal_data
# ...
end
private
def delete_photos
#user.photos.destroy_all
end
def clear_personal_data
#user.update_attributes!(
:email => deleted_email,
:nickname => '<deleted>')
end
def deleted_email
"deleted##{random_string}.com"
end
def random_string(length = 20)
SecureRandom.hex(length)[0..length]
end
#...
end

Related

What is better way to create Client record after User is registered (corresponding client record)

Basically i have user registering himself to the app , by using devise gem.
Instead of having standard sign up form like (email, password) i have an extra 2 fields (name, contact_nr) in total used (name, contact_nr, email, password, password_confirm) fields, :name and :contact_nr attributes exists in 'clients' table only.
Table name: clients
id :integer not null, primary key,
name :string(255)
surname :string(255)
contact_nr :string(255)
user_id :integer
class Client < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_one :client, dependent: :destroy
after_create :update_user_client
def name
return unless client
client.name
end
def contact_nr
return unless client
client.contact_nr
end
def update_user_client
Client.last.update_attributes(user: self)
end
end
In my RegistrationsController I have only one method
class RegistrationsController < Devise::RegistrationsController
before_action :create_client
private
def create_client
return if params[:user].blank?
Client
.new(name: params[:user][:name],
contact_nr: params[:user][:contact_nr])
.save(validate: false)
end
end
What bothers me is that kind of writing code, it feels like code smell.
How would you implement it?
Thanks guys looking forward to your answers..
First advice I can give is do not separate client and user into two tables if you don't have valid reasons and/or requirements for now. That would make things much easier.
If you have valid reasons, here are my advices on how to improve your existing state of this code piece:
Rails and all mature gems around it rely on 'convention over configuration', so you should check if there are conventional ways to achieve same results.
In your RegistrationsController instead of doing params[:user].blank? check, you should use Devise's way of doing this, provided with inherited methods as devise_parameter_sanitizer.permit within a before_action callback.
Instead of creating client in your controller, move that to model logic, and in your user model put accepts_nested_attributes_for :client.
Since both of your models(client and user) share same name, put a before_save callback, so that you can pass user's name attribute to client itself.
after_create callback is very risky, since it is not an atomic save (no guarantee that client will be updated after user record is updated.). So don't use it. accepts_nested_attributes_for will handle both create and update calls.
If name attribute for user would be fetched through client only, there is no need to keep name within user.
If you want to access client's contact_nr and name attributes directly from user model, then use delegate method inside it.
Putting all together, I would refactor that code piece as this:
class User < ActiveRecord::Base
has_one :client, dependent: :destroy
accept_nested_attributes_for :client
delegate :name, to: :client
delegate :contact_nr, to: :client
# optional. if you want to keep name attr in both models.
before_save :sync_names
private
def sync_names
self.client.name = name if client.present?
end
end
class RegistrationsController < Devise::RegistrationsController
before_action :configure_permitted_parameters
protected
def configure_permitted_parameters
added_attrs = [:name, :email, :password, :password_confirmation, client_attributes: [:contact_nr]]
devise_parameter_sanitizer.permit :sign_up, keys: added_attrs
devise_parameter_sanitizer.permit :account_update, keys: added_attrs
end
end
Don't forget to update your signup and account update forms to accept nested attributes for client resource.
As far as you are validating the data with JS and filtering with params.require(:client).permit, the code looks fine. Try to create many differente scenarios in your Rspec. The test usually reveals unexpected flaws.

Best way to update elements from an has_many relationship without instantiating full models and triggering the callbacks

This is something I'm trying to do now: A has_many Bs. B has certain callbacks that need to be triggered. Just, when I save from A, I want something to be updated in Bs. Since the Bs can be 10000, I would like not to load them into memory, and still have the benefit of seeing the callbacks triggered. What is the best strategy for this?
And please, no "find_each" or any find by batches variant, I'm aware of them and they will be my last resort in case nothing else works.
When I encountered this problem, I use this solution
define "callback methods" in a class and use they with ids,
define really callbacks in a instance and pass id of record in "class callback"
example of code:
class Post < AR
has_many :comments
after_save do |post|
Post.recalculate_counters(post.comment_ids)
end
end
class Comment < AR
belongs_to :post
after_save :recalculate_couters
def self.recalculate_couters(ids)
... huge and slow update statement ...
end
def recalcuate_couters
self.class.recalculate_couters([id])
end
end
I don't think there's any way you can have the callbacks executed without loading the models. However, if you give up using callback you can use update_all that performs really fast.
You just use:
B.update_all({:timestamp => Time.now}, { :a_id => id })

Rails 3: HABTM, destroy and before_destroy callback

If I have read the rails guides correctly, a before_destroy callback that returns false will stop the object being destroyed by issuing a rollback command.
However, while the object itself is not being removed I'm finding that all objects in a HABTM relationship on that object are. How can I stop this from happening?
Here's the appropriate code block:
class UserGroup < ActiveRecord::Base
# Associations
has_and_belongs_to_many :users, :join_table => "user_group_membership"
attr_protected :is_default
# Callbacks
before_destroy :destroy_associations
def destroy_associations
if self.is_default?
errors.add(:base,"You can't delete the default")
return false
end
self.users.clear
end
end
Now when I call destroy on a "is_default" group, I get the correct error message added to the base, the UserGroup object is NOT destroyed but every association in the :users collection is cleared.
I know that the code "self.users.clear" is never reached - so why am I losing my HABTM collection?
If it helps, I am using JRuby 1.9 with an mssql database.

Whenever a User object is created, create UserInfo object too

Whenever a User object is created, I want a UserInfo object to be created too, and linked to it.
Unfortunately this does not create any UserInfo:
class User < ActiveRecord::Base
has_one :user_info
...
def init
self.user_info = user_info
self.save!
end
Why is the init method not called? How to reach my goal?
sombe's technique is right, but his details aren't ideal. In fact, since create_user_info is already a method on User instances, all you want is something like:
class User < ActiveRecord::Base
has_one :user_info
before_create :create_user_info
end
Edit: init doesn't do anything particularly magical under Rails (I... don't think it does under basic Ruby either - are you thinking of initialize? I'll assume you are). initialize is fired off when an instance of the Ruby class is created in memory. That's divorced by quite some margin from an instance of the model being created in the database; a new class instance could be due to you calling build (and not saving yet), or even due to reading an instance out of the database.
If you want to step in on database operations, you need to make use of the ActiveRecord callbacks. You might find my answer to this question useful.
before_save callback triggers on create and update.
I'd suggest to use after_create because before_create can return errors
class User < ActiveRecord::Base
has_one :user_info
...
after_create do
create_user_info
end
In your User model, use a before_save filter instead of init like this:
before_save :create_user_info
...
private
def create_user_info
user_info = UserInfo.new
if user_info.save
self.user_info_id = user_info.id
end
end

devise after create hook

Is there a hook or callback that I can implement so that right after the user is created, I would like to invoke some custom code ?
I tried after_confirmation hook in the user model but that didn't work.
Use the standard after_create callback provided by Rails.
class User < ActiveRecord::Base
after_create :do_something
def do_something
puts "Doing something"
end
end
Using a callback is perfectly legit if you're dealing with the internal state of the model you created.
After creating a User, I needed to create default a Team. It's preferable to avoid using callbacks to deal with other objects.
“after_*” callbacks are primarily used in relation to saving or persisting the object. Once the object is saved, the purpose (i.e. responsibility) of the object has been fulfilled, and so what we usually see are callbacks reaching outside of its area of responsibility, and that’s when we run into problems.
From this awesome blog post.
In this case it's better to act on the controller, where you can add your functionality directly, or delegate to a service for an even cleaner solution:
# shell
rails g devise:controllers users
# config/routes.rb
devise_for :users, controllers: { registrations: "users/registrations" }
# app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
after_action :create_default_team, only: :create
private
def create_default_team
Team.create_default(#user) if #user.persisted?
end
end
I'm using Rails 4 with Devise 3.5 with confirmable and had to do this due to various surprises.
class User < ActiveRecord::Base
# don't use after_create, see https://github.com/plataformatec/devise/issues/2615
after_commit :do_something, on: :create
private
def do_something
# don't do self.save, see http://stackoverflow.com/questions/22567358/
self.update_column(:my_column, "foo")
end
end