【问题标题】:has_many :through : How do you access join table attributes?has_many :through : 你如何访问连接表属性?
【发布时间】:2013-09-18 22:41:11
【问题描述】:

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

不包括连接表数据。

谢谢。

【问题讨论】:

  • ModelOption 模型在两个 id 之外还有什么作用吗?
  • 是的,ModelOption 上面有很多属性。例如,价格,其中一个选项的价格会因型号而异。
  • 我也有同样的问题。你是怎么解决的?
  • @Karsten,我发现在您指定之后,连接表属性可以作为model.price 透明访问(其中price 作为ModelOption 的属性) has_many ... select() 声明以包含它们。

标签: ruby-on-rails activerecord


【解决方案1】:

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

加载结果

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

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

现在,如果您必须检索一条狗,您不希望其中包含人员属性。在 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 还允许您在查询中使用该表。

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

【讨论】:

  • 很遗憾,这不是我所看到的。这是我运行时的结果:options = model.options.includes(:model_options) 然后 options.to_sql:=&gt; "SELECT "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_options 属性。但是请注意,我可以说:model.options.where(model_options: {price: 10}),它会返回适当的选项。但 AR 仍然没有返回 model_options 数据。
  • 这可能是因为 Rails 决定在这里执行两个查询会有更好的性能。您不应该相信#to_sql 显示的内容,因为它只会在有多个查询时显示第一个查询。要查看实际运行的内容,请在 rails console 中触发查询。
  • 是的,我触发了查询。结果集不包含链接表属性。我在上面发布的 SQL 是这样做执行的查询。需要明确的是,这在 Rails 3 中不是问题。至少对我来说,这是 Rails 4 中的新问题。
  • 实际上,在某些情况下,您希望将一件事的属性“组合”到另一件事中。例如userhas_many :folders, through: :folder_permissionsfolderhas_many :users, through :folder_permissions。人们期望文件夹权限是文件夹的一个属性,但在 habtm 关联中,将这些权限存储在连接模型中是最有意义的。我也想知道如何在 rails 4 中做到这一点。
  • 您知道如何使用连接表中定义的范围吗?因为如果我使用&lt;user&gt;.items.custom_scope.references(:join_table),它将尝试在Item::ActiveRecord_Associations_CollectionProxy 之上执行custom_scope。
【解决方案2】:

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

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

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

options = model.options.eager_load( :model_options )

# then
data = options.first.model_options.first

【讨论】:

    【解决方案3】:

    你编辑你的模型:

    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 >>model = Model.first

    002 >>model.all_options

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

    【讨论】:

      【解决方案4】:

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

      如果ModelOption 有一个名为name 的列,并且您希望它可以在Model 中访问 s Option 实例,您只需这样做:

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

      这是因为当你调用model.options 时,Rails 会自动从OptionModelOption 发出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 清楚地做到这一点。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-10-25
        • 1970-01-01
        • 1970-01-01
        • 2012-03-10
        • 2013-08-14
        相关资源
        最近更新 更多