Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
566 views
in Technique[技术] by (71.8m points)

ruby on rails - has_many :through : How do you access join table attributes?

I have the following models in Rails 4 with a simple has_many :through association:

class Model < ActiveRecord::Base
  has_many :model_options
  has_many :options, through: :model_options
end

class Option < ActiveRecord::Base
  has_many :model_options
  has_many :models, through: :model_options
end

class ModelOption < ActiveRecord::Base
  belongs_to :model
  belongs_to :option
end

I want to be able to iterate over a Model instance's Options:

  model = Model.find.first
  model.options.each {}

and access the attributes on the join table.

In Rails 3 you could do this:

class Model < ActiveRecord::Base
  has_many :model_options
  has_many :options, through: :model_options , select: 'options.*, model_options.*'
end

But select: is deprecated and this produces a deprecation warning.

That said, the SQL generated contains the link table data:

SELECT options.*, model_options.* FROM "options"
INNER JOIN "model_options" ON "options"."id" = "model_options"."option_id"
WHERE "model_options"."model_id" = $1 ORDER BY "options".name ASC  [["model_id", 1]]

But the collection returned by AR from model.options removes the link table data.

To remove the deprecations warning in Rails 4, and still produce the same SQL, I did this:

class Model < ActiveRecord::Base
  has_many :model_options
  has_many :options, -> { select('options.*, model_options.*') }, through: :model_options
end

So, the query is correct, but I am struggling to find the correct way to access the link table data.

I have tried various ways:

 model options
 model.options.joins(:model_options)
 model.options.select('options.*, model_options.*')
 model.model_options.joins(:option)
 ...

None include the join table data.

Thanks.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

The answer may be different regarding what you want to achieve. Do you want to retrieve those attributes or to use them for querying ?

Loading results

ActiveRecord is about mapping table rows to objects, so you can't have attributes from one object into an other.

Let use a more concrete example : There are House, Person and Dog. A person belongs_to house. A dog belongs_to a person. A house has many people. A house has many dogs through people.

Now, if you have to retrieve a dog, you don't expect to have person attributes in it. It wouldn't make sense to have a car_id attribute in dog attributes.

That being said, it's not a problem : what you really want, I think, is to avoid making a lot of db queries, here. Rails has your back on that :

# this will generate a sql query, retrieving options and model_options rows
options = model.options.includes( :model_options )

# no new sql query here, all data is already loaded
option = options.first

# still no new query, everything is already loaded by `#includes`
additional_data = option.model_options.first

Edit : It will behaves like this in console. In actually app code, the sql query will be fired on second command, because first one didn't use the result (the sql query is triggered only when we need its results). But this does not change anything here : it's only fired a single time.

#includes does just that : loading all attributes from a foreign table in the result set. Then, everything is mapped to have a meaningful object oriented representation.

Using attributes in query

If you want to make query based on both Options and ModelOptions, you'll have to use #references. Let say your ModelOption has an active attribute :

# This will return all Option related to model 
# for which ModelOption is active
model.options.references( :model_options ).where( model_options: { active: true })

Conclusion

#includes will load all foreign rows in result set so that you can use them later without further querying the database. #references will also allow you to use the table in queries.

In no case will you have an object containing data from an other model, but that's a good thing.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...