Ruby on rails associations parents - ruby-on-rails

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

Related

Rails setter method that updates two other fields?

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

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

superclass mismatch for class User - inheriting from ActiveRecord::Base

I am trying to figure out my superclass mismatch error. All the posts I've read about this describe the problem as being that User is defined twice as a class in my application.
In my case, it isn't defined twice. I have a services folder and within that I have a user folder (for user service classes). In that user folder, I have a file called organisation_mapper_service.rb, with:
class User < ActiveRecord::Base
class OrganisationMapperService
def self.call(user: u)
new(user: user).call
end
def initialize(user: u)
self.user = user
end
def call
if matching_organisation.present?
# user.organisation_request.new(organisation_id: matching_organisation.id)
# user.update_attributes!(organisation_id: matching_organisation.id)
else
#SystemMailer.unmatched_organisation(user: user).deliver_now
end
end
private
attr_accessor :user
def matching_organisation
User::OrganisationMapperService.new(user).matching_organisation
end
end
end
Separate to that, I have my user model which defines user as:
class User < ApplicationRecord
I thought it should be fine to define the service class in the way I have because it inherits from ActiveRecord::Base rather than ApplicationRecord.
Can anyone see what I've done wrong here? Where else could I look for a second definition of User?
TAKING SERGIO'S SUGGESTION
I change the user organisation mapper service to open as follows:
class User::OrganisationMapperService < ActiveRecord::Base
But that then gives an error with my Users::OrgRequestsController which has new defined as follows:
def new
#all_organisations = Organisation.select(:title, :id).map { |org| [org.title, org.id] }
#org_request = OrgRequest.new#form(OrganisationRequest::Create)
matched_organisation = User::OrganisationMapperService.new(current_user).matching_organisation
#org_request.organisation_id = matched_organisation.try(:id)
end
the error message then says:
PG::UndefinedTable at /users/4/org_requests/new
ERROR: relation "user_organisation_mapper_services" does not exist
LINE 8: WHERE a.attrelid = '"user_organisation_mapper...
**TAKING SERGIO'S SUGGESTION (exactly) **
I change my service class to:
class User::OrganisationMapperService
But then I get an error that says:
wrong number of arguments (given 1, expected 0)
That error highlights this line of my service class:
def initialize(user: u)
self.user = user
end
I don't know what to do about that because I clearly have a user if there is an inheritance from user.
Even once you solve all your other issues, you actually have an infinite recursion going on.
User::OrganisationMapperService.call(user: User.first)
Is equivalent to calling:
User::OrganisationMapperService.new(user: User.first).call
Which internally calls matching_organisation, so is sort of equivalent to:
User::OrganisationMapperService.new(user: User.first).matching_organisation
Meanwhile, matching_organisation calls
User::OrganisationMapperService.new(user).matching_organisation
It's just going to go round and round in circles.
The only reason it doesn't is because of the wrong number of arguments (given 1, expected 0) error. This is because it should be User::OrganisationMapperService.new(user: user) rather than User::OrganisationMapperService.new(user) in your matching_organisation method.
Update in response to comment:
From what I understand, the User::OrganisationMapperService is a service class that does the job of finding some Organisation and then performing some sort of work.
The User::OrganisationMapperService#matching_organisation method should actually contain the code that returns the matching organisation for the given user. The implementation will completely depend on how you have structured your database, but I'll give a couple of examples to put you on the right track or give you ideas.
First, Your organisations table may have a user_id column. In this case you could do a simple query on the Organisation model and perform a search using the user's id:
class User::OrganisationMapperService
def matching_organisation
# find the organisation and cache the result
#matching_organisation ||= ::Organisation.where(user_id: user).first
end
end
Alternatively, you may have some sort of join table where there may be multiple Users at an Organisation (just for this example let us call this table 'employments'):
class Employment < ApplicationRecord
belongs_to :user
belongs_to :organisation
end
We can add scopes (this is a must read) to the Organisation model to assist with the query:
class Organisation < ApplicationRecord
has_many :employments
has_many :users, through: :employments
scope :for_user, ->(user) {
# return organisations belonging to this user
joins(:users).merge( Employment.where(user_id: user) )
}
end
Then finally, the OrganisationMapperService#matching_organisation method becomes:
class User::OrganisationMapperService
def matching_organisation
# find the organisation and cache the result
#matching_organisation ||= ::Organisation.for_user(user).first
end
end
You are defining User class with two separate parent classes. Don't do that.
It should be
class User::OrganisationMapperService
This way, your existing User class will be loaded and used, rather than a new one created.
I thought it should be fine to define the service class in the way I have because it inherits from ActiveRecord::Base rather than ApplicationRecord.
The service class in your example doesn't inherit from anything.

