Referencing Associated Object Returns Nil - ruby-on-rails

I have two models.
class User < ActiveRecord::Base
has_one :message
end
class Message < ActiveRecord::Base
belongs_to :user
end
If I have a created user with an associated Message and I delete that message and create a new one like, user.message returns nil. For example.
user = User.create
message = Message.create(user_id: user.id)
Message.where(user_id: user.id).destroy_all
Message.create(user_id: user.id)
# Now if I call this below, it always returns nil
user.message
Why does this occur? Shouldn't Rails 3 pick up on that change? How do I fix this?

Just load the object again before doing user.message like, user.reload.
reload - Reloads the record from the database.

Related

Filter nested model attributes in RoR

I have two models: Users and PaymentMethods, the association between this models is:
class User < ApplicationRecord
has_many :payment_methods, dependent: :destroy
end
class PaymentMethod < ApplicationRecord
belongs_to :user, optional: true
end
I want to loop in each user and see in an attribute of PaymentMethod, named 'period_end_date'. so I do this:
#users = User.all
#users.each do |u|
u.payment_methods.last.period_end_date
end
I'm getting this error => NoMethodError: undefined method `payment_methods' for User::ActiveRecord_Relation
The error is shown because I have 2 test users, in the first user there is still no data in the attribute 'period_end_date' and association exist, but is empty, in the second user there is data in the attributes, if I say, u.payment_methods.last.period_end_date I get => Wed, 13 Jun 2018 (only in the second user)
I want to filter in my loop only the users who has data in PaymentMethod attributes for get rid of => NoMethodError: undefined method `payment_methods' for User::ActiveRecord_Relation
How I do this?
thanks
I want to filter in my loop only the users who has data in PaymentMethod attributes for get rid of => NoMethodError: undefined method `payment_methods' for User::ActiveRecord_Relation
The actual problem seems to be you have users without payment methods (see my comment on your question).
You have some options, depending on how you're going to use the results.
1) You can filter out users without payment methods when you query them from the database like this:
#users = User.joins :payment_methods
2) If #users must include users that without payment methods, you can skip them when looping like this:
#users.map do |user|
next unless user.payment_methods.any?
user.payment_methods.last.period_end_date
end
3) You can guard by checking for payment_methods before calling .last.
User.all.map do |user|
user.payment_methods.last.period_end_date if user.payment_methods.any?
end
4) You can add a period_end_date method to the user
class User < ApplicationRecord
def period_end_date
payment_methods.limit(1).pluck :period_end_date
end
end
5) push #4 into the association by extending it with a helper method
class User < ApplicationRecord
has_many :payment_methods, class_name: 'PaymentMethod' do
def last_period_end_date
last.period_end_date if any?
end
end
end
which you can call like this
User.all.map do |user|
user.payment_methods.last_period_end_date
end
If you're really only concerned about PaymentMethods without a period_end_date then try this:
6) You can still filter users when you query them from the database
#users = User.joins(:payment_methods).where.not(payment_methods: { period_end_date: nil })
7) This can be simplified a bit by pushing the where.not conditions into a scope of the PaymentMethod class:
class PaymentMethod < ApplicationRecord
scope :period_ends, -> { where.not period_end_date: nil }
end
and merging it
#users = User.joins(:payment_methods).merge PaymentMethod.period_ends
Notes
payment_methods.last doesn't specify an order, you should set one (either as part of this chain, when you specify the association, or with a default scope) otherwise the order is up to your database and may be indeterminate.
chain .includes(:payment_methods) to eager load the payment methods and avoid n+1 queries
it sounds like a nil period_end_date could be invalid data. Consider adding a validation / database constraint to prevent this from happening

Rails update associated model's object

