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.
The answer may be different regarding what you want to achieve. Do you want to retrieve those attributes or to use them for querying ?
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.
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 })
#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.
Just like Olivier said you have to eager load the association.
For some reason the association is not returning the single model_options data when you use includes .
It works for me when i force AR to do a single query with eager_load .
options = model.options.eager_load( :model_options )
# then
data = options.first.model_options.first
your edit your model :
class Model < ActiveRecord::Base
has_many :model_options
has_many :options, through: :model_options
def all_options
model_options + options
end
end
Now can access all atribute from joining ("model_options") table and "options" table, like this:
001 >>model = Model.first
002 >>model.all_options
More:https://blog.codedge.io/rails-join-table-with-extra-attributes/
The solution is actually quite simple, and OP had it right on his question.
If ModelOption
has a column named name
and you want it to be accessible in the Model
s Option
instances, you simply do:
model.options.select('options.*, model_options.*').each do |model|
puts model.name
end
The way this works is because Rails is going to automatically issue a JOIN from Option
to ModelOption
when you call model.options
, so all their columns are already "merged" by SQL in a single line. Then the .select
call on that relation will make them available in the resulting instances.
Notice, however, that you are selecting ALL attributes (*)
from the join model this way, and you are going to have ambiguity in many fields (like id, created_at, updated_at
, and so on.
So, the way to go is being explicit about the columns of the joined models that you want to select, for instance:
model.options.select('options.*, model_options.name').each do |model|
puts model.name
end
Lastly, if both your Model
and ModelOption
models had a column named #name, you could rename one of them to eliminate the ambiguity like this:
model.options.select('options.*, model_options.name as model_option_name').each do |model|
puts model.name # will get you Model#name
puts model.model_option_name # will get you ModelOption#name
end
Lastly, if you are always going to use this data from the join model, you can even declare this in the association itself, like so:
class Model < ApplicationRecord
has_many :model_options
has_many :options, -> { distinct.select("options.*, model_options.name") }, through: :model_options
end
# And then you can:
model.options.each do |option|
puts option.name # this will get you ModelOption#name
end
As you can see, the most upvoted answer is incorrect, because it states that you can't have attributes from one object into an other
, and you can clearly do that with Rails.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.