"has_many :through" association through a polymorphic association with STI - ruby-on-rails

I have two models that use the people table: Person and Person::Employee (which inherits from Person). The people table has a type column.
There is another model, Group, which has a polymorphic association called :owner. The groups table has both an owner_id column and an owner_type column.
app/models/person.rb:
class Person < ActiveRecord::Base
has_one :group, as: :owner
end
app/models/person/employee.rb:
class Person::Employee < Person
end
app/models/group.rb:
class Group < ActiveRecord::Base
belongs_to :owner, polymorphic: true
belongs_to :supervisor
end
The problem is that when I create a Person::Employee with the following code, the owner_type column is set to an incorrect value:
group = Group.create
=> #<Group id: 1, owner_id: nil, owner_type: nil ... >
group.update owner: Person::Employee.create
=> true
group
=> #<Group id: 1, owner_id: 1, owner_type: "Person" ... >
owner_type should be set to "Person::Employee", but instead it is set to "Person".
Strangely, this doesn't seem to cause any problems when calling Group#owner, but it does cause issues when creating an association like the following:
app/models/supervisor.rb:
class Supervisor < ActiveRecord::Base
has_many :groups
has_many :employees, through: :groups, source: :owner,
source_type: 'Person::Employee'
end
With this type of association, calling Supervisor#employees will yield no results because it is querying for WHERE "groups"."owner_type" = 'People::Employees' but owner_type is set to 'People'.
Why is this field getting set incorrectly and what can be done about it?
Edit:
According to this, the owner_type field is not getting set incorrectly, but it is working as designed and setting the field to the name of the base STI model.
The problem appears to be that the has_many :through association searches for Groups with a owner_type set to the model's own name, instead of the base model's name.
What is the best way to set up a has_many :employees, through: :group association that correctly queries for Person::Employee entries?

You're using Rails 4, so you can set a scope on your association. Your Supervisor class could look like:
class Supervisor < ActiveRecord::Base
has_many :groups
has_many :employees, lambda {
where(type: 'Person::Employee')
}, through: :groups, source: :owner, source_type: 'Person'
end
Then you can ask for a supervisor's employees like supervisor.employees, which generates a query like:
SELECT "people".* FROM "people" INNER JOIN "groups" ON "people"."id" =
"groups"."owner_id" WHERE "people"."type" = 'Person::Employee' AND
"groups"."supervisor_id" = ? AND "groups"."owner_type" = 'Person'
[["supervisor_id", 1]]
This lets you use the standard association helpers (e.g., build) and is slightly more straightforward than your edit 2.

I did came up with this workaround, which adds a callback to set the correct value for owner_type:
class Group < ActiveRecord::Base
belongs_to :owner, polymorphic: true
before_validation :copy_owner_type
private
def copy_owner_type
self.owner_type = owner.type if owner
end
end
But, I don't know if this is the best and/or most elegant solution.
Edit:
After finding out that the owner_type field is supposed to be set to the base STI model, I came up with the following method to query for Person::Employee entries through the Group model:
class Supervisor < ActiveRecord::Base
has_many :groups
def employees
Person::Employee.joins(:groups).where(
'people.type' => 'Person::Employee',
'groups.supervisor_id' => id
)
end
end
However, this does not seem to cache its results.
Edit 2:
I came up with an even more elegant solution involving setting the has_many :through association to associate with the base model and then creating a scope to query only the inherited model.
class Person < ActiveRecord::Base
scope :employees, -> { where type: 'Person::Employee' }
end
class Supervisor < ActiveRecord::Base
has_many :groups
has_many :people, through: :groups, source: :owner, source_type: 'Person'
end
With this I can call Supervisor.take.people.employees.

Related

has many relationship for polymorphic association