My Opportunity model looks like this:
class Opportunity < ApplicationRecord
# opportunity belongs to a user
belongs_to :user
def self.create_opportunity(params)
# Fetch opportunity params and user params from request parameters
opportunity_params = params[:opportunity_params]
user_params = params[:user_params]
opportunity = Opportunity.find_or_initialize_by(id: opportunity_params[:id])
opportunity.assign_attributes(opportunity_params)
opportunity.user = User.find_or_initialize_by(email: user_params[:email])
opportunity.user.assign_attributes(user_params)
opportunity.save
end
user.rb model
class User < ApplicationRecord
# validate user email
validates :email, presence: true, email: true
enum gender: { male:1, female:2 }
end
We create a new user if a user with the email provided does not exists. This works well when a new user is created, but doesn't work when there already is a user. The update for user model doesn't work.
For update on user to work, I need to specifically call opportunity.user.save
Any idea how to make this work without explicitly calling save on user model?
With this line you can create a new user if it does not exist and update its data.
opportunity.user.find_or_create_by(email: user_params[:email]).update(user_params)
This method will return true/false if the user has been updated or not. If you want the user created itself use this:
opportunity.user.find_or_create_by(email: user_params[:email]).tap{ |user| user.update(user_params) }
edit:
THIS HAS TO WORK:
opportunity.user = User.find_or_create_by(email: user_params[:email]).tap{ |user| user.update(user_params) }

rails autosave nested model selected with where/find_by

i have a special case for which i need to know the best practice.
Given a simple has_many association:
class Authentication < ActiveRecord::Base
belongs_to :user
#provider can be :password, :facebook_oauth etc
#code is the encrypted password on provider == :password
end
class User < ActiveRecord::Base
has_many :authentications
#this works
def encrypted_password=(pw)
set = false
self.authentications.each do |auth|
if auth.provider.to_sym == :password
set = true
auth.code = pw
end
end
self.authentications.build(provider: :password, code: pw) unless set
pw
end
#this only when no password-auth exist yet
def encrypted_password=(pw)
self.authentications.find_or_initialize_by(provider: :password).code = pw
end
end
and then
user = User.last
user.password="abcdefg"
user.save
While the first solution works, it loads and iterates over ALL associated Authentication objects. It was a workaround but this is a no-go.
The second solution does not work when it loads an existing Password-Authentication object. The User object does not know about the change on the Authentication object loaded with the find_or_initialize_by method. The change won't be saved...
Is there a way to register the changed Authentication object back to the User object so that it will be autosaved when called user.save?
It seems saving associating object returned with find back to parent object is impossible as of now. Refer to this issue https://github.com/rails/rails/issues/17466.
I had the same issue, and my workaround was, even though this is not you nor I wanted, to use save in the method yourself and make all the saves inside the transaction.
def encrypted_password=(pw)
self.authentications.find_or_initialize_by(provider: :password).update_attribute(code, pw)
end
Is there a way to register the changed Authentication object back to the User object so that it will be autosaved when called user.save?
If your question only consists of needing to know how to save an associated class, you can add this to your class definition:
class User < ActiveRecord::Base
has_many :authentications, autosave: true
end
The Authentication object is already referenced back to the User object via the user_id column that should be on Authentication by way of the belongs_to method. This autosave: true will save the associated object Authentication when the parent object (User) is saved.

Rails: passing params hash to model

