I'm working with a fairly straightforward has_many through: situation where I can make the class_name/foreign_key parameters work in one direction but not the other. Perhaps you can help me out. (p.s. I'm using Rails 4 if that makes a diff):
English: A User manages many Listings through ListingManager, and a Listing is managed by many Users through ListingManager. Listing manager has some data fields, not germane to this question, so I edited them out in the below code
Here's the simple part which works:
class User < ActiveRecord::Base
has_many :listing_managers
has_many :listings, through: :listing_managers
end
class Listing < ActiveRecord::Base
has_many :listing_managers
has_many :managers, through: :listing_managers, class_name: "User", foreign_key: "manager_id"
end
class ListingManager < ActiveRecord::Base
belongs_to :listing
belongs_to :manager, class_name:"User"
attr_accessible :listing_id, :manager_id
end
as you can guess from above the ListingManager table looks like:
create_table "listing_managers", force: true do |t|
t.integer "listing_id"
t.integer "manager_id"
end
so the only non-simple here is that ListingManager uses manager_id rather than user_id
Anyway, the above works. I can call user.listings to get the Listings associated with the user, and I can call listing.managers to get the managers associated with the listing.
However (and here's the question), I decided it wasn't terribly meaningful to say user.listings since a user can also "own" rather than "manage" listings, what I really wanted was user.managed_listings so I tweaked user.rb to change
has_many :listings, through: :listing_managers
to
has_many :managed_listings, through: :listing_managers, class_name: "Listing", foreign_key: "listing_id"
This is an exact analogy to the code in listing.rb above, so I thought this should work right off. Instead my rspec test of this barfs by saying
ActiveRecord::HasManyThroughSourceAssociationNotFoundError:
Could not find the source association(s) :managed_listing or :managed_listings in model ListingManager. Try 'has_many :managed_listings, :through => :listing_managers, :source => <name>'. Is it one of :listing or :manager?
the test being:
it "manages many managed_listings" do
user = FactoryGirl.build(:user)
l1 = FactoryGirl.build(:listing)
l2 = FactoryGirl.build(:listing)
user.managed_listings << l1
user.managed_listings << l2
expect( #user.managed_listings.size ).to eq 2
end
Now, I'm convinced I know nothing. Yes, I guess I could do an alias, but I'm bothered that the same technique used in listing.rb doesn't seem to work in user.rb. Can you help explain?
UPDATE:
I updated the code to reflect #gregates suggestions, but I'm still running into a problem: I wrote an additional test which fails (and confirmed by "hand"-tesing in the Rails console). When one writes a test like this:
it "manages many managed_listings" do
l1 = FactoryGirl.create(:listing)
#user = User.last
ListingManager.destroy_all
#before_count = ListingManager.count
expect( #before_count ).to eq 0
lm = FactoryGirl.create(:listing_manager, manager_id: #user.id, listing_id: l1.id)
expect( #user.managed_listings.count ).to eq 1
end
The above fails. Rails generates the error PG::UndefinedColumn: ERROR: column listing_managers.user_id does not exist (It should be looking for 'listing_managers.manager_id'). So I think there's still an error on the User side of the association. In user.rb's has_many :managed_listings, through: :listing_managers, source: :listing, how does User know to use manager_id to get to its Listing(s) ?
The issue here is that in
has_many :managers, through: :listing_managers
ActiveRecord can infer that the name of the association on the join model (:listing_managers) because it has the same name as the has_many :through association you're defining. That is, both listings and listing_mangers have many managers.
But that's not the case in your other association. There, a listing_manager has_many :listings, but a user has_many :managed_listings. So ActiveRecord is unable to infer the name of the association on ListingManager that it should use.
This is what the :source option is for (see http://guides.rubyonrails.org/association_basics.html#has-many-association-reference). So the correct declaration would be:
has_many :managed_listings, through: :listing_managers, source: :listing
(p.s. you don't actually need the :foreign_key or :class_name options on the other has_many :through. You'd use those to define direct associations, and then all you need on a has_many :through is to point to the correct association on the :through model.)
I know this is an old question, but I just spent some time running into the same errors and finally figured it out. This is what I did:
class User < ActiveRecord::Base
has_many :listing_managers
has_many :managed_listings, through: :listing_managers, source: :listing
end
class Listing < ActiveRecord::Base
has_many :listing_managers
has_many :managers, through: :listing_managers, source: :user
end
class ListingManager < ActiveRecord::Base
belongs_to :listing
belongs_to :user
end
This is what the ListingManager join table looks like:
create_table :listing_managers do |t|
t.integer :listing_id
t.integer :user_id
end
Hope this helps future searchers.
i had several issues with my models, had to add the foreign key as well as the source and class name... this was the only workaround i found:
has_many :ticket_purchase_details, foreign_key: :ticket_purchase_id, source: :ticket_purchase_details, class_name: 'TicketPurchaseDetail'
has_many :details, through: :ticket_purchase_details, source: :ticket_purchase, class_name: 'TicketPurchaseDetail'
Related
So I have three models, User, Team, and Game. Currently constructed as such.
class Team < ApplicationRecord
has_and_belongs_to_many :users
has_many :home_games, class_name: 'Game', foreign_key: 'home_team_id'
has_many :away_games, class_name: 'Game', foreign_key: 'away_team_id'
has_many :wins, class_name: 'Game', foreign_key: 'winner_id'
belongs_to :owner, class_name: 'User'
end
class User < ApplicationRecord
has_and_belongs_to_many :teams
has_many :teams_owned, class_name: 'Team', foreign_key: 'owner_id'
has_many :games, through: :teams
end
class Game < ApplicationRecord
belongs_to :home_team, class_name: "Team"
belongs_to :away_team, class_name: "Team"
belongs_to :winner, class_name: "Team", optional: true
end
I want to add an association between users and games. So I can call User.games and Game.users.
I tried adding this:
#in user model
has_many :games, through: :teams
#in team model
has_many :games, ->(team) { where('home_team_id = ? or away_team_id = ?', team.id, team.id) }, class_name: 'Game'
As the api docs said to do. But, when I try to call this association, I get an error that "game.team_id does not exist". Since each game has a home_team_id and away_team_id, but no team_id.
Did I just implement this extremely poorly? Or am I missing something? Any help appreciated.
I would say this isn't a really good solution.
In ActiveRecord you can't actually define associations where the foreign key can potentially be in two different columns like this:
has_many :games, ->(team) { where('home_team_id = ? or away_team_id = ?', team.id, team.id) }, class_name: 'Game'
It definitely won't work as Rails will still join the assocation as JOIN games ON games.team_id = teams.id. Just adding a WHERE clause to the query won't fix that. Since ActiveRecord actually creates a variety of different queries there is no option to simply provide a different join.
A kludge to make this work would be to add an instance method:
class Game < ApplicationRecord
def users
User.joins(:teams)
.where(teams: { id: home_team.id })
.or(Team.where(id: away_team.id))
end
end
As its not an actual association you cant join through it or use an sort of eager loading to avoid n+1 queries.
If you actually want to create a single association that you can join through you would need to add a join table between games and teams.
class Team < ApplicationRecord
# ...
has_many :game_teams
has_many :games, through: :game_teams
end
# rails g model game_team team:belongs_to game:belongs_to score:integer
class GameTeam < ApplicationRecord
belongs_to :team
belongs_to :game
end
class Game < ApplicationRecord
has_many :game_teams
has_many :teams, through: :game_teams
has_many :users, through: :teams
end
This is a better idea since it gives you a logical place to record the score per team.
As an aside if the composition of teams can change and accurate record keeping is important you might actually need additional join tables as the lineup when a game is played may not actually match the current lineup.
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.
I'm looking to make sure my methods are correct before pursuing this association. The implementation sounds too complicated, so I think there must be something wrong with my plan. I am using structured (SQL) data storage, conforming to the rails storage conventions. What I have is a User model, it has an email address, password_digest, and name in the Schema.
class User < ActiveRecord::Base
has_many :posts
end
I'd like to implement a has_many association to a friends collection, so that Users can belong_to Users (as friends). I'm hoping to be able to have User.last.friends.last return a User object when properly built and populated.
I believe I can create a model for this association like:
Class Friend < ActiveRecord::Base
belongs_to :user
belongs_to :friendlies, class: 'User'
end
Class User < ActiveRecord::Base
has_many :posts
has_many :friends
has_many :friendly, class: 'Friend'
end
But I think that will require me to add an as to the models and query using User.last.friends.last.user So what I was thinking is this is a has_and_belongs_to_many relationship. Can I get away with the following (or something similar):
class User < ActiveRecord::Base
has_and_belongs_to_many :friends, class: 'User'
end
I found this:
class User < ActiveRecord::Base
has_many :user_friendships
has_many :friends, through: :user_friendships
class UserFriendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, class_name: 'User', foreign_key: 'friend_id'
And this(self proclaimed 'standard'):
has_many :friendships, :dependent => :destroy
has_many :friends, :through => :friendships, :dependent => :destroy
has_many :inverse_friendships, :class_name => "Friendship", :foreign_key => "friend_id", :dependent => :destroy
has_many :inverse_friends, :through => :inverse_friendships, :source => :user, :dependent => :destroy
Which I assume requires a Friendship model. I don't feel like I need a friendship model. I think that the class UserFriendship method looks right, but it requires an additional model. Now onto the questions:
Can I do a has_and_belongs_to_many relationship with a table that relates user to friends which are users, without incurring an additional model to do so?
Is it prudent to have an additional model 'in case' additional requirements pop up later?
What you're looking for is something called a self referential join, which means that you can reference the same model with a different association:
#app/models/user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :friendships,
class_name: "User",
join_table: :friendships,
foreign_key: :user_id,
association_foreign_key: :friend_user_id
end
You'd have to create a habtm join table to have a reference:
$ rails g migration CreateFriendshipsTable
#db/migrate/create_friendships_table____.rb
class CreateFriendShipsTable < ActiveRecord::Migration
def change
create_table :friendships, id: false do |t|
t.integer :user_id
t.integer :friend_user_id
end
add_index(:friendships, [:user_id, :friend_user_id], :unique => true)
add_index(:friendships, [:friend_user_id, :user_id], :unique => true)
end
end
$ rake db:migrate
There is a great reference here: Rails: self join scheme with has_and_belongs_to_many?
Is it prudent to have an additional model 'in case' additional requirements pop up later?
Only if you know you're going to have extra attributes. If not, you can get away with the habtm for as long as you want.
You can try this
--friendship.rb
class Friendship < ApplicationRecord
belongs_to :user
belongs_to :friend, :class_name => 'User'
end
--user.rb
class User < ApplicationRecord
has_many :friendships
has_many :friends, through: :friendships
end
--db migrate xxxx_create_friendship.rb
class CreateFriendships < ActiveRecord::Migration[5.0]
def change
create_table :friendships do |t|
t.belongs_to :user
t.belongs_to :friend, class: 'User'
t.timestamps
end
end
end
Rich's answer was an excellent resource. I managed to subvert a few steps by following this convention:
# app/model/user.rb
has_and_belongs_to_many :friends,
class_name: "User",
join_table: :friends_users,
foreign_key: :user_id,
association_foreign_key: :friend_id
This migration was sufficient (without modifying the resulting code):
rails g migration CreateFriendlyJoinTable users friends
The JoinTable syntax was found using rails generate migration -h
I'm not sure how to create this, I'd like to create a many-to-many polymorphic association.
I have a question model, which belongs to a company.
Now the question can has_many users, groups, or company. Depending on how you assign it.
I'd like to be able to assign the question to one / several users, or one / several groups, or the company it belongs to.
How do I go about setting this up?
In this case I would add a Assignment model which acts as an intersection between questions and the entities which are assigned to it.
Create the table
Lets run a generator to create the needed files:
rails g model assignment question:belongs_to assignee_id:integer assignee_type:string
Then let's open up the created migration file (db/migrations/...__create_assignments.rb):
class CreateAssignments < ActiveRecord::Migration
def change
create_table :assignments do |t|
t.integer :assignee_id
t.string :assignee_type
t.belongs_to :question, index: true, foreign_key: true
t.index [:assignee_id, :assignee_type]
t.timestamps null: false
end
end
end
If you're paying attention here you can see that we add a foreign key for question_id but not assignee_id. That's because the database does not know which table assignee_id points to and cannot enforce referential integrity*. We also add a compound index for [:assignee_id, :assignee_type] as they always will be queried together.
Setting up the relationship
class Assignment < ActiveRecord::Base
belongs_to :question
belongs_to :assignee, polymorphic: true
end
The polymorpic: true option tells ActiveRecord to look at the assignee_type column to decide which table to load assignee from.
class User < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
end
class Group < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
end
class Company < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
end
Unfortunately one of the caveats of polymorphic relationships is that you cannot eager load the polymorphic assignee relationship. Or declare a has_many :assignees, though: :assignments.
One workaround is:
class Group < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
def assignees
assignments.map(&:assignee)
end
end
But this can result in very inefficient SQL queries since each assignee will be loaded in a query!
Instead you can do something like this:
class Question < ActiveRecord::Base
has_many :assignments
# creates a relationship for each assignee type
['Company', 'Group', 'User'].each do |type|
has_many "#{type.downcase}_assignees".to_sym,
through: :assignments,
source: :assignee,
source_type: type
end
def assignees
(company_assignees + group_assignees + user_assignees)
end
end
Which will only cause one query per assignee type which is a big improvement.
My schema has Articles and Journals that can be tagged with Tags. This requires a has_many through: association with a polymorphic relationship to my Tagging join table.
Okay, that's the easy and well documented part.
My problem is that Articles can have both primary-tags and sub-tags. The primary tags are what I'm most interested in, but my model also needs to keep track of these sub tags. Sub tags are simply labels describing the Article that are of lesser importance, but come from the same global pool of Tags. (in fact, one Article's primary-tag may be another's sub-tag).
Achieving this requires the Article model to have two associations to the Tagging model and two has_many through: associations to Tags (i.e. #tags & #sub-tags)
This is what I have so far, which while valid does not keep primary-tags and sub-tags separate.
class Article < ActiveRecord::Base
has_many :taggings, as: :taggable
has_many :tags, through: :taggings
has_many :sub_taggings, as: :taggable, class_name: 'Tagging',
source_type: 'article_sub'
has_many :sub_tags, through: :sub_taggings, class_name: 'Tag', source: :tag
end
class Tagging < ActiveRecord::Base
# id :integer
# taggable_id :integer
# taggable_type :string(255)
# tag_id :integer
belongs_to :tag
belongs_to :taggable, :polymorphic => true
end
class Tag < ActiveRecord::Base
has_many :taggings
end
I know that somewhere in there I need to find the right combination of source and source_type but I can't work it out.
For completeness here is my article_spec.rb that I'm using to test this — currently failing on "the incorrect tags".
describe "referencing tags" do
before do
#article.tags << Tag.find_or_create_by_name("test")
#article.tags << Tag.find_or_create_by_name("abd")
#article.sub_tags << Tag.find_or_create_by_name("test2")
#article.sub_tags << Tag.find_or_create_by_name("abd")
end
describe "the correct tags" do
its(:tags) { should include Tag.find_by_name("test") }
its(:tags) { should include Tag.find_by_name("abd") }
its(:sub_tags) { should include Tag.find_by_name("abd") }
its(:sub_tags) { should include Tag.find_by_name("test2") }
end
describe "the incorrect tags" do
its(:tags) { should_not include Tag.find_by_name("test2") }
its(:sub_tags) { should_not include Tag.find_by_name("test") }
end
end
Thanks in advance for any help on achieving this. The main problem is I cannot work out how to tell Rails the source_type to use for the sub_tags association in Articles.
Hmmm... Silence again...? What gives SO? Hello...? Bueller?
Never fear, here's the answer:
After looking into Single Table Inheritance (not the answer, but an interesting technique for other slightly-related problems), I stumbled across a SO question regarding multiple-references to a polymorphic association on the same model. (Thank you hakunin for your detailed answer, +1.)
Basically we need to explicitly define the contents of the taggable_type column in the Taggings table for the sub_taggings association, but not with source or source_type, instead with :conditions.
The Article model shown below now passes all the tests:
class Article < ActiveRecord::Base
has_many :taggings, as: :taggable
has_many :tags, through: :taggings, uniq: true, dependent: :destroy
has_many :sub_taggings, as: :taggable, class_name: 'Tagging',
conditions: {taggable_type: 'article_sub_tag'},
dependent: :destroy
has_many :sub_tags, through: :sub_taggings, class_name: 'Tag',
source: :tag, uniq: true
end
UPDATE:
This is the correct Tag model that produces functional reverse polymorphic associations on Tags. The reverse association (ie. Tag.articles and Tag.sub_tagged_articles) passes tests.
class Tag < ActiveRecord::Base
has_many :articles, through: :taggings, source: :taggable,
source_type: "Article"
has_many :sub_tagged_articles, through: :taggings, source: :taggable,
source_type: "Article_sub_tag", class_name: "Article"
end
I have also extended and successfully tested the schema to allow tagging & sub_tagging of other models using the same Tag model and Tagging join table. Hope this helps someone.