asing
asing

Reputation: 395

Joining a "has many through" association using ActiveRecord

Working with Exported Data from API

I'm building a leaderboard that displays the Team.name of each team as well as the users who have picked that particular team as their favorites. I'm also populating another attribute Favorite.points; to display the users with the most points accumulated for that respective team.

Here are the models I'm working with:

Favorite.rb

class Favorite < ActiveRecord::Base
  belongs_to :users
  belongs_to :teams
end

Team.rb

class Team < ActiveRecord::Base
  has_many :favorites
  has_many :users, :through => :favorites 
end

User.rb

class User < ActiveRecord::Base
  has_many :favorites
  has_many :teams, :through => :favorites 
end

To start this process, I'm trying to match up the id's that are common between Team.external_id and Favorite.team_id (the same is the case for User.external_id => Favorites.user_id). I can use Team.find_all_by_external_id(3333) to get the IDs of all Team objects that have an external_id of '3333'and the same goes for Favorite.find_all_by_team_id.

What's the next best step for me to obtain/show the data I'm looking for? Is a SQL join clause best? Or is it better to write if statements matching up values and iterating through the JSON arrays?

Any help is appreciated, thanks!

Upvotes: 0

Views: 337

Answers (1)

Chris Salzberg
Chris Salzberg

Reputation: 27374

This will get you all the favorites whose team_id matches the external_id attribute of a row in the teams table, for a specific team (here, the team with id 3333):

Favorite.joins("left outer join teams on teams.external_id = favorites.team_id")\
  .where('team_id' => 3333)

The tricky thing here, as I mentioned in my comments, is that you are going entirely against the grain of rails associations when you match the external id on the Team model (an attribute which you have created) to the team_id on the Favorite model (which is used throughout rails to get and assign associations).

You will see the problem as soon as you try to actually get the team for the favorite you find in the above join:

f = Favorite.joins("left outer join teams on teams.external_id = favorites.team_id")\
     .where('team_id' => 3333).first
=> #<Favorite id: 1, user_id: nil, team_id: 3333, points: nil, created_at: ... >
f.team
  Team Load (0.3ms)  SELECT "teams".* FROM "teams" WHERE "teams"."id" = 3333 LIMIT 1
=> nil

What's going on here? If you look closely at the query, you'll see that rails is selecting teams whose id is 3333. Note that it is not looking for teams whose external id is 3333, which is what you want.

The fundamental problem is that you are trying to use external ids (ids specific to your API) for associations, which won't work. And indeed, there is no reason to do it this way.

Instead, try this:

Favorite.joins(:team).where('teams.external_id = 3333')

This will get you all favorites whose teams have the external id 3333. Note that Rails will do this by joining on teams.id = favorites.team_id, then filtering by teams.external_id:

SELECT "favorites".* FROM "favorites" INNER JOIN "teams"
  ON "teams"."id" = "favorites"."team_id" WHERE (teams.external_id = 3333)

You can do the same thing the other way around:

Team.joins(:favorites).where('teams.external_id = 3333')

which will generate the SQL:

SELECT "teams".* FROM "teams" INNER JOIN "favorites"
  ON "favorites"."team_id" = "teams"."id" WHERE (teams.external_id = 3333)

Note again that it is the id that is being used in the join, not the external id. This is the right way to do this: use the conventional id for your associations, and then just filter wherever necessary by your (custom-defined, API-specific) external id.

Hope that helps!

UPDATE:

From the comments, it seems that the team_id on your Favorite model is being defined from the API data, which means that the id corresponds to the external_id of your Team model. This is a bad idea: in rails, the foreign key <model name>_id (team_id, user_id, etc.) has a specific meaning: the id is understood to map to the id field of the corresponding associated model (Team).

To get your associations to work, you need to use ids (not external ids) for associations everywhere (with your User model as well). To do this, you need to translate associations defined in the API to ids in the rails app. When you add a favorite from the API, find the Team id corresponding to the API team id.

external_team_id = ... # get external team id from API JSON data
favorite.team_id = Team.find_by_external_id(external_team_id).id

So you are assigning the id of the team with a given external id. You need to query the DB for each favorite you load from the API, which is potentially costly performance-wise, but since you only do it once it's not a big deal.

Upvotes: 2

Related Questions