31

我在 Rails 4 中有以下模型,带有一个简单的 has_many :through 关联:

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.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

4 回答 4

33

关于您想要实现的目标,答案可能会有所不同。您想检索这些属性还是将它们用于查询?

加载结果

ActiveRecord 是关于将表行映射到对象,因此您不能将属性从一个对象映射到另一个对象。

让我们用一个更具体的例子:有房子、人和狗。一个人属于房子。一条狗属于一个人。一个房子有很多人。一个房子通过人有很多狗。

现在,如果你必须检索一条狗,你不希望它有 person 属性。在 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还将允许您在查询中使用该表。

在任何情况下,您都不会拥有包含来自其他模型的数据的对象,但这是一件好事。

于 2013-09-14T10:36:10.917 回答
3

就像 Olivier 说的那样,你必须急切地加载关联。

由于某种原因,当您使用include时,关联不会返回单个 model_options 数据。

当我强制 AR 使用eager_load进行单个查询时,它对我有用。

options = model.options.eager_load( :model_options )

# then
data = options.first.model_options.first
于 2013-11-07T01:03:20.790 回答
2

你编辑你的模型:

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

  def all_options 
        model_options + options
  end

end

现在可以从加入(“model_options”)表和“options”表中访问所有属性,如下所示:

001 >>模型=模型.first

002 >>model.all_options

更多:https ://blog.codedge.io/rails-join-table-with-extra-attributes/

于 2018-01-10T06:19:14.197 回答
0

解决方案实际上非常简单,OP 在他的问题上是正确的。

如果ModelOption有一个名为的列name并且您希望它可以在Model sOption实例中访问,您只需执行以下操作:

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

这样做的方式是因为 Rails 会在您调用时自动发出 JOIN from Optionto ,因此它们的所有列都已经被 SQL “合并”在一行中。然后对该关系的调用将使它们在结果实例中可用。ModelOptionmodel.options.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 清楚地做到这一点。

于 2021-06-07T20:33:19.167 回答