我在Rails 4中使用简单的has_many:通过关联:
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
我希望能够遍历Model实例的选项:
model = Model.find.first
model.options.each {}
并访问连接表上的属性。
在Rails 3中你可以这样做:
class Model < ActiveRecord::Base
has_many :model_options
has_many :options, through: :model_options , select: 'options.*, model_options.*'
end
但是select:已弃用,这会产生弃用警告。
也就是说,生成的SQL包含链接表数据:
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]]
但AR从model.options返回的集合删除了链接表数据。
要删除Rails 4中的弃用警告,并仍然生成相同的SQL,我这样做了:
class Model < ActiveRecord::Base
has_many :model_options
has_many :options, -> { select('options.*, model_options.*') }, through: :model_options
end
所以,查询是正确的,但我很难找到正确的方法来访问链接表数据。
我尝试过各种方法:
model options
model.options.joins(:model_options)
model.options.select('options.*, model_options.*')
model.model_options.joins(:option)
...
无包含联接表数据。
感谢。
答案 0 :(得分:30)
关于您想要达到的目标,答案可能会有所不同。您要检索这些属性还是使用它们进行查询?
ActiveRecord是关于将表行映射到对象,因此您不能将属性从一个对象转换为另一个对象。
让我们用一个更具体的例子:有House,Person和Dog。一个人属于房子。狗属于一个人。一所房子有很多人。一个房子里有很多人通过。
现在,如果您必须检索一只狗,您不希望其中包含人物属性。在dog属性中使用car_id属性是没有意义的。
话虽这么说,但这不是问题:我认为,你真正想要的是避免在这里进行大量的数据库查询。 Rails背后有:
# 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
编辑:它在控制台中的行为与此类似。在实际的应用程序代码中,sql查询将在第二个命令上触发,因为第一个命令没有使用结果(仅当我们需要结果时才会触发sql查询)。但这并没有改变任何事情:它只发了一次。
#includes
就是这样做:从结果集中的外表加载所有属性。然后,所有内容都被映射为具有有意义的面向对象的表示。
如果您想根据Options和ModelOptions进行查询,则必须使用#references
。假设您的ModelOption具有active
属性:
# This will return all Option related to model
# for which ModelOption is active
model.options.references( :model_options ).where( model_options: { active: true })
#includes
将加载结果集中的所有外部行,以便您以后可以使用它们而无需进一步查询数据库。 #references
还允许您在查询中使用该表。
在任何情况下,您都不会有包含来自其他模型的数据的对象,但这是一件好事。
答案 1 :(得分:3)
就像Olivier所说,你必须急切加载这个联想。
由于某些原因,当您使用包含时,关联不会返回单个model_options数据。
当我强制AR使用 eager_load 执行单个查询时,它适用于我。
options = model.options.eager_load( :model_options )
# then
data = options.first.model_options.first
答案 2 :(得分:2)
您编辑模型:
class Model < ActiveRecord::Base
has_many :model_options
has_many :options, through: :model_options
def all_options
model_options + options
end
end
现在可以通过加入(&#34; model_options&#34;)表和&#34;选项&#34;来访问所有属性。表,像这样:
001&gt;&gt; model = Model.first
002&gt;&gt; model.all_options
更多:https://blog.codedge.io/rails-join-table-with-extra-attributes/
答案 3 :(得分:0)
解决方案实际上很简单,OP 在他的问题上说得对。
如果 ModelOption
有一个名为 name
的列并且您希望它可以在 Model
s Option
个实例,您只需:
model.options.select('options.*, model_options.*').each do |model|
puts model.name
end
这是因为当您调用 Option
时,Rails 会自动发出从 ModelOption
到 model.options
的 JOIN,因此它们的所有列都已经被 SQL“合并”在单行。然后对该关系的 .select
调用将使它们在结果实例中可用。
但是请注意,您正在以这种方式从联接模型中选择所有属性 (*)
,并且您将在许多字段(如 id, created_at, updated_at
等)中产生歧义。
因此,要走的路是明确您要选择的连接模型的列,例如:
model.options.select('options.*, model_options.name').each do |model|
puts model.name
end
最后,如果您的 Model
和 ModelOption
模型都有一个名为 #name 的列,您可以重命名其中一个以消除这样的歧义:
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
最后,如果您总是要使用来自连接模型的这些数据,您甚至可以在关联本身中声明它,如下所示:
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
如您所见,投票最多的答案是不正确的,因为它指出 you can't have attributes from one object into an other
,而且您可以使用 Rails 清楚地做到这一点。