I have a simple set up of User and UserProfile model with User has_one :user_profile and UserProfile belongs_to :user.
But I am unable to wrap my head around how Rails defines execution order of after_create callback and accepts_nested_attributes_for defined in my model. Lets consider these two cases.
Case 1:
class User < ActiveRecord::Base
has_one :user_profile
accepts_nested_attributes_for :user_profile
after_create :test_test
end
Now, if I create a user(with user_profile_attributes hash too) via the console, the after_create callback is triggered after the user and its user profile is created.
Case 2:
If the after_create is placed at the top,
class User < ActiveRecord::Base
after_create :test_test
has_one :user_profile
accepts_nested_attributes_for :user_profile
end
the callback is triggered after a user has been created but before creating a user profile.
Is this the way it is expected to function. What does Rails do internally here? Is the execution sequence simply determined by the order of the code?
Where do I start to dig deeper into or debug this ?
The order of the declarations in your model can have an impact on the execution order of the code. This is a source for various weird things. (for example, currently callback definitions and has_and_belongs_to_many associations are order dependent: https://github.com/rails/rails/pull/8674 )
To debug the issue you need to browse the rails source. Since your problem has to do with execution order, callbacks and nested attributes I would start by reading up on:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/nested_attributes.rb#L256
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/callbacks.rb#L302
https://github.com/rails/rails/blob/master/activemodel/lib/active_model/callbacks.rb#L98
This gives you the necessary background to dig deeper. You'll notice that accepts_nested_attributes_for calls into add_autosave_association_callbacks https://github.com/rails/rails/blob/master/activerecord/lib/active_record/autosave_association.rb#L173
This method adds an after_create callback and as far as I know callbacks are executed in order of definition.
Related
I need some help to solve my problem.
I'm working on a project where I develop the manager's functions of the application. My problem is related with two Destroy functions and how they related with themselves:
Simplifying, I have 3 models (Test, Application and JoinTestJob) described below in code sample.
I want the manager be able to destroy a JoinTestJob's object only if this is related with any Application's objects. I need consistency in Application's objects. That is the reason I created a before_destroy method in JoinTestJob model. Until here is working fine.
I also want the manager be able to destroy a Test's object, and with that, the associated objects should be destroy too, as declared in Test model. So here start the problem:
When I delete a Test's object I can see in log that all Application's objects associated are deleted first. After that, should be JoinTestJob's objects to be deleted, but I got a rollback. I know the reason of the rollback is the before_destroy method in JoinTestJob (I can see it in log). Apparently, this method still can find Application's objects, even though I saw in log that they were deleted. In this case, it seems they really would be deleted after the transaction is done.
So, how can I accomplish to having those two features working?
Model:
class Test < ApplicationRecord
has_many :applications, :dependent => :destroy
has_many :persons, through: :applications
has_many :join_test_jobs, :dependent => :destroy
has_many :jobs, through: :join_test_jobs
class Application < ApplicationRecord
belongs_to :join_test_job
belongs_to :person
class JoinTestJob < ApplicationRecord
belongs_to :test
belongs_to :job
before_destroy :check_relation_between_application_and_join_test_job
def check_relation_between_application_and_join_test_job
if Application.find_by(join_test_job_id: "#{self.id}")
self.errors.add(:base, "It's not possible to delete this item, because there are some applications related with. Please, delete them first.")
throw(:abort)
end
end
Edit 1:
About the log requested in comments, it's really basic. I will ask to you guys ignore some details in the image. I had to translate the problem to English, so there are some Portuguese words in the image. What is important to observe in the log:
All Application's objects related to Test are deleted first. Then, when rails started to look up for JoinTestJob's objects you can see the rollback. Before the rollback, there is a last request from Application trigged by the before_destroy method, as you can see in Log's image. The throw(:abort) method is called in this last request.
Word's Translation:
Application/applications => Inscricao/inscricaos
JoinTestJob/join_test_jobs => JoinConcursoCargo/join_concurso_cargos
To clarify things, DadosPessoalCandidato and DadosPortadorDeficiencia are models associated with Application and they hold personal informations about who is applying for, like address and phone. They can be ignored.
Edit 2:
Adding more information about what represent the models, so you guys can understand the problem better.
Test model: It's like a Civil service exam, where people would apply for getting a job. Who gets the best score in the exam would be selected for the job. This model hold information as: when this exam will happen, documents related with the exam's rules, when the result of the exam will be release and etc.
JoinTestJob model: It joins the concept of a Job and the Test that are offering these jobs. Here, we could find information as: specificity of the job, like the period of work, how much hours of work per day, salary and etc.
Application model: This model hold information related with the person who is applying for the job and the Test that are offering the job. So, we could find here information as: what job the person apply for, how much he/she payed for applying, the date of the Test (exam) will be happening, personal informations as age, name, address. A person could apply for more than one job for the same Test.
I hope I can help you guys with these new informations.
I think you should change your models:
class Test < ApplicationRecord
has_many :applications, :dependent => :destroy
class Application < ApplicationRecord
has_one :join_test_job, dependent: :destroy
class JoinTestJob < ApplicationRecord
belongs_to :application
validates :application, presence: true
so, if a Test is destroyed, then applications relates with destroyed test will be destroyed, and then JoinTestJob relates with destroyed applications are also destroyed.
I assume JoinTestJob model is stored in join_test_jobs model and have application_id.
You can make field store application_id is NOT NULL at Database level, and add validates :application, presence: true code make sure that a JoinTestJob always has one related application.
I have the following classes:
class Account
has_many :payment_methods
has_many :orders
class PaymentMethod
belongs_to :account, inverse_of: :payment_methods
has_many :payments
class Order
belongs_to :account, inverse_of: :orders
has_many :payments
class Payment
belongs_to :order, inverse_of: :payments
belongs_to :payment_method, inverse_of: :payments
I have an order form that creates a payment as a child. That payment also has an associated payment_method. The idea is that placing an order will build the order and payment objects, the order will call payment.process (below), the payment will call a charge method on the payment_method object, and if all that passes, the payment and order are saved. Still with me?
Ok, so I need to validate at the payment level that both order and payment_method have the same parent (account), otherwise it needs to fail validation and not try to process. I have this method in the payment class:
def process
charge = payment_method.charge amount
self.is_paid = true
self.transaction_id = charge.id
rescue Exception => e
# handle exception
end
And this validation:
def payment_method_belongs_to_account
if payment_method and payment_method.account != order.account
# add error
end
end
I'm running into a few issues.
I am calling payment_method.charge before the payment is validated.
I can solve this by explicitly calling self.validate inside the payment.process method before calling payment_method.charge, but that leads to #2 below.
If I attempt to call validate on the payment object, I get an exception because payment.order is nil. The order hasn't been saved yet, so the reference inside the child object (payment) hasn't been set up.
It seems like solving for #2 is the easiest solution. I'm not sure why the reference from the child (payment) to the parent (order) isn't getting set up. Any suggestions?
I figured out the answer to my own question. I'll leave this here in case it can help someone else.
Adding this to my Order class fixed the problem:
has_many :payments, inverse_of: :order
It seems like that should happen automatically, but obviously not.
I have associated models User and Channel in Rails 3 app. Channel is created at the moment of User creation
class User < ActiveRecord::Base
before_create do
self.channels.build
end
has_many :channels
end
class Channel < ActiveRecord::Base
belongs_to :user
validations block
...
end
Problem is that if validations for Channel will not pass User will be created at DB but Channel won't. In what callback place Channel creation to create User and Channel in one 'transaction'? Or, maybe, there is another right way?
Thanks in advance.
UPD1:
Channel autocreate on User create placed in model because in some cases objects created not invoking controllers.
You can use "accepts_nested_attributes_for"
class User < ActiveRecord::Base
has_many :channels
accepts_nested_attributes_for :channels
end
class Channel < ActiveRecord::Base
belongs_to :user
validations block
end
You think too much. This is very common case and has a convention.
Firstly at Pedro said, you need a validation of association in Channel model. This will prevent saving of channel without user_id.
Then, in controller's create action, you just make sure all params including user object is sent here for creation.
Use validates :channels, associated: true.
You should probably review your Channel validations because if it is not saving, you're doing something your app doesn't expect.
I have the following model:
class PhoneNumber < ActiveRecord::Base
has_many :personal_phone_numbers, :dependent => :destroy
has_many :people, :through => :personal_phone_numbers
end
I want to set up an observer to run an action in a delayed_job queue, which works for the most part, but with one exception. I want the before_destroy watcher to grab the people associated with the phone number, before it is destroyed, and it is on those people that the delayed job actually works.
The problem is, when a phone number is destroyed, it destroys the :personal_phone_numbers record first, and then triggers the observer when it attempts to destroy the phone number. At that point, it's too late.
Is there any way to observe the destroy action before dependent records are deleted?
While this isn't ideal, you could remove the :dependent => :destroy from the personal_phone_numbers relationship, and delete them manually in the observer after operating on them.
However, I think that this issue might be showing you a code smell. Why are you operating on people in an observer on phone number. It sounds like that logic is better handled in the join model.
Use alias_method to intercept the destroy call?
class PhoneNumber < ActiveRecord::Base
has_many :personal_phone_numbers, :dependent => :destroy
has_many :people, :through => :personal_phone_numbers
alias_method :old_destroy, :destroy
def destroy(*args)
*do your stuff here*
old_destroy(*args)
end
end
It sounds like your problem in a nutshell is that you want to gather and act on a collection of Person when a PersonalPhoneNumber is destroyed. This approach may fit the bill!
Here is an example of a custom callback to collect Person models. Here it's an instance method so we don't have to instantiate a PersonalPhoneNumberCallbacks object in the ActiveRecord model.
class PersonalPhoneNumberCallbacks
def self.after_destroy(personal_phone_number)
# Something like people = Person.find_by_personal_phone_number(personal_phone_number)
# Delayed Job Stuff on people
end
end
Next, add the callback do your ActiveRecord model:
class PersonalPhoneNumber < ActiveRecord::Base
after_destroy PictureFileCallbacks
end
Your after_destroy callback will have the model passed down and you can act on its data. After the callback chain is complete, it will be destroyed.
References
http://guides.rubyonrails.org/active_record_validations_callbacks.html#relational-callbacks
http://guides.rubyonrails.org/association_basics.html#association-callbacks
You can use a before_destroy callback in the model, then grab the data and do whatever operation you need to before destroy the parent. Something like this example should be what you are looking for:
class Example < ActiveRecord::Base
before_destroy :execute_random_method
private
def execute_random_method
...
end
handle_asynchronously :execute_random_method
A bit old but thought I'd share that rails now has the nice 'prepend' option for the before_destroy callback now. This goes follows the same line of thought with tomciopp had but allows you to define the before_destroy whereever in the class.
before_destroy :find_associated_people, prepend: true
def find_associated_people
# using phone number, find people
end
I have models Group, Membership and User. Connected with a has_many :through association.
Route wise, membership is nested inside group.
What I want is that whenever someone joins or leaves the group (ie. on create or destroy membership), to initiate a check on the group to check what the dominating language is (this is an attribute in the User model) and update the language attribute in the Group model.
I have a method called define_language in the Group model which seems to work independently.
Now I need to call this method from the Membership model, I was thinking to do this with an after_save callback, but I'm having trouble referencing it to the method in the (different) Group model.
I put this method in the Group model and not the Membership model as I feel semantically it has little to do with the membership. Is this assumption wrong? How would I go about this problem in an efficient way?
One way is :
class Membership < ActiveRecord::Base
belongs_to :group
before_save :update_group_language
...
private
def update_group_language
self.group.define_language
end
end
I can't see how this could work though :
class Membership < ActiveRecord::Base
belongs_to :group
before_save group.define_language
end
The problem with that is that belongs_to is only evaluated by Ruby when rails is first loaded.
I figured it out, you just run in Membership.rb
before_save group.define_language
And tadaa! It will call define_language in Group.rb model.
Optional you can add such to define the relation:
before_save group.define_language "id = #{group_id}"