callback on associated has_many not working - ruby-on-rails

Seems like i’ve gone back to basics and am missing something… ..
I have two models User and Lists. User has many lists and lists belongs to User… When we remove the user from the list, i.e. the lists user_id == nil, we can’t seem to catch the update through a callback like after_update or after_commit.
class List < ApplicationRecord
after_update :check_user
belongs_to :user
private
def check_user
binding.pry
if user_id.blank?
end
end
end
we even tried before_save but it doesn’t seem like the model sees the change. Are we missing something obvious??

What method are you using to update the record? Because there are methods that DO NOT trigger callbacks like update_column, update_attribute and update_all

Related

Custom relationship similar to dependent destroy

I would like to implement certain relationship between 2 models.
I have 2 models: quiz and question that have many-to-many relationship.
Quiz model have quiz_flag and question model have question_flag.
What I want to happen is when quiz_flag is changed to true, every question that is in direct relationship (basically every question that is contained within that quiz), should also change question_flag to true.
Logic is similar to dependent: :destroy, but it's a custom function that I want to trigger when quiz_flag becomes true.
But how do I specifically do that?
You could just add additional logic to whatever form/action is responsible for setting quiz.
I.e.:
if params[:quiz_flag] #if the quiz_flag params is set to true.
#quiz.questions.update_all(question_flag: true)
end
Or if it's for multiple controllers, you could use callbacks:
Quiz Model:
before_save :some_method #will work before object is saved
(works with both create and update, if you just want update use before_update)
def some method
if self.quiz_flag == true
self.questons.update_all(question_flag:true)
end
end
I would caution you on using callbacks though. It can lead to some messy code that will be difficult to test for later.
You can use the callback :before_update inside your model.
I'd do something like this:
class Quiz < ApplicationRecord
before_update :update_question_flags, :if => :question_flag_changed?
def update_question_flags
self.questons.update_all(question_flag:true)
end
end

Rails: Save data in two tables

I have a user table and a setting table with 1-1 relationship. I want to insert some default setting for the newly created user. I am thinking to use after_create callback of user. However, I am not sure if this will be transactional. What is the best approach for this condition?
You may find after_initialize callback useful for building the setting object for user and assigning default setting. Example:
class User < ActiveRecord::Base
has_one :setting
after_initialize :init_user_setting
private
def init_user_setting
# Assign default setting or build
self.setting = ...
end
end
With this you'll have your complete parent user including child setting. When you call user.save both user and setting are saved and both happen inside a transaction.
You can use after_create callback or observer both. Both will be okay. But You should also set your default values for settings in a initialize method for attribute assignment. Or you can also use user's create method to do same after save call. But it's not a good way. So prefer either call_back or observer.
--UPDATE--
As you start registering new callbacks for your models, they will be queued for execution. This queue will include all your model's validations, the registered callbacks, and the database operation to be executed.
The whole callback chain is wrapped in a transaction. If any before callback method returns exactly false or raises an exception, the execution chain gets halted and a ROLLBACK is issued; after callbacks can only accomplish that by raising an exception.
After setting an attribute to false at the end of a before_save callback, I could not for the life of me figure out why the object would never save! Before callbacks must return truthy values or it will rollback.
--Previous--
You need to do something like this. This is my default approach for anything short of dealing with currency.
class User < ActiveRecord::Base
has_one :setting
after_create :setup_user
private
def setup_user
user_settings = self.setting.new
user_settings.attr1 = foo1
user_settings.attr2 = foo2
user_settings.save
end
end
class Setting < ActiveRecord::Base
belongs_to :user
end
Ideally you would put validations on the user and make it so that if a user is valid a setting is also valid. But if you don't do that, you need to use
if !user_settings.save
self.destroy
end
For where to put the default values,if the setting default values depend on the user, stick them in the setup_user method. If the setting default values doesn't care about the user, stick them in a before_save or before_validation method on the setting. With regards to the user, you need to use an after_create method for the case where a user doesn't validate, you want those callbacks to only fire if a user is successfully created. You don't want to user after_validation because the user would not have been created and if the setting contains a foreign id to the user that is not yet created, for an instant your database will be inconsistent.
Transaction based approach
class User < ActiveRecord::Base
has_one :setting
after_commit :setup_user, on: [:create]
after_rollback :undo_user, on: [:create]
private
def setup_user
user_settings = self.setting.new
user_settings.attr1 = foo1
user_settings.attr2 = foo2
user_settings.save!
end
def undo_user
#The users settings didn't save so roll back the user
self.destroy
end
end
class Setting < ActiveRecord::Base
belongs_to :user
end
To put it in the same transaction you need to use after_commit and after_rollback. Using .save! will throw an exception and trigger after_rollback. You will not have a user without the setting.

Rails: set default record if none assigned or if relation removed

I've got a model called Brand, on which several things rely including in this example a model called User. If a Brand is deleted then a lot of things will fail. What's the best way to set a default Brand for all its relationships in the event that a Brand is deleted?
I thought writing stuff like this might work:
class User < ActiveRecord::Base
after_save :assign_to_default_brand, :if => :not_branded?
def not_branded?
!self.brand_id?
end
def assign_to_default_brand
self.brand_id = Brand.first
end
end
But it doesn't seem to behave the way I want it to. Is there a best-practice established here? Cheers.
UPDATED
I've thrown a default boolean onto Brand and written this but again it seems to have no effect. Am I missing something?
class Brand < ActiveRecord::Base
after_save :assign_users_to_default
def assign_users_to_default
self.users.all.each { |user| user.brand_id = Brand.where(:default => true).first.id if user.not_branded? }
end
end
It should be a before_save instead of after_save That way the value will be persisted to the database when the instance is saved.
For deletion on a brand you could use after_destroy
class Brand
after_destroy :switch_assigned_users
def switch_assigned_users
User.where(:brand_id => id).update_all(:brand_id => Brand.first)
end
end
This finds all users that assigned to that brand and switches them to the first one.

