使用多态关联时如何避免n + 1个查询?

时间:2015-01-27 12:47:38

标签: ruby-on-rails activerecord

我有4个型号,让我们说:

class Photo < ActiveRecord::Base
  belongs_to :photoable, polymorphic: true
end

class User < ActiveRecord::Base
  has_one :photo, as: :photoable
end    

class Company < ActiveRecord::Base
  has_one :photo, as: :photoable
  has_many :products
end

class Products < ActiveRecord::Base
  belongs_to :company
end

因此,查询Photo.all.includes(:photoable)有效。

但是,如果我使用Photo.all.includes(photoable: :products),只有所有加载的照片都属于公司才有效。如果关系包含用户和公司的照片,则会出现此错误:

ActiveRecord::ConfigurationError (Association named 'products' was not found; perhaps you misspelled it?):

这是因为用户与产品没有关系。

是否有任何方法可以将用户和产品公司急切加载到照片关系中?

修改

此问题与Eager load polymorphic 不重复。正如我在下面评论的那样,在这个问题中,我希望对具有不同关联的多态关联做一个急切的加载(一个有产品而另一个没有关联)。在该问题中,OP对表名使用了错误的名称。

5 个答案:

答案 0 :(得分:1)

Rails 6增加了对这种情况的支持,现在只有在为对象定义了嵌套关联时才预加载嵌套关联,您甚至可以混合来自不同类的关联:

Photo.all.includes(photoable: [:products, :posts, :some_undefined])

完整的可运行示例:

require "bundler/inline"

gemfile(!ENV['SKIP_INSTALL']) do
  source "https://rubygems.org"
  # rails 5 gives the error, run with RAILS5=1 to see
  gem "activerecord", ENV['RAILS5'] && "~>5.0" || "~>6.0"
  gem "sqlite3"
end

require "active_record"
require "minitest/autorun"
require "logger"

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table(:photos){|t| t.references :photoable, polymorphic: true }
  create_table(:users)
  create_table(:companies)
  create_table(:products){|t| t.references :company }
  create_table(:posts){|t| t.references :user }
end

# --- Models:
class Photo < ActiveRecord::Base
  belongs_to :photoable, polymorphic: true
end

class User < ActiveRecord::Base
  has_one :photo, as: :photoable
  has_many :posts
end    

class Company < ActiveRecord::Base
  has_one :photo, as: :photoable
  has_many :products
end

class Product < ActiveRecord::Base
  belongs_to :company
end

class Post < ActiveRecord::Base
  belongs_to :user
end

# --- Test:

class SomeTest < Minitest::Test
  def setup
    2.times{    
      Company.create!.tap{|company|
        company.products.create!
        company.create_photo!
      }
      User.create!.tap{|user|
        user.posts.create!
        user.create_photo!
      }  
    }
  end

  def test_association_stuff
    rel = Photo.all.includes(photoable: [:products, :posts, :some_undefined])
    arr = rel.to_a # fetch all records here

    ActiveRecord::Base.logger.extend(Module.new{
      define_method(:debug){|msg|  raise "should not log from now on" }
    })

    assert_kind_of(Product, arr.first.photoable.products.first)
    assert_kind_of(Post, arr.last.photoable.posts.first)
  end
end

答案 1 :(得分:0)

我有同样的问题,并找到了解决方案。

您可以在查找后调用急切加载:

photos = Photo.all.includes(:photoable)
photos_with_products_association = photos.select{|p| p.photoable.is_a?(Company)}
ActiveRecord::Associations::Preloader.new.preload(
  photos_with_products_association,
  :products
)

......或更通用的

photos_with_products_association = photos.select do |p|
  p.class.reflections.keys.include?(:products)
end

答案 2 :(得分:0)

您加入公司照片时很奇怪,而不是相反。为什么要这么做?你真的需要视图或控制器中的那些公司产品吗?

SELECT * from photos
LEFT JOIN users AS u ON u.id = photos.photoable_id AND 
                        photos.photoable_type = 'User'
LEFT JOIN companies AS c ON c.id = photos.phottoable_id AND
                        photos.photoable_type = 'Company'
LEFT JOIN products AS p ON p.company_id = c.id

答案 3 :(得分:0)

您可以向Photo模型添加特定关联:

class Photo < ActiveRecord::Base
  belongs_to :photoable, polymorphic: true

  belongs_to :user, -> { where(photoable_type: 'User' ) }, foreign_key: :photoable_id
  belongs_to :company, -> { where(photoable_type: 'Company' ) }, foreign_key: :photoable_id
end

之后,您可以预加载嵌套的特定关联:

photos = Photo.all.includes(:photoable, company: :products)

可以访问这样的记录:

photos.each do |photo|
  puts photo.photoable.inspect
  puts photo.company.products.inspect if photo.photoable_type == "Company"
end

此方法加载company关联两次,但不执行n + 1次查询。

答案 4 :(得分:0)

class Products < ActiveRecord::Base
  belongs_to :company
end

应该是

class Product < ActiveRecord::Base
  belongs_to :company
end

然后它会起作用。