My "has_many through" join model has nil reference after saving - ruby-on-rails

I'm trying to create an object and adding an existing object to a "has_many through" association, but after saving my object the reference to my newly created object is set to nil in the join model.
To be specific, I'm creating a Notification object and adding a pre-existing Member object to the Notification.members association. I'm using nested resources and I'm invoking the notification controller's new function using the following relative URL:
/members/1/notifications/new
After filling out the form and submitting, the create function is called, and from what I understand from the Rails Associations guide, section 4.3.3 "When are Objects Saved?", the members associations should be created in the database when the new notification object is saved:
"If the parent object (the one declaring the has_many association) is unsaved (that is, new_record? returns true) then the child objects are not saved when they are added. All unsaved members of the association will automatically be saved when the parent is saved."
After creating the notification object, the following record was created in the database:
select id, notification_id, notifiable_type, notifiable_id from deliveries;
1|<NULL>|Member|1
I worked around the problem by saving the notification object before adding the member object to the association. At first this seemed to be an ok solution for now, but I soon discovered that this has it's downsides. I don't want to save the notification without it's member association since I then have to write workarounds for my callbacks so that they don't start performing tasks on the not yet valid notification object.
What am I doing wrong here? All tips are appreciated. :D
Models
class Notification < ActiveRecord::Base
has_many :deliveries, :as => :notifiable
has_many :members, :through => :deliveries, :source => :notifiable, :source_type => "Member"
has_many :groups, :through => :deliveries, :source => :notifiable, :source_type => "Group"
end
class Member < ActiveRecord::Base
has_many :deliveries, :as => :notifiable
has_many :notifications, :through => :deliveries
end
class Delivery < ActiveRecord::Base
belongs_to :notification
belongs_to :notifiable, :polymorphic => true
end
# Group is not really relevant in this example.
class Group < ActiveRecord::Base
has_many :deliveries, :as => :notifiable
has_many :notifications, :through => :deliveries
end
Controller
class NotificationsController < ApplicationController
def create
#notification = Notification.new(params[:notification])
#member = Member.find(params[:member_id])
#notification.members << #member
respond_to do |format|
if #notification.save
...
end
end
end
end

After posting a bug report, I got som help from one of the Rails gurus. In short, it's impossible to get this working the way I thought.
I decided to proceed with slightly more controller code, seems to be working just fine:
def create
#notification = Notification.new(params[:notification])
#member = Member.find(params[:member_id])
respond_to do |format|
if #notification.save
#member.notifications << #notification
#member.save
...

Related

How to detect changes in has_many through association?

I have the following models.
class Company < ApplicationRecord
has_many :company_users
has_many :users, :through => :company_users
after_update :do_something
private
def do_something
# check if users of the company have been updated here
end
end
class User < ApplicationRecord
has_many :company_users
has_many :companies, :through => :company_users
end
class CompanyUser < ApplicationRecord
belongs_to :company
belongs_to :user
end
Then I have these for the seeds:
Company.create :name => 'Company 1'
User.create [{:name => 'User1'}, {:name => 'User2'}, {:name => 'User3'}, {:name => 'User4'}]
Let's say I want to update Company 1 users, I will do the following:
Company.first.update :users => [User.first, User.second]
This will run as expected and will create 2 new records on CompanyUser model.
But what if I want to update again? Like running the following:
Company.first.update :users => [User.third, User.fourth]
This will destroy the first 2 records and will create another 2 records on CompanyUser model.
The thing is I have technically "updated" the Company model so how can I detect these changes using after_update method on Company model?
However, updating an attribute works just fine:
Company.first.update :name => 'New Company Name'
How can I make it work on associations too?
So far I have tried the following but no avail:
https://coderwall.com/p/xvpafa/rails-check-if-has_many-changed
Rails: if has_many relationship changed
Detecting changes in a rails has_many :through relationship
How to determine if association changed in ActiveRecord?
Rails 3 has_many changed?
There is a collection callbacks before_add, after_add on has_many relation.
class Project
has_many :developers, after_add: :evaluate_velocity
def evaluate_velocity(developer)
#non persisted developer
...
end
end
For more details: https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Association+callbacks
You can use attr_accessor for this and check if it changed.
class Company < ApplicationRecord
attr_accessor :user_ids_attribute
has_many :company_users
has_many :users, through: :company_users
after_initialize :assign_attribute
after_update :check_users
private
def assign_attribute
self.user_ids_attribute = user_ids
end
def check_users
old_value = user_ids_attribute
assign_attribute
puts 'Association was changed' unless old_value == user_ids_attribute
end
end
Now after association changed you will see message in console.
You can change puts to any other method.
I have the feelings you are asking the wrong question, because you can't update your association without destroy current associations. As you said:
This will destroy the first 2 records and will create another 2 records on CompanyUser model.
Knowing that I will advice you to try the following code:
Company.first.users << User.third
In this way you will not override current associations.
If you want to add multiple records once try wrap them by [ ] Or ( ) not really sure which one to use.
You could find documentation here : https://guides.rubyonrails.org/association_basics.html#has-many-association-reference
Hope it will be helpful.
Edit:
Ok I thought it wasn't your real issue.
Maybe 2 solutions:
#1 Observer:
what I do it's an observer on your join table that have the responsability to "ping" your Company model each time a CompanyUser is changed.
gem rails-observers
Inside this observer call a service or whatever you like that will do what you want to do with the values
class CompanyUserObserver < ActiveRecord::Observer
def after_save(company_user)
user = company_user.user
company = company_user.company
...do what you want
end
def before_destroy(company_user)
...do what you want
end
end
You can user multiple callback in according your needs.
#2 Keep records:
It turn out what you need it keep records. Maybe you should considerate use a gem like PaperTrail or Audited to keep track of your changes.
Sorry for the confusion.