Rails 4 Create Associated Object on Save

How can I create multiple associated objects automatically just after I save a new primary object?
For example
In Rails 4, I have three objects: Businesses, Budgets, and Categories.
#app/models/business.rb
class Business < ActiveRecord::Base
#attrs id, name
has_many :budgets
end
#app/models/budget.rb
class Budget < ActiveRecord::Base
#attrs id, business_id, department_id, value
belongs_to :business
belongs_to :category
end
#app/models/category.rb
class Category < ActiveRecord::Base
#attrs id, name
has_many :budgets
end
When I create a new Business, after saving the new Business, I would like to atomically create a Budget for each Category and give it value of $0. This way, when I go to show or edit a new Business, it will already have the associated Categories and Budgets, which can then be edited. Thus, upon creating a new Business, multiple new Budgets will be created, one for each Category, each with the value of 0.
I read this article: Rails 3, how add a associated record after creating a primary record (Books, Auto Add BookCharacter)
And I am wondering if I should use the after_create callback in the Business model and have the logic then exist in the Budgets controller (not exactly sure how to do this) or if I should add logic to the businesses_controller.rb in the 'new' call with something similar to:
#business = Business.new
#categories = Category.all
#categories.each do |category|
category.budget.build(:value => "0", :business_id => #business.id)
end
In my experience, it's best to avoid using callbacks unless it relates to a given model's persistence. In this case, letting a budget set it's own default value when one isn't supplied is good use of a callback. That also removes some complexity from your logic.
class Budget
before_validate :set_value
...
private
def set_value
self.value ||= 0
end
end
For the rest, I would create custom classes, each with a single responsibility, to systematically generate a new business. Here's an example. Keep in mind that this is not meant to be copy and pasted, it's just to illustrate a concept:
class BusinessGenerator < Struct.new(:business_params)
attr_reader :business
def generate
create_business
create_budgets
end
private
def create_business
#business = Business.create!(business_params)
end
def create_budgets
BudgetGenerator.new(#business).create
end
end
class BudgetGenerator < Struct.new(:business)
def generate
categories.each do |c|
business.budgets.create!(category: c)
end
end
private
def categories
Category.all
end
end
This is nice because it separates concerns and is easily extensible, testable and doesn't use Rails magic like accepts_nested_attributes_for. For example, if in the future you decide that not all businesses need a budget in every category, you can easily pass the ones you want as an argument to BudgetGenerator.
You'll instantiate the BusinessGenerator class in the controller:
class BusinessController < ActionController::Base
...
def create
generator = BusinessGenerator.new(business_params)
if generator.generate
flash[:success] = "Yay"
redirect_to generator.business
else
render :new
end
end
...
end
Some sticking points you might have with this approach include:
Returning validation errors to your business form
If the creation of a budget fails, you're stuck with a budget-less business. You can't wait to save business until after the budgets are created because there is no id to associate. Perhaps consider putting a transaction inside of the generator method.
Regardless of Brent Eicher's great advice, I've never experienced anything bad from using callbacks. If you don't mind using them, you could do the following (if you're setting the budget at 0 each time):
#app/models/business.rb
class Business < ActiveRecord::Base
before_create :build_budgets
private
def build_budgets
Category.all.each do |category|
self.budgets.build(category: category, value: "0")
end
end
end
--
Also, you need to make sure your budget foreign keys are correct.
I see you have department_id when Budget belongs_to Category. You should make this category_id or define the foreign_key:
#app/models/budget.rb
class Budget < ActiveRecord::Base
belongs_to :category, foreign_key: "department_id"
end
I ended up adding the logic to the create method in the Business controller to loop through all Categories and create a budget just after save. Note that I was lazy and didn't put in any error handling. :
def create
#business = Business.new(params[:business])
#results = #business.save
#categories = Categories.all
#categories.each do |category|
category.budgets.create(:amount => "0", :business_id => #business.id)
end
respond_to do |format|
...
end
end

Resources