Rails - Best-Practice: How to create dependent has_one relations

What's the best practice to create has_one relations?
For example, if I have a user model, and it must have a profile...
How could I accomplish that?
One solution would be:
# user.rb
class User << ActiveRecord::Base
after_create :set_default_association
def set_default_association
self.create_profile
end
end
But that doesn't seem very clean... Any suggestions?
Best practice to create has_one relation is to use the ActiveRecord callback before_create rather than after_create. Or use an even earlier callback and deal with the issues (if any) of the child not passing its own validation step.
Because:
with good coding, you have the opportunity for the child record's validations to be shown to the user if the validations fail
it's cleaner and explicitly supported by ActiveRecord -- AR automagically fills in the foreign key in the child record after it saves the parent record (on create). AR then saves the child record as part of creating the parent record.
How to do it:
# in your User model...
has_one :profile
before_create :build_default_profile
private
def build_default_profile
# build default profile instance. Will use default params.
# The foreign key to the owning User model is set automatically
build_profile
true # Always return true in callbacks as the normal 'continue' state
# Assumes that the default_profile can **always** be created.
# or
# Check the validation of the profile. If it is not valid, then
# return false from the callback. Best to use a before_validation
# if doing this. View code should check the errors of the child.
# Or add the child's errors to the User model's error array of the :base
# error item
end
Your solution is definitely a decent way to do it (at least until you outgrow it), but you can simplify it:
# user.rb
class User < ActiveRecord::Base
has_one :profile
after_create :create_profile
end
If this is a new association in an existing large database, I'll manage the transition like this:
class User < ActiveRecord::Base
has_one :profile
before_create :build_associations
def profile
super || build_profile(avatar: "anon.jpg")
end
private
def build_associations
profile || true
end
end
so that existing user records gain a profile when asked for it and new ones are created with it. This also places the default attributes in one place and works correctly with accepts_nested_attributes_for in Rails 4 onwards.
Probably not the cleanest solution, but we already had a database with half a million records, some of which already had the 'Profile' model created, and some of which didn't. We went with this approach, which guarantees a Profile model is present at any point, without needing to go through and retroactively generate all the Profile models.
alias_method :db_profile, :profile
def profile
self.profile = Profile.create(:user => self) if self.db_profile.nil?
self.db_profile
end
Here's how I do it. Not sure how standard this is, but it works very well and its lazy in that it doesn't create extra overhead unless it's necessary to build the new association (I'm happy to be corrected on this):
def profile_with_auto_build
build_profile unless profile_without_auto_build
profile_without_auto_build
end
alias_method_chain :profile, :auto_build
This also means that the association is there as soon as you need it. I guess the alternative is to hook into after_initialize but this seems to add quite a bit of overhead as it's run every time an object is initialized and there may be times where you don't care to access the association. It seems like a waste to check for its existence.
There is a gem for this:
https://github.com/jqr/has_one_autocreate
Looks like it is a bit old now. (not work with rails3)
I had an issue with this and accepts_nested_attributes_for because if nested attributes were passed in, the associated model was created there. I ended up doing
after_create :ensure_profile_exists
has_one :profile
accepts_nested_attributes_for :profile
def ensure_profile_exists
profile || create_profile
end
If you need the has_one association to exist before saving the object (when testing, for instance), you should use the after_initialize callback instead. Here is how it could be applied to your use case:
class User < ActiveRecord::Base
has_one :profile
after_initialize :build_profile, unless: :profile
end

How can I access ActiveRecord Associations in class callbacks in rails?

Updated
Appears to be a precedence error and nothing to do with the question I originally asked. See discussion below.
Original question
Is it possible to use active record associations in callbacks? I've tested this code in the console and it works fine as long as it isn't in a callback. I'm trying to create callbacks that pull attributes from other associated models and I keep getting errors of nil.attribute.
If callbacks are not the correct approach to take, how would one do a similar action in rails? If the associations are simple, you could use create_association(attributes => ), but as associations get more complex this starts to get messy.
For example...
class User < ActiveRecord::Base
belongs_to :b
before_validation_on_create {|user| user.create_b} #note, other logic prevents creating multiple b
end
class B < ActiveRecord::Base
has_many :users, :dependent => destroy
after_create{ |b| b.create_c }
has_one :c
end
class C < ActiveRecord::Base
belongs_to :b
after_create :create_alert_email
private
def create_alert_email
self.alert_email = User.find_by_b_id(self.b_id).email #error, looks for nil.email
end
end
Off course associations are available in your callbacks. After all, the create_after_email is simply a method. You can even call it alone, without using a callback. ActiveRecord doesn't apply any special flag to callback methods to prevent them from working as any other method.
Also notice you are running a User#find query directly without taking advantage of any association method. An other reason why ActiveRecord association feature should not be the guilty in this case.
The reason why you are getting the error should probably searched somewhere else.
Be sure self.b_id is set and references a valid record. Perhaps it is nil or actually there's no User record with that value. In fact, you don't test whether the query returns a record or nil: you are assuming a record with that value always exists. Are you sure this assumption is always statisfied?

Resources