I am developing a Rails 5 application in which I encountered the following difficulty.
I've got two models, let's say Kid and Toy, which are in one-to-one relationship like this:
class Kid < ActiveRecord::Base
has_one :toy
end
class Toy
belongs_to :kid, optional: true
end
So the toys can belong to zero or one kid, and from day to day it can change - it is always another kid's responsibility to look after a certain toy. Now, when I edit a toy, changing its kid is easy as can be: I just send kid_id in the strong params to update the record:
params.require(:toy).permit(:name, :type, :kid_id)
But recently, I was asked to implement the changing feature from the other way too, that is, when editing a kid, I should do something like this:
params.require(:kid).permit(:name, :age, :toy_id)
The problem is that - while belongs_to works with association_id and even has_many provides association_ids getter and setter - has_one relationship has nothing like this. What is more, has_one association gets saved the moment I call association = #record. So I simply cannot set it by sending the toy_id in the strong parameters.
I could do something like #kid.update(kid_params); #kid.toy = #toy on the controller level, but that would rather bring model logics to my controller, not to mention that I want to check if the newly assigned toy did not belong to another kid, which I imagine as some kind of validation.
The best I could come up with was to define some rails-like methods for Kid class like
def toy_id
#toy_id = toy.id unless defined?(#toy_id)
#toy_id
end
def toy_id_changed?
toy_id != toy.id
end
and set a validation and a before_commit callback
validate if: -> { toy_id.present? && toy_id_changed? } do
errors.add :toy_id, :other_has_it if new_toy.kid_id.present? && new_toy.kid_id != id
end
before_commit if: -> { toy_id_changed? } do
toy = new_toy
end
private
def new_toy
#new_toy ||= Toy.find(toy_id)
end
So far it works as expected, and now I can send toy_id in the strong params list to update a kid, and it updates the toy association if -
and only if - there is no validation error. I have even put it in a concern to be nice and separated.
My question is: isn't there a rails way to do this? haven't I reinvented the wheel?
Thanks in advance!
Related
In my Rails 5.2 app, I have a polymorphic model Vehicle of types Car, Bike, Jeep etc. which has belongs_to association vehicle_type. I would like to validate associated record attribute display_name. The following code snippet does the job but I would like to know a better way to do this.
class Car < Vehicle
validates :vehicle_type,
:inclusion => {
:in => [VehicleType.find_by(display_name: 'four wheeler')],
:message => "A Car can only be of vehicle_type 'four wheeler'",
}
}
You should put the validation on the id rather than the display name since you would have to refactor your code if you ever decide to change the display name.
class VehiculeType
FOUR_WHEELER = 1 (id of the four_wheeler type)
end
class Car < Vehicule
validate :validate_vehicule_type
private
def validate_vehicule_type
errors.add(:vehicule, "A Car can only be of vehicle_type 'four wheeler'") unless vehicule_type_id == VehiculeType::FOUR_WHEELER
end
end
I don't know what is the best way, but I'll share what i have done in one of my projects:
I decided to extend ActiveModel::Validator and create my own validation for my polymorphic associations
In you case
class CarValidator < ActiveModel::Validator
def validate_vehicle_type(record)
# where did you save the veicle type associatuon?
unless VehicleType.find_by(display_name: record.veicle_type).exists?
record.errors.add :veicle_type, "This veicle type does not exist"
end
end
then validates with CarValidator
I agree with Mathieu Larouche. One small thing I would add to this discussion is that this is not really a polymorphic association, as polymorphic associations are about how "a model can belong to more than one other model, on a single association". This is done via a combination of type and id fields (imageable_id and imageable_type, for example). See docs here.
It doesn't really affect the response your question, but I just wanted to mention it because polymorphic associations took me forever to wrap my head around, and I thought calling out the distinction could be helpful.
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
Context:
Each Order has many Items & Logistics. Each Item & Logistic (as well as the Order itself) have many Revenues.
I am creating Order + Items & Logistics at once using an accepts_nested_attributes_for on Order. However, Revenues gets created using an after_create callback on each of the models Order, Item, and Logistics. Why? Because given the difference in interpretation in these models, the code reads cleaner this way. (But if this way of doing it is what's causing this question to be asked, I will obviously reconsider!)
One key attribute that I need to store in Revenues is pp_charge_id. But pp_charge_id is not something that either Order, Items, or Logistics needs to worry about. I've attached an attr_accessor :pp_charge_id to Order, so that one works fine, however, once I'm in the child Items or Logistics models, I no longer have access to pp_charge_id which again I need to save an associated Revenue. How should I do this?
Controller Code:
#order = Order.new(params) #params includes Order params, and nested params for child Item & Logistics
#order.pp_charge_id = "cash"
#order.save #I need this to not only save the Order, the children Item & Logistics, but then to also create the associated Revenue for each of the aforementioned 3 models
ORDER Model Code:
has_many :items
has_many :revenues
attr_accessor :pp_charge_id
after_create :create_revenue
def create_revenue
self.revenues.create(pp_charge_id: self.pp_charge_id)
end
#This WORKS as expected because of the attr_accessor
ITEM/ LOGISTIC model code:
has_many :revenues
belongs_to :order
after_create :create_revenue
def create_revenue
self.revenues.create(pp_charge_id: self.order.pp_charge_id)
end
#This DOES NOT work because self.order.pp_charge_id is nil
ORDER model code:
belongs_to :order
belongs_to :item
belongs_to :logistic
Again I understand the attr_accessor is not designed to persist across a request or even if the Order itself is reloaded. But it also doesn't make sense to save it redundantly in a table that has no use for it. If the only way to do this is to put the pp_charge_id into the params for the order and save everything all at once (including Revenues), then let me know because I know how to do that. (Again, would just rather avoid that because of how it's interpreted: params are coming from User, Revenue data is something I'm providing)
I think if you want the order's pp_charge_id to apply to all its items and logistics, I'd put all that into the order's after_create callback:
# order.rb
def create_revenue
revenues.create(pp_charge_id: pp_charge_id)
items.each {|i| i.revenues.create(pp_charge_id: pp_charge_id)}
logistics.each {|l| l.revenues.create(pp_charge_id: pp_charge_id)}
end
EDIT: Alternately, you could add inverse_of to your belongs_to declarations, and then I believe Item#create_revenue would see the same Order instance that you set in the controller. So if you also added an attr_accessor to the Item class, you could write its create_revenue like this:
# item.rb
def create_revenue
revenues.create(pp_charge_id: pp_charge_id || order.pp_charge_id)
end
This should cover the new requirement you've mentioned in your comment.
instead of using after_create and accessors you should consider having a proper method that does exactly what you need, ie:
Order.create_with_charge(:cash, params)
i find it disturbing to persist redundant information in the database just because the code reads cleaner that way!
I'm not getting a concept (nothing new there) on how to scope a Active Record query. I want to only receive the records where there is a certain condition in a related record. The example I have happens to be polymorphic just in case that is a factor. I'm sure there is somewhere where this is explained but I have not found it for whatever reason.
My Models:
class User < ActiveRecord::Base
belongs_to :owner, polymorphic: true
end
class Member < ActiveRecord::Base
has_one :user, as: :owner
end
I want to basically run a where on the Member class for related records that have a certain owner_id/owner_type.
Lets say we have 5 Members with ids 1-5 and we have one user with the owner_id set to 3 and the owner_type set to 'Member'. I want to only receive back the one Member object with id 3. I'm trying to run this in Pundit and thus why I'm not just going at it form the User side.
Thanks for any help as always!!!
Based on your comment that you said was close I'd say you should be able to do:
Member.joins(:user).where('users.id = ?', current_user.id)
However based on how I'm reading your question I would say you want to do:
Member.joins(:user).where('users.owner_id = ?', current_user.id)
Assuming current_user.id is 3.
There may be a cleaner way to do this, but that's the syntax I usually use. If these aren't right, try being a little more clear in your question and we can go from there! :)
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?