Ask A Question

Notifications

You’re not receiving notifications from this thread.

Dynamically Defined has_many with odd behavior

Chris Zempel asked in General

Here's a quirk that's confounding me:

Data Model:

class Athlete < ApplicationRecord
    has_many :stats
end

class Stat < ApplicationRecord
  belongs_to :athlete
  belongs_to :position, polymorphic: true
end

# then I have a long list of positions, namespaced like so:

class Position::Quarterback < ApplicationRecord
  has_many :stats
  has_many :athletes, through: :stats
end

class Position::RunningBack < ApplicationRecord
  has_many :stats
  has_many :athletes, through: :stats
end

I've got a big list of all the other positions elsewhere in the application.

Position.full_position_names #=> ["Quarterback", "RunningBack", ...]

Figured it would be much nicer to define all the has_many relationships off that list rather than type them all out by hand, so when the list changes, so does the defined relationship.

class Athlete < ApplicationRecord
  Position.full_position_names.each do |position|
    self.send(:has_many,                                    # => dynamically calling has_many with position names so we can change things easier
              "#{position.to_s.underscore}_stats".to_sym,   # => position name Quarterback will produce association quarterback_stats
              -> { order(season: :asc)},                    # => ordered by season with lowest year first
              through: :stats,                              # => look at stats table
              source: :position,                            # => since stat->position is polymorphic, we want it to look at :position_type column on stats table
              source_type: "Position::#{position.to_s}")    # => with a :position_type of Position::Quarterback
  end
end

So far, so good. Now when I call quarterback_stats, it works:

irb(main):007:0> Athlete.first.quarterback_stats
  Athlete Load (0.7ms)  SELECT  "athletes".* FROM "athletes" ORDER BY "athletes"."id" ASC LIMIT $1  [["LIMIT", 1]]
  Position::Quarterback Load (0.8ms)  SELECT "quarterbacks".* FROM "quarterbacks" INNER JOIN "stats" ON "quarterbacks"."id" = "stats"."position_id" WHERE "stats"."athlete_id" = $1 AND "stats"."position_type" = $2 ORDER BY "quarterbacks"."season" ASC  [["athlete_id", 212080005], ["position_type", "Position::Quarterback"]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Position::Quarterback id: 801653172, season: 2013, passing_yards: 180, passing_touchdowns: 8, rushing_yards: 80, rushing_touchdowns: 2, created_at: "2016-05-10 16:20:06", updated_at: "2016-05-10 16:20:06">, #<Position::Quarterback id: 833410073, season: 2014, passing_yards: 180, passing_touchdowns: 8, rushing_yards: 80, rushing_touchdowns: 2, created_at: "2016-05-10 16:20:06", updated_at: "2016-05-10 16:20:06">, #<Position::Quarterback id: 111928452, season: 2015, passing_yards: 180, passing_touchdowns: 8, rushing_yards: 80, rushing_touchdowns: 2, created_at: "2016-05-10 16:20:06", updated_at: "2016-05-10 16:20:06">, #<Position::Quarterback id: 530756924, season: 2016, passing_yards: 180, passing_touchdowns: 8, rushing_yards: 80, rushing_touchdowns: 2, created_at: "2016-05-10 16:20:06", updated_at: "2016-05-10 16:20:06">]>

But when I go and call running_back_stats...It doesn't work :(

irb(main):008:0> Athlete.first.running_back_stats
  Athlete Load (0.6ms)  SELECT  "athletes".* FROM "athletes" ORDER BY "athletes"."id" ASC LIMIT $1  [["LIMIT", 1]]
  Position::RunningBack Load (0.7ms)  SELECT "running_backs".* FROM "running_backs" INNER JOIN "stats" ON "running_backs"."id" = "stats"."position_id" WHERE "stats"."athlete_id" = $1 AND "stats"."position_type" = $2 ORDER BY "running_backs"."season" ASC  [["athlete_id", 212080005], ["position_type", "Position::RunningBack"]]
=> #<ActiveRecord::Associations::CollectionProxy []>

But when I do the same query manually....

irb(main):009:0> Athlete.first.stats.where(position_type: "Position::RunningBack")
  Athlete Load (0.5ms)  SELECT  "athletes".* FROM "athletes" ORDER BY "athletes"."id" ASC LIMIT $1  [["LIMIT", 1]]
  Stat Load (0.5ms)  SELECT "stats".* FROM "stats" WHERE "stats"."athlete_id" = $1 AND "stats"."position_type" = $2  [["athlete_id", 212080005], ["position_type", "Position::RunningBack"]]
=> #<ActiveRecord::AssociationRelation [#<Stat id: 419195171, athlete_id: 212080005, position_type: "Position::RunningBack", position_id: 505436969, created_at: "2016-05-10 16:20:06", updated_at: "2016-05-10 16:20:06">, #<Stat id: 110689410, athlete_id: 212080005, position_type: "Position::RunningBack", position_id: 4509324, created_at: "2016-05-10 16:20:06", updated_at: "2016-05-10 16:20:06">, #<Stat id: 832556053, athlete_id: 212080005, position_type: "Position::RunningBack", position_id: 927202847, created_at: "2016-05-10 16:20:06", updated_at: "2016-05-10 16:20:06">, #<Stat id: 680959409, athlete_id: 212080005, position_type: "Position::RunningBack", position_id: 776646567, created_at: "2016-05-10 16:20:06", updated_at: "2016-05-10 16:20:06">]>

I get back all the relevant stats that point to the existing positions.

Any idea what could be going on here?

Reply

Maybe try adding the class_name to each dynamic has_many that you define

Reply

No dice! I think Rails will automatically take the modelname_type stored on whatever polymorphic model and get the table/instantiate based off that, else I wouldn't have been able to successfully query the quarterbacks.

But if you have any other ideas I would love to hear em! I'm stuck. :(

Reply

The queries that aren't working are because the positions don't exist. I just assumed they had to in order for the join table to point at them...not sure what happened yet!

my feels: :D .... D: ... :D ... :'(

Reply

Retrospecting, I should have tried to reproduce this independently of the app much, much sooner in the process. like after 15 minutes soon. Would've realized it right then.

Reply
Join the discussion
Create an account Log in

Want to stay up-to-date with Ruby on Rails?

Join 86,796+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.