Activerecord has_one build new when association not found

So I have this model relationships
class User
has_one :wallet, :foreign_key => :user_id
end
class Wallet
after_initialize :set_value
def set_value
# Whatever
end
end
And I'd like that when I do User.last.wallet, User.last.wallet.new gets called.
I could achieve this by creating another method in the User model:
def get_wallet
self.wallet||self.wallet.new
end
and call get_wallet when needed.
But can't I get this without this useless and dirty extra method?
Something like:
has_one :wallet, :foreign_key => :user_id #, :build_if_not_found => true
Gems like this one: https://github.com/febuiles/auto_build don't do what I want: they build Wallet after creating the User object instead of creating when User.last.wallet is called.
Thanks
You can try this:
class User
has_one :wallet, :foreign_key => :user_id
def wallet
super || build_wallet
end
end
You still need to add some extra code, but it will do exactly what you want without any additional calls.

Add record to a model upon create used in many models

I have a survey and I would like to add participants to a Participant model whenever a user answers to a question for the first time. The survey is a bit special because it has many functions to answer questions such as Tag words, Multiple choices and Open Question and each function is actually a model that has its own records. Also I only want the Participant to be saved once.
The Participant model is fairly simple:
class Participant < ActiveRecord::Base
belongs_to :survey
attr_accessible :survey_id, :user_id
end
The Survey model is also straightforward:
class Survey < ActiveRecord::Base
...
has_many :participants, :through => :users
has_many :rating_questions, :dependent => :destroy
has_many :open_questions, :dependent => :destroy
has_many :tag_questions, :dependent => :destroy
belongs_to :account
belongs_to :user
accepts_nested_attributes_for :open_questions
accepts_nested_attributes_for :rating_questions
accepts_nested_attributes_for :tag_questions
...
end
Then you have models such as rating_answers that belong to a rating_question, open_answers that belong to open_questions and so on.
So initially I thought for within my model rating_answers I could add after_create callback to add_participant
like this:
class RatingAnswer < ActiveRecord::Base
belongs_to :rating_question
after_create :add_participant
...
protected
def add_participant
#participant = Participant.where(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
if #participant.nil?
Participant.create!(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
end
end
end
In this case, I didn't know how to find the survey_id, so I tried using the params but I don't think that is the right way to do it. regardles it returned this error
NameError (undefined local variable or method `current_user' for #<RatingAnswer:0x0000010325ef00>):
app/models/rating_answer.rb:25:in `add_participant'
app/controllers/rating_answers_controller.rb:12:in `create'
Another idea I had was to create instead a module Participants.rb that I could use in each controllers
module Participants
def add_participant
#participant = Participant.where(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
if #participant.nil?
Participant.create!(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
end
end
end
and in the controller
class RatingAnswersController < ApplicationController
include Participants
def create
#rating_question = RatingQuestion.find_by_id(params[:rating_question_id])
#rating_answer = RatingAnswer.new(params[:rating_answer])
#survey = Survey.find(params[:survey_id])
if #rating_answer.save
add_participant
respond_to do |format|
format.js
end
end
end
end
And I got a routing error
ActionController::RoutingError (uninitialized constant RatingAnswersController::Participants):
I can understand this error, because I don't have a controller for participants with a create method and its routes resources
I am not sure what is the proper way to add a record to a model from a nested model and what is the cleaner approach.
Ideas are most welcome!
current_user is a helper that's accessible in views/controller alone. You need to pass it as a parameter into the model. Else, it ain't accessible in the models. May be, this should help.
In the end I ended up using the after_create callback but instead of fetching the data from the params, I used the associations. Also if #participant.nil? didn't work for some reason.
class RatingAnswer < ActiveRecord::Base
belongs_to :rating_question
after_create :add_participant
...
protected
def add_participant
#participant = Participant.where(:user_id => self.user.id, :survey_id => self.rating_question.survey.id)
unless #participant.any?
#new_participant = Participant.create(:user_id => self.user.id, :survey_id => self.survey.rating_question.id)
end
end
end
The cool thing with associations is if you have deeply nested associations for instead
Survey has_many questions
Question has_many answers
Answer has_many responses
in order to fetch the survey id from within the responses model you can do
self.answer.question.survey.id
very nifty!

(Rails Question) Merging multiple polymorphic has_many relationships

(This is not the actual code I'm using, although this sums up the idea of what I want to do)
class Connection < ActiveRecord::Base
belongs_to :connection1, :polymorphic => true
belongs_to :connection2, :polymorphic => true
end
class User < ActiveRecord::Base
has_many :followers, :class_name => 'Connection', :as => :connection1
has_many :followings, :class_name => 'Connection', :as => :connection2
end
My question is that I want to know how I will be able to create a method called "network" such that what is returned isn't an array. Like so,
u = User.first
u.network # this will return a merged version of :followings and :followers
So that I'll still be able to do this:
u.network.find_by_last_name("James")
ETA:
Or hmm, I think my question really boils down to if it is possible to create a method that will merge 2 has_many associations in such a way that I can still call on its find_by methods.
Are you sure that you want a collection of Connections, rather than a collection of Users?
If it's a collection of Connections that you need, it seems like you'll be well served by a class method on Connection (or scope, if you like such things).
connection.rb
class Connection < ActiveRecord::Base
class << self
def associated_with_model_id(model, model_id)
include([:connection1, :connection2]).
where("(connection1_type IS #{model} AND connection1_id IS #{model_id})
OR (connection2_type IS #{model} AND connection2_id IS #{model_id})")
end
end
end
user.rb
class User < ActiveRecord::Base
def network
Connection.associated_with_model_id(self.class.to_s, id)
end
end
Probably not as useful as you'd like, but maybe it'll give you some ideas.

ActiveRecord (Rails 2.3.8) - Update existing, add new record when updating nested attributes

I have a "user" model that "has_one" "membership" (active at a time). For auditing and data integrity reasons, I'd like it so that if the membership changes for a user, the old/current record (if existing) has an inactive/active flag swapped, and a new row is added for the new changed record. If there are no changes to the membership, I'd like to just ignore the update. I've tried implementing this with a "before_save" call-back on my user model, but have failed many times. Any help is greatly appreciated.
models:
class User < ActiveRecord::Base
has_one :membership, :dependent => :destroy
accepts_nested_attributes_for :membership, :allow_destroy => true
end
class Membership < ActiveRecord::Base
default_scope :conditions => {:active => 1}
belongs_to :user
end
I have what I think is a pretty elegant solution. Here's your user model:
class User < ActiveRecord::Base
has_one :membership, :dependent => :destroy
accepts_nested_attributes_for :membership
def update_membership_with_history attributes
self.membership.attributes = attributes
return true unless self.membership.changed?
self.membership.update_attribute(:active, false)
self.build_membership attributes
self.membership.save
end
end
This update_membership_with_history method allows us to handle changed or unchanged records. Next the membership model:
class Membership < ActiveRecord::Base
default_scope :conditions => {:active => true}
belongs_to :user
end
I changed this slightly, since active should be a boolean, not 1's and 0's. Update your migration to match. Now the update action, which is the only part of your scaffold that needs to change:
def update
#user = User.find(params[:id], :include => :membership)
membership_attributes = params[:user].delete(:membership_attributes)
if #user.update_attributes(params[:user]) && #user.update_membership_with_history(membership_attributes)
redirect_to users_path
else
render :action => :edit
end
end
We're simply parsing out the membership attributes (so you can still use fields_for in your view) and updating them separately, and only if needed.
Did you look at acts_as_versioned? In the before_save of the Membership you could create a new version of the User, which would be acts_as_versioned.
Got it working. While it's probably not the best implementation, all my tests are passing. Thanks for the input guys.
before_save :soft_delete_changed_membership
def soft_delete_changed_membership
if !membership.nil? then
if !membership.new_record? && membership.trial_expire_at_changed? then
Membership.update_all( "active = 0", [ "id = ?", self.membership.id ] )
trial_expire_at = self.membership.trial_expire_at
self.membership = nil
Membership.create!(
:user_id => self.id,
:trial_expire_at => trial_expire_at,
:active => true
)
self.reload
end
end
end
Why don't you just assume that the latest membership is the active one. This would save you a lot of headache.
class User < ActiveRecord::Base
has_many :memberships, :dependent => :destroy
end
class Membership < ActiveRecord::Base
nested_scope :active, :order => "created_at DESC", :limit => 1
belongs_to :user
def update(attributes)
self.class.create attributes if changed?
end
end
then you can use
#user.memberships.active
to get the active membership, and you can just update any membership to get a new membership, which will become the active membership because it is the latest.

Resources