I have a user-to-user messaging system. I'm trying to pass an array of user ids to a ConversationUser (join table) model which would then create multiple conversation_users from each individual user.id. The two fields in ConversationUser are conversation_id and user_id. I'm able to initialize a single conversation user because the new conversation_id is being passed along to the model, but for some reason, the hash of user ids is not getting to my model. I'm getting a Validation failed: User can't be blank
My conversation/new view for capturing the user_ids:
<%= check_box_tag "conversation_user[recipient][]", user.id %> <%= user.name %><br />
I know this is working because part of my params that I'm receiving back are:
"conversation_user"=>{"recipient"=>["9", "10"]}
The essentials of my Rails 4 controller & strong params:
class ConversationsController < ApplicationController
def new
#user = User.find(params[:user_id])
#conversation = #user.conversation_users.build
#conversation.build_conversation.messages.build
end
def create
#conv = Conversation.create!
#conversation = #conv.conversation_users.create!(conversation_user_params)
end
def conversation_user_params
params.require(:conversation_user).permit(recipient: [])
end
The essentials of my ConversationUser model:
class ConversationUser < ActiveRecord::Base
attr_accessor :recipient
before_create :acquire_conversation
validates :user_id, :conversation_id, presence: true
def acquire_conversation
unless recipient.blank?
recipient.each do |u|
ConversationUser.create(user_id: u, conversation: conversation)
end
end
end
end
I think the problem is somewhere in my controller's conversation_user_params. But it also might be in the model's before_create method. I've been trying to fix this problem for a day now, with lots of debugging with no success. If anyone can be of assistance, I thank you in advance.
The problem is in the model. before_create callback is called before creating a ConversationUser. Let's name this created ConversationUser as CURRENT. So, before creating the CURRENT ConversationUser you loop through recipient ids and create a ConversationUser for each of them. The ConversationUsers that you are creating here are not CURRENT ConversationUser. CURRENT ConversationUser is saved after the callback is executed (after you create other ConversationUsers). But in this case CURRENT ConversationUser doesn't know wich User it belongs to, because you pass user_id parameter to ConversationUsers that you create in before_create callback, but you do not pass it to CURRENT ConversationUser when it is created (when original create! method is executed).
To solve this problem you can override original create! method or not use it at all for creating ConversationUsers by recipient ids. Add a new method to your Conversation model (for example create_conversation_users):
Solution
In the controller:
def create
#conv = Conversation.create!
#conversation = #conv.create_conversation_users!(conversation_user_params[:recipient])
end
In the model:
class Conversation
def create_conversation_users!(recipient_ids)
return if recipient_ids.blank?
recipient_ids.each do |recipient_id|
conversation_users.create!(user_id: recipient_id, conversation: self)
end
end
end
You should also update ConversationUser model:
class ConversationUser < ActiveRecord::Base
validates :user_id, :conversation_id, presence: true
end
The error is in the ConversationUser. before_create callbacks are ran before a record is created in the database BUT after validations are ran. To solve your issue, there's a few things you can do. One of them was answered by Chumakoff. Here's another option you can use.
Remove all the code inside ConversationUser and change conversation_user_params to
def conversation_user_params
params[:conversation_user][recipient].map do |recipient|
{ user_id: recipient }
end
end
What happens is you're passing an array of { user_id: 1 } to create! which is the same as calling multiple create!({ user_id: 1 }).

How can I validate that two associated objects have the same parent object?

I have two different objects which can belong to one parent object. These child objects can both also belong to each other (many to many). What's the best way to ensure that child objects which belong to each other also belong to the same parent object.
As an example of what I'm trying to do I have a Kingdom which has both many People and Land. The People model would have a custom validate which checks each related Land and error.adds if one has a mismatched kingdom_id. The Land model would have a similar validate.
This seems to work, but when updating it allows the record to save the 'THIS IS AN ERROR' error is in people.errors, however the Land which raised the error has been added to the People collection.
kingdom = Kingdom.create
people = People.create(:kingdom => kingdom)
land = Land.create(:kingdom_id => 999)
people.lands << land
people.save
puts people.errors.inspect # #messages={:base=>["THIS IS AN ERROR"]
puts people.lands.inspect # [#<Land id: 1...
Ideally I'd want the error to cancel the record update. Is there another way I should be going about this, or am I going in the wrong direction entirely?
# Models:
class Kingdom < ActiveRecord::Base
has_many :people
has_many :lands
end
class People < ActiveRecord::Base
belongs_to :kingdom
has_and_belongs_to_many :lands
validates :kingdom_id, :presence => true
validates :kingdom, :associated => true
validate :same_kingdom?
private
def same_kingdom?
if self.lands.any?
errors.add(:base, 'THIS IS AN ERROR') unless kingdom_match
end
end
def kingdom_match
self.lands.each do |l|
if l.kingdom_id != self.kingdom_id
return false
end
end
end
end
class Land < ActiveRecord::Base
belongs_to :kingdom
has_and_belongs_to_many :people
end
Firstly, the validation won't prevent the record from being added to the model's unpersisted collection. It will prevent the revised collection from being persisted to the database. So the model will be in an invalid state, and flagged as such with the appropriate errors. To see this, you can simply reload the people object.
You also have an error in your logic - the kingdom_match method will never return true even if no invalid kingdom_id's are found. You should add a line to fix this:
def kingdom_match
self.lands.each do |l|
return false if l.kingdom_id != self.kingdom_id
end
true
end
And you can make this validation a bit more concise and skip the kingdom_match method entirely:
def same_kingdom?
if self.lands.any?{|l| l.kingdom_id != self.kingdom_id }
errors.add(:base, 'THIS IS AN ERROR')
end
end

Resources