has_many:through:如何访问连接表属性?

时间:2013-09-14 08:41:13

标签: ruby-on-rails activerecord

我在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)
 ...

无包含联接表数据。

感谢。

4 个答案:

答案 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 会自动发出从 ModelOptionmodel.options 的 JOIN,因此它们的所有列都已经被 SQL“合并”在单行。然后对该关系的 .select 调用将使它们在结果实例中可用。

但是请注意,您正在以这种方式从联接模型中选择所有属性 (*),并且您将在许多字段(如 id, created_at, updated_at 等)中产生歧义。

因此,要走的路是明确您要选择的连接模型的列,例如:

model.options.select('options.*, model_options.name').each do |model|
  puts model.name
end

最后,如果您的 ModelModelOption 模型都有一个名为 #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 清楚地做到这一点。