class Sample
has_many :pictures
end
class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
belongs_to :sample
end
class Employee < ApplicationRecord
has_many :pictures, as: :imageable
end
class Product < ApplicationRecord
has_many :pictures, as: :imageable
end
What should be the association to get all product or employee of a given sample.
Sample.first.pictures.map(&:imageable). I want to get it as an activerecord association.
Workaround:
class Sample
has_many :pictures
has_many :imageable_employees, through: :pictures, source: :imageable, source_type: 'Employee'
has_many :imageable_products, through: :pictures, source: :imageable, source_type: 'Product'
end
Usage:
sample = Sample.first
employees = sample.imageable_employees
products = sample.imageable_products
...see docs
Explanation:
Sample.first.pictures.map(&:imageable). I want to get it as an activerecord association.
... is I don't think it's possible, but you can still get them all as an Array instead. The reason is that there is no table (model) that corresponds to the imageable association, but that it corresponds to ANY model instead, which complicates the SQL query, and thus I don't think it's possible.
As an example, consider the following query:
imageables_created_until_yesterday = Sample.first.something_that_returns_all_imageables.where('created_at < ?', Time.zone.now.beginning_of_day)
# what SQL from above should this generate? (without prior knowledge of what tables that the polymorphic association corresponds to)
# => SELECT "WHAT_TABLE".* FROM "WHAT_TABLE" WHERE (sample_id = 1 AND created_at < '2018-08-27 00:00:00.000000')
# furthermore, you'll notice that the SQL above only assumes one table, what if the polymorphic association can be at least two models / tables?
Alternative Solution:
Depending on the needs of your application and the "queries" that you are trying to do, you may or may not consider the following which implements an abstract_imageable (a real table) model for you to be able to perform queries on. You may also add more attributes here in this abstract_imageable model that you think are "shared" across all "imageable" records.
Feel free to rename abstract_imageable
class Sample
has_many :pictures
has_many :abstract_imageables, through: :pictures
end
class Picture
belongs_to :sample
has_many :abstract_imageables
end
# rails generate model abstract_imageable picture:belongs_to imageable:references{polymorphic}
class AbstractImageable
belongs_to :picture
belongs_to :imageable, polymorphic: true
end
class Employee < ApplicationRecord
has_many :abstract_imageables, as: :imageable
has_many :pictures, through: :abstract_imageables
end
class Product < ApplicationRecord
has_many :abstract_imageables, as: :imageable
has_many :pictures, through: :abstract_imageables
end
Usage:
sample = Sample.first
abstract_imageables = sample.abstract_imageables
puts abstract_imageables.first.class
# => AbstractImageable
puts abstract_imageables.first.imageable.class
# => can be either nil, or Employee, or Product, or whatever model
puts abstract_imageables.second.imageable.class
# => can be either nil, or Employee, or Product, or whatever model
# your query here, which I assumed you were trying to do because you said you wanted an `ActiveRecord::Relation` object
abstract_imageables.where(...)

Making self-referencing models

All,
I'm still working my way around learning Rails and I'm not having much luck finding the relevant answers I need; that said, I suspect it's something that's been done before since I've done this by hand many times in the past myself.
I have a table called tab_accounts with account_id(int) as PK. I also have a lookup table called mtom_account_relations. This table has two int columns (account_subject, account_associate), both of which are FK to tab_accounts.account_id. The purpose of this layout is to allow for a many-to-many relationship between tab_account entries. The goal is to create an endpoint that returns an account's details along with a list of its associates, also accounts.
At this point I have the following:
models/account_relation.rb:
class AccountRelation < ApplicationRecord
self.table_name = "mtom_account_relations"
belongs_to :subject, foreign_key: "account_id", class_name: "Account"
belongs_to :associate, foreign_key: "account_id", class_name: "Account"
end
models/account.rb
class Account < ApplicationRecord
self.table_name = "tab_accounts"
self.primary_key = "account_id"
...
has_many :account_relations
has_many :associates, :through => :account_relations
has_many :subjects, :through => :account_relations
end
controllers/account_controller.rb
class AccountsController < ApplicationController
...
def associates
_account_id = params[:account_id]
#rs_account = Account
.select("tab_accounts.account_id, tab_accounts.screen_name, tab_accounts.friends, tab_accounts.followers")
.where(:tab_accounts => {account_id: _account_id})
.as_json[0]
#rs_account['associates'] = Account.select("tab_accounts.account_id, tab_accounts.screen_name")
.joins(:subjects)
.where(:tab_accounts => {account_id: _account_id})
.as_json
render json: #rs_account
end
end
config/routes.rb:
Rails.application.routes.draw do
...
get 'accounts/associates/:account_id', :to => "accounts#associates"
end
When I run the method I get the following error:
PG::UndefinedColumn: ERROR: column mtom_account_relations.account_id does not exist LINE 1: ..._accounts" INNER JOIN "mtom_account_relations" ON "mtom_acco... ^ : SELECT tab_accounts.account_id, tab_accounts.screen_name FROM "tab_accounts" INNER JOIN "mtom_account_relations" ON "mtom_account_relations"."account_id" = "tab_accounts"."account_id" INNER JOIN "tab_accounts" "subjects_tab_accounts" ON "subjects_tab_accounts"."account_id" = "mtom_account_relations"."account_id" WHERE "tab_accounts"."account_id" = $1
I suspect the call to the non-existent table "subjects_tab_accounts" is being created from my .joins(:subjects) clause in the controller.
It thinks there's a "mtom_account_relations"."account_id" column.
I'd be grateful for any actionable assistance. Thank you for your attention.
Joe
Consider this example of a family tree. Since Person can be either in the parent_id or child_id column setting up has_many :relationships relation won't work since we don't know which column to use in the join.
Instead we need to setup separate relationships depending on which foreign key on Relationship we joining and then query through this relationship
class Person
has_many :relationships_as_child,
class_name: 'Relationship'
foreign_key: 'child_id'
has_many :relationships_as_parent,
class_name: 'Relationship'
foreign_key: 'parent_id'
has_many :parents,
through: :relationships_as_child,
source: :parent
has_many :children,
through: :relationships_as_child,
source: :child
end
class Relationship
belongs_to :parent, class_name: 'Person'
belongs_to :child, class_name: 'Person'
end
class AccountRelation < ApplicationRecord
self.table_name = "mtom_account_relations"
belongs_to :subject,
foreign_key: "account_id",
class_name: "Account"
belongs_to :associate, foreign_key: "account_id",
class_name: "Account"
end
class Account < ApplicationRecord
self.table_name = "tab_accounts"
self.primary_key = "account_id"
...
has_many :account_relations_as_subject,
class_name: 'AccountRelation',
foreign_key: 'subject_id'
has_many :account_relations_as_associate,
class_name: 'AccountRelation',
foreign_key: 'associate_id'
has_many :associates,
through: :account_relations_as_subject,
source: :associate
has_many :subjects,
through: :account_relations_as_associate,
source: :subject
end
When learning Rails I would really encourage you to learn to love the conventions when it comes to naming tables, primary keys and columns in general. Don't make it harder on yourself.
When it comes to the controller I would set it up like so:
#routes.rb
resources :accounts do
resources :associates, only: [:index]
end
class AssociatesController
# GET /accounts/:account_id/associates
def index
#account = Account.joins(:associates).find(params[:account_id])
#associates = #account.associates
end
end
This models associates as a RESTful resource which is nested under an account and gives you straight forward and conventional way to add more CRUD actions.

ActiveRecord has_many through a scoped association with arg

I've seen a number of answers to questions that address how to use scope blocks in ActiveRecord associations that include passing the object itself into the block like ...
class Patron < ActiveRecord::Base
has_many :bars,
->(patron) { baz: patron.blah },
foreign_key: :somekey,
primary_key: :somekey
end
class Bar < ActiveRecord::Base
belongs_to :patron,
->(bar) { blah: bar.baz },
foreign_key: :somekey,
primary_key: :somekey
end
The usage of the explicit PK and FK here is due to the legacy relationship between the underlying tables. There are many hundreds of millions of "patrons" in the production system.
As a point of clarification re #TJR - the relationship between Patron and Bar is actual a compound foreign key on the fields :somekey and :baz (or :blah in the reverse direction). ActiveRecord's :foreign_key option doesn't allow arrays.
I've discovered that unfortunately this prevents subsequent has_many :throughs from working as expected.
class Patron < ActiveRecord::Base
has_many :bars,
->(patron) { baz: patron.blah },
foreign_key: :somekey,
primary_key: :somekey
has_many :drinks, through: :bars
end
Using the through association produces errors like ...
ArgumentError:
wrong number of arguments (0 for 1)
The association between bar and drinks is a classic has_many with the standard foreign and primary key (bar_id, id).
I have come up with some ugly work arounds that allow me to accomplish the desired functionality but the whole thing smells terrible. So far the best of these look like
class Patron < ActiveRecord::Base
has_many :bars,
->(patron) { baz: patron.blah },
foreign_key: :somekey,
primary_key: :somekey
def drinks
bars.drinks
end
end
I've received the existing Bar class and it consists of many hundreds of millions of records, as I previously mentioned, making a database side change difficult.
Some posts seemed to suggest a dynamic string evaluation inside the proc to avoid having to pass in the current patron object - but as mentioned in other posts, this doesn't work.
Please advise me on what I might do to get the has_many through relationship working.
I just have tried this kind of associations in Rails 4.2. It works pretty well:
class Patron < ActiveRecord::Base
has_many :bars,
->(patron) { where(baz: patron.blah) },
foreign_key: :patron_id,
primary_key: :id
has_many :drinks, through: :bars
end
class Bar < ActiveRecord::Base
belongs_to :patron,
->(bar) { where(blah: bar.baz) },
foreign_key: :patron_id,
primary_key: :id
has_many :drinks
end
class Drink < ActiveRecord::Base
end
Check associations:
> p1 = Patron.first
> p1.drinks
Drink Load (0.8ms) SELECT "drinks".* FROM "drinks" INNER JOIN "bars" ON "drinks"."bar_id" = "bars"."id" WHERE "bars"."patron_id" = 1 AND "bars"."baz" = 1 [["patron_id", 1], ["baz", 1]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Drink id: 3, name: "drink 3", bar_id: 2, created_at: "2017-04-07 03:30:06", updated_at: "2017-04-07 03:30:06">]>

How to set a 'has many :through' a polymorphic association

I have defined my models as follows. I am trying to do #user.orders using has_many. I have defined a method:orders to show the behaviour I want.
class Location < ActiveRecord::Base
belongs_to :locationable, polymorphic: true
has_many :orders, ->(location) { where(location.locationable_type == 'User') }, class_name: 'Order', foreign_key: :drop_location_id
# but this does not check for type
# it produces the SQL: SELECT "orders".* FROM "orders" WHERE "orders"."drop_location_id" = $1
end
class Order < ActiveRecord::Base
# location is polymorphic, so type of location can be merchant/user
belongs_to :pickup_location, class_name: 'Location'
belongs_to :drop_location, class_name: 'Location'
end
class User < ActiveRecord::Base
has_many :locations, as: :locationable
# TODO: make it a has_many :orders instead
def orders
Order.where(drop_location: locations)
end
end
Using a method doesn't feel like the rails way. Moreover, I want it to work well with rails_admin.
By now, you should have received an error indicating that you can't use has_many through a polymorphic association. If you think about it, it makes perfect sense, how could your ORM (ActiveRecord) formulate the query as the joins would be horrendous.

How to model schema with Ruby on Rails?

I have two models, Group and User. A user can create many groups and is then the owner of those groups. Other users can join those groups and therefore become members of those groups.
My question now is how to model this with Ruby on Rails.
I have created a join table between Group and Users and I added a reference from Group to the owner (foreign id to user).
Note: not sure if I did all this correctly.
Here are the attributes I currently have in Group and User (I don't know how to access the join table, so I can't show it to you).
Group: id, topic, home_town, created_at, updated_at, user_id
user_id was added with the following migration and I want it to reference the owner:
class AddOwnerReferenceToGroup < ActiveRecord::Migration
def change
add_reference :groups, :user, index: true
end
end
User: id, name, email, password_digest, remember_token, created_at, updated_at
As for the relationships, here's what each class contains:
User -> has_many :groups
Group -> has_many :users
Is it possible (and do I have) to add a belongs_to relationship in the group class to reference to the owner?
I would set it up like so:
class User < ActiveRecord::Base
has_many :group_users, :dependent => :destroy
has_many :groups, :through => :group_users
has_many :owned_groups, :through => :group_users, :class_name => "Group", :conditions => ["group_users.owner = ?", true]
...
end
class Group < ActiveRecord::Base
has_many :group_users, :dependent => :destroy
has_many :users, :through => :group_users
def owner
self.users.find(:first, :conditions => ["group_users.owner = ?", true])
end
def owner=(user)
if gu = self.group_users.find_by_user_id(user.id)
gu.update_attributes(:owner => true)
else
self.group_users.create(:user_id => user, :owner => true)
end
end
...
end
#group_users table has fields user_id, group_id, owner(bool)
class GroupUser < ActiveRecord::Base
belongs_to :group
belongs_to :user
after_save :enforce_single_owner
def enforce_single_owner
if self.changes["owner"] && self.owner
GroupUser.find(:all, :conditions => ["group_id = ? and id <> ? and owner = ?", self.group_id, self.id, true]).each{|gu| gu.update_attributes(:owner => false)
end
end
...
end
In this schema, the join table model has responsibility for tracking which of the members of the group is the owner of the group. A group has many users, and one of those wil be the owner.
Yes, it is possible and you should.
By adding to your Group
belongs_to :owner, class_name: 'User'
you can model your requirement that a group is created by one of the users.
In my snippet I used an arbitrary name in belongs_to just to make the code more readable. You don't have to but it definitely clears up things. The downside is that ActiveRecord cannot guess to which class you refer when using custom association names, thus, you need to manually specify it.
In a similar fashion you could change has_many :users to
has_many :members, class_name: 'User'
Whatever feels more natural when you read your code.

Resources