Rails setter method that updates two other fields? - ruby-on-rails

Background
I have some polymorphic relationships - one in particular assetable where I have a parent-child Asset relationship and the parent has various classes.
I also have a global Tracker model that creates a global id type scheme across my various models which I also use the FriendlyID gem for.
My Goal
Something like this:
parent = Tracker.find_by(tracker: '4Q73XEGK').trackable
Asset.find(1).update(parent_tracker: parent.tracker_id)
thinking I could do something like this - set the new polymorphic relationship by the tracker_id:
class Asset < ActiveRecord::Base
def parent_tracker
assetable.tracker_id
end
def parent_tracker=(val)
a = Tracker.find_by(tracker: val).trackable
assetable_id = a.id
assetable_type = a.class.name
end
end
Question?
Am I on the right path here (with tweaks) OR show that I am on the completely wrong path here.
I know that there are before_save filters etc. but the setter approach seems more elegant and clear as I can apply this across many other models.

You should not have to set both the type and id - use the setter created by the association instead:
class Asset < ActiveRecord::Base
belongs_to :assetable, polymorphic: true
def parent_tracker
assetable.tracker_id
end
def parent_tracker=(val)
self.assetable = Tracker.find_by(tracker: val).trackable
end
end
The setter for polymorphic associations will set both the id and type attributes. Also note that you need to use self explicitly when calling setters.
assetable_id = a.id
assetable_type = a.class.name
Will just set local variables that are garbage collected when the method ends.
Tracker.find_by(tracker: val) feels really smelly too. If your Tracker class just keeps track of global ids shouldn't it provide a method that takes such an id and returns the trackable?
class Tracker < ActiveRecord::Base
def self.lookup(global_id)
find_by(tracker: global_id).trackable
end
end
class Asset < ActiveRecord::Base
belongs_to :assetable, polymorphic: true
# ...
def parent_tracker=(val)
self.assetable = Tracker.lookup(val)
end
end

Related

Is overriding an ActiveRecord relation's count() method okay?

Let's say I have the following relationship in my Rails app:
class Parent < ActiveRecord::Base
has_many :kids
end
class Kid < ActiveRecord::Base
belongs_to :parent
end
I want parents to be able to see a list of their chatty kids, and use the count in paginating through that list. Here's a way to do that (I know it's a little odd, bear with me):
class Parent < ActiveRecord::Base
has_many :kids do
def for_chatting
proxy_association.owner.kids.where(:chatty => true)
end
end
end
But! Some parents have millions of kids, and p.kids.for_chatting.count takes too long to run, even with good database indexes. I'm pretty sure this cannot be directly fixed. But! I can set up a Parent#chatty_kids_count attribute and keep it correctly updated with database triggers. Then, I can:
class Parent < ActiveRecord::Base
has_many :kids do
def for_chatting
parent = proxy_association.owner
kid_assoc = parent.kids.where(:chatty => true)
def kid_assoc.count
parent.chatty_kids_count
end
end
end
end
And then parent.kids.for_chatting.count uses the cached count and my pagination is fast.
But! Overriding count() on singleton association objects makes the uh-oh, I am being way too clever part of my brain light up big-time.
I feel like there's a clearer way to approach this. Is there? Or is this a "yeah it's weird, leave a comment about why you're doing this and it'll be fine" kind of situation?
Edit:
I checked the code of will_paginate, seems like it is not using count method of AR relation, but i found that you can provide option total_entries for paginate
#kids = #parent.kids.for_chatting.paginate(
page: params[:page],
total_entries: parent.chatty_kids_count
)
This is not working
You can use wrapper for collection like here
https://github.com/kaminari/kaminari/pull/818#issuecomment-252788488​,
just override count method.
class RelationWrapper < SimpleDelegator
def initialize(relation, total_count)
super(relation)
#total_count = total_count
end
def count
#total_count
end
end
# in a controller:
relation = RelationWrapper.new(#parent.kids.for_chatting, parent.chatty_kids_count)

Rails how to find id by another attr in a callback model?

I'm looking to create a callback where update a object if find the id attribute of another model.
in this case if find update Odata model if find the order_id.
someone know how to find the object based on another model id attribute?
class Order < ActiveRecord::Base
after_update :update_odata
def update_odata
order = Order.find_by_id(attributes['id'])
od = Odata.find_by_id(attributes['order_id'])
od.shipping_cost = order.shipping_cost
od.shipping_method = order.shipping_method
od.status = order.status
od.feedback_id = order.feedback_id
od.track_number = order.track_number
od.seller_name = order.seller_name
od.buyer_name = order.buyer_name
od.save
end
end
In general you should check the docs and at least make an effort to learn the tools you're using before resorting to asking for someone to help explain it to you on StackOverflow.
To answer your question, find(1) is effectively a shortcut method for find_by(id: 1). Thusly, if you want to find an order by customer_id you could do this: Order.find_by(customer_id: 42).
Or, if you're trying to make this contingent on order (making some assumptions based on how Rails apps are built vs this unusual attributes stuff you have in your example):
order = Order.find(params[:id])
od = Odata.find_by(order_id: order.id)
In which case, you should probably just use relations:
class Order < ApplicationRecord
has_one :odata
end
class Odata < ApplicationRecord
belongs_to :order
end
# controller:
order = Order.find params[:id]
od = order.odata
If you wanted to do exactly what you are above, which is probably a bad path to go down, you would probably want to do something like this:
class Order < ApplicationRecord
has_one :odata
def attributes_for_odata
%w{ shipping_cost shipping_method status feedback_id track_number seller_name buyer_name }
end
def update_order_data
odata.update attributes.slice(*attributes_for_odata)
end
end

Ruby on rails associations parents

I would like to know, whatever the association is (simple belongs_to, polymorphic ...), when I make an association like :
class Toto < ActiveRecord::Base
belongs_to :test_one
belongs_to :test_two
end
class TestOne < ActiveRecord::Base
has_many :totos
end
class TestTwo < ActiveRecord::Base
has_many :totos
end
and then
test_one = TestOne.create
test_two = TestTwo.create
test1 = test_one.totos.create
test2 = test_two.totos.create
I would like to know into a callback of Toto what object instantiate me. In this case, it's obviously test_one and then test_two. I know I could check ids for example but the problem is when i do :
test3 = test_one.totos.create(test_two: test_two)
I can't know if test3 was created through test_one or test_two.
Thank you.
According to your example, I understand that you want to identify the type of object which is associated to your totos object (has_many :totos).
Since there are multiple different objects that might be associated to your totos object through the has_many and belongs_to associations, you might want to perform some kind of verification first to identify the type of the associated object.
First Answer:
This will only work if you know beforehand all the object types that has_many :totos
if test3.respond_to?(:test_one)
test = test3.test_one
elsif test3.respond_to?(:test_two)
test = test3.test_two
end
Second Answer:
I found this on Stackoverflow, and it somehow answeres your question. So if I rephrase the answer to:
def get_belongs_to(object)
associated = []
object.class.reflect_on_all_associations(:belongs_to).map do |reflection|
associated << object.try(reflection.name)
end
associated.compact
end
This method will return an array of all objects associated to your totos object. This will also work when totos belongs to multiple objects say test_one and test_two at the same time. So the following:
associated_objects = get_belongs_to(test3)
and in your case associated_objects[0] will yield the object you desire.
Hope this helps.
Rails does not persist the data you're looking for, so you'll have to store it yourself if you want it. This means you'll need a migration for the new field:
rails generate migration AddOriginalParentTypeToTotos original_parent_type:string
rake db:migrate
You can then override the assignment methods so that the first parent assigned will assign the original_parent_type attribute (and it will remain the same once assigned):
class Toto < ActiveRecord::Base
def test_one=(val)
self[:original_parent_type] ||= 'test_one'
super
end
def test_one_id=(val)
self[:original_parent_type] ||= 'test_one'
super
end
def test_two=(val)
self[:original_parent_type] ||= 'test_two'
super
end
def test_two_id=(val)
self[:original_parent_type] ||= 'test_two'
super
end
end
You can then use send to add an original_parent method:
class Toto < ActiveRecord::Base
def original_parent
send(original_parent_type) if original_parent_type
end
end

rails build association from the polymorphic belongs_to object

I have a polymorphic relation as follows:
class Profile
belongs_to :practice, polymorphic: :true
end
class ForeclosurePractice
has_one :profile, as: :practice
end
I want to build a practice object based on the profile I have, but unfortunately practice returns nil:
p = Profile.new
p.practice # => nil
How can I build the practice object from the Profile object?
p.build_practice won't work, because the build_other method is not generated for polymorphic associations.
If you want a way to dynamically create an instance, for example based on a class name selected in a form, you can try to use safe_constantize - simple example:
p.practice = params[:practice_type].safe_constantize.new
Building on Matt's answer I would adjust the build_practice method like this:
I ran into the issue that updating the polymorphic practice would create a new object instead. (Because build_practice always calls. new.
class Profile < ApplicationRecord
def build_practice(params)
if practice.present?
practice.assign_attributes(params)
else
self.practice = practice_type.classify.constantize.new(params)
end
end
First, I was only using the second part and would wonder that it creates a new record with every update.
You need to explicitly build the association:
p = Profile.new
p.build_practice
See: http://apidock.com/rails/v4.0.2/ActiveRecord/Associations/ClassMethods/belongs_to

How to dynamically set a belongs_to association in ActiveRecord?

I'm trying to create a mixin that allows an ActiveRecord model to act as a delegate for another model. So, doing it the typical way:
class Apple < ActiveRecord::Base
def foo_species
"Red delicious"
end
end
class AppleWrapper < ActiveRecord::Base
belongs_to :apple
# some meta delegation code here so that AppleWrapper
# has all the same interface as Apple
end
a = Apple.create
w = AppleWrapper.create
w.apple = a
w.foo_species
# => 'Red delicious'
What I want is to abstract this behavior into a Mixin, so that given a bunch of data models, I can create "Wrapper" classes that are also ActiveRecords, but that each wrapper corresponds to a specific class. Why? Each of the data models have calculations, aggregations with other models, and I want the "Wrapper" classes to contain fields (in the schema) that correspond to these calculations...so in effect. the Wrapper acts as a cached version of the original data model, with the same interface.
I will have to write out each Wrapper...so for Apple, Orange, Pear, there is a different Wrapper model for each of them. However, I just want to abstract out the wrapper behavior...so that there's a class level method that sets what the Wrapper points to, a la:
module WrapperMixin
extend ActiveSupport::Concern
module ClassMethods
def set_wrapped_class(klass)
# this sets the relation to another data model and does the meta delegation
end
end
end
class AppleWrapper < ActiveRecord::Base
include WrapperMixin
set_wrapped_class Apple
end
class OrangeWrapper < ActiveRecord::Base
include WrapperMixin
set_wrapped_class Orange
end
How would I set this up? And would this have to be a STI type relation? That is, does the Wrapper class have to have a wrapped_record_id and a wrapped_record_type?
You can use belongs_to in your set_wrapped_class method.
def set_wrapped_class(klass)
belongs_to klass.to_s.downcase.to_sym
end

Resources