【问题标题】:Rails eager loading seems to be querying wrongRails 急切加载似乎查询错误
【发布时间】:2011-05-15 18:15:22
【问题描述】:

我正在尝试在我的 Rails 3 应用程序中快速加载。我已将其缩小到一个非常基本的示例,而不是生成我期望的一个查询,而是生成 4 个。

首先,这是我的模型的简单分解。

class Profile < ActiveRecord::Base
  belongs_to :gender

  def to_param
    self.name
  end
end

class Gender < ActiveRecord::Base
  has_many :profiles, :dependent => :nullify
end

然后我有一个 ProfilesController::show 操作,我在哪里查询模型。

def ProfilesController < ApplicationController
  before_filter :find_profile, :only => [:show]

  def show
  end

  private

    def find_profile
      @profile = Profile.find_by_username(params[:id], :include => :gender)
      raise ActiveRecord::RecordNotFound, "Page not found" unless @profile
    end
end

当我查看生成的查询时,它显示以下内容:

SELECT `profiles`.* FROM `profiles` WHERE `profiles`.`username` = 'matt' LIMIT 1
SELECT `genders`.* FROM `genders` WHERE (`genders`.`id` = 1)

我期望看到的是单个查询:

SELECT `profiles`.*, `genders`.* FROM `profiles` LEFT JOIN `genders` ON `profiles`.gender_id = `genders`.id WHERE `profiles`.`username` = 'matt' LIMIT 1

有人知道我在这里做错了什么吗?我在急切加载中发现的所有内容听起来都应该可行。

编辑:在尝试joins 后,按照 sled 的建议,我仍然看到相同的结果。

代码:

@profile = Profile.joins(:gender).where(:username => params[:id]).limit(1).first

查询:

SELECT `profiles`.* FROM `profiles` INNER JOIN `genders` ON `genders`.`id` = `profiles`.`gender_id` WHERE `profiles`.`username` = 'matt' LIMIT 1

再次,您可以看到没有genders 数据被检索,因此正在对genders 进行第二次查询。

我什至尝试添加select,但无济于事:

@profile = Profile.joins(:gender).select('profiles.*, genders.*').where(:username => params[:id]).limit(1).first

正确的结果是:

SELECT profiles.*, genders.* FROM `profiles` INNER JOIN `genders` ON `genders`.`id` = `profiles`.`gender_id` WHERE `profiles`.`username` = 'matt' LIMIT 1

...但稍后在访问@profile.gender 的属性时,它仍然对genders 执行了第二次查询。

编辑 2: 我还尝试创建一个包含 select 和 joins 的范围,以获取我需要的所有字段,(类似于演示的自定义左连接方法) .它看起来像这样:

class Profile < ActiveRecord::Base
  # ...
  ALL_ATTRIBUTES = [:photo, :city, :gender, :relationship_status, :physique, :children,
    :diet, :drink, :smoke, :drug, :education, :income, :job, :politic, :religion, :zodiac]

  scope :with_attributes,
    select((ALL_ATTRIBUTES.collect { |a| "`#{reflect_on_association(a).table_name}`.*" } + ["`#{table_name}`.*"]).join(', ')).
    joins(ALL_ATTRIBUTES.collect { |a|
      assoc = reflect_on_association(a)
      "LEFT JOIN `#{assoc.table_name}` ON `#{table_name}`.#{assoc.primary_key_name} = `#{assoc.table_name}`.#{assoc.active_record_primary_key}"
    }.join(' '))
  # ...
end

这会生成以下查询,看起来是正确的:

SELECT `photos`.*, `cities`.*, `profile_genders`.*, `profile_relationship_statuses`.*, `profile_physiques`.*, `profile_children`.*, `profile_diets`.*, `profile_drinks`.*, `profile_smokes`.*, `profile_drugs`.*, `profile_educations`.*, `profile_incomes`.*, `profile_jobs`.*, `profile_politics`.*, `profile_religions`.*, `profile_zodiacs`.*, `profiles`.* FROM `profiles` LEFT JOIN `photos` ON `profiles`.photo_id = `photos`.id LEFT JOIN `cities` ON `profiles`.city_id = `cities`.id LEFT JOIN `profile_genders` ON `profiles`.gender_id = `profile_genders`.id LEFT JOIN `profile_relationship_statuses` ON `profiles`.relationship_status_id = `profile_relationship_statuses`.id LEFT JOIN `profile_physiques` ON `profiles`.physique_id = `profile_physiques`.id LEFT JOIN `profile_children` ON `profiles`.children_id = `profile_children`.id LEFT JOIN `profile_diets` ON `profiles`.diet_id = `profile_diets`.id LEFT JOIN `profile_drinks` ON `profiles`.drink_id = `profile_drinks`.id LEFT JOIN `profile_smokes` ON `profiles`.smoke_id = `profile_smokes`.id LEFT JOIN `profile_drugs` ON `profiles`.drug_id = `profile_drugs`.id LEFT JOIN `profile_educations` ON `profiles`.education_id = `profile_educations`.id LEFT JOIN `profile_incomes` ON `profiles`.income_id = `profile_incomes`.id LEFT JOIN `profile_jobs` ON `profiles`.job_id = `profile_jobs`.id LEFT JOIN `profile_politics` ON `profiles`.politic_id = `profile_politics`.id LEFT JOIN `profile_religions` ON `profiles`.religion_id = `profile_religions`.id LEFT JOIN `profile_zodiacs` ON `profiles`.zodiac_id = `profile_zodiacs`.id WHERE `profiles`.`username` = 'matt' LIMIT 1

不幸的是,对关系属性的调用(例如:@profile.gender.name)似乎没有使用原始 SELECT 中返回的数据。相反,我在第一个查询之后看到了大量查询:

Profile::Gender Load (0.2ms)  SELECT `profile_genders`.* FROM `profile_genders` WHERE `profile_genders`.`id` = 1 LIMIT 1
Profile::Gender Load (0.4ms)  SELECT `profile_genders`.* FROM `profile_genders` INNER JOIN `profile_attractions` ON `profile_genders`.id = `profile_attractions`.gender_id WHERE ((`profile_attractions`.profile_id = 2))
City Load (0.4ms)  SELECT `cities`.* FROM `cities` WHERE `cities`.`id` = 1 LIMIT 1
Country Load (0.3ms)  SELECT `countries`.* FROM `countries` WHERE `countries`.`id` = 228 ORDER BY FIELD(code, 'US') DESC, name ASC LIMIT 1
Profile Load (0.4ms)  SELECT `profiles`.* FROM `profiles` WHERE `profiles`.`id` = 2 LIMIT 1
Profile::Language Load (0.4ms)  SELECT `profile_languages`.* FROM `profile_languages` INNER JOIN `profile_profiles_languages` ON `profile_languages`.id = `profile_profiles_languages`.language_id WHERE ((`profile_profiles_languages`.profile_id = 2))
SQL (0.3ms)  SELECT COUNT(*) FROM `profile_ethnicities` INNER JOIN `profile_profiles_ethnicities` ON `profile_ethnicities`.id = `profile_profiles_ethnicities`.ethnicity_id WHERE ((`profile_profiles_ethnicities`.profile_id = 2))
Profile::Religion Load (0.5ms)  SELECT `profile_religions`.* FROM `profile_religions` WHERE `profile_religions`.`id` = 2 LIMIT 1
Profile::Politic Load (0.2ms)  SELECT `profile_politics`.* FROM `profile_politics` WHERE `profile_politics`.`id` = 3 LIMIT 1

【问题讨论】:

    标签: ruby-on-rails ruby-on-rails-3 query-optimization eager-loading


    【解决方案1】:

    您的示例很好,它将以两个查询结束,因为这就是在 Rails 中实现急切加载的方式。如果您有许多关联记录,它会变得很方便。你可以阅读更多关于它here

    您可能想要的是一个简单的连接:

    @profile = Profile.joins(:gender).where(:username => params[:id])
    

    编辑

    如果配置文件由许多部分组成,这里有多种方法:

    自定义左连接 - 也许有一个插件可以完成这项工作,否则我建议执行以下操作:

    class Profile < ActiveRecord::Base
    
      # .... code .....
    
      def self.with_dependencies
    
        attr_joins    = []
        attr_selects  = []
    
        attr_selects << "`profiles`.*"
        attr_selects << "`genders`.*"
        attr_selects << "`colors`.*"
    
        attr_joins << "LEFT JOIN `genders` ON `gender`.`id` = `profiles`.gender_id"
        attr_joins << "LEFT JOIN `colors` ON `colors`.`id` = `profiles`.color_id"
    
        prep_model  = select(attr_selects.join(','))
    
        attr_joins.each do |c_join|
          prep_model = prep_model.joins(c_join)
        end
    
        return prep_model
      end
    
    end
    

    现在您可以执行以下操作:

    @profile = Profile.with_dependencies.where(:username => params[:id])
    

    另一种解决方案是使用:include =&gt; [:gender, :color],它可能需要更多查询,但它是更清洁的“rails 方式”。如果您遇到性能问题,您可能需要重新考虑您的 DB Schema,但您真的有这么重的负载吗?

    我的一个朋友为这种简单的 1:n 关系(如性别)写了一个不错的小解决方案,称为simple_enum

    【讨论】:

    • 但是,如果搜索条件需要,我相信 Rails 会将这些加入到单个查询中(例如,@profile = Profile.includes(:gender).where("genders.name = '...'"))
    • 你说得对,但不建议这样做尽管 Active Record 允许您像连接一样在急切加载的关联上指定条件,但推荐的方法是使用连接。 @ 987654323@
    • 最终,我将加入profile 的多个关系,有时这些关联将为空。因此,我将需要进行左连接。这是否意味着我必须完整地写出一个左连接以及每个连接的表名,还是在 Rails 3 中有更好的方法?
    • 谢谢雪橇。不幸的是,似乎仍在创建多个查询。我用正在生成的代码和相关查询更新了我的问题。
    • 嗯,是的,这是很重要的一点 - 如果您加入某些东西,rails 不会将其填充为“关联”对象。 Rails 只是将性别属性与个人资料的属性混合在一起。因此,在 rails 中使用连接或多或少是为了减少结果集,而不是为了检索“信息”;)所以最简单的解决方案是坚持使用 :include =&gt; [...] 解决方案,而忘记多个查询。也许你想使用像我提到的simple_enum 这样的插件。
    【解决方案2】:

    根据 sled 的建议,我终于想出了这个解决方案。我确信它可以通过插件变得更干净,但这是我现在所拥有的:

    class Profile < ActiveRecord::Base
      ALL_ATTRIBUTES = [:photo, :city, :gender, :relationship_status, :physique, :children,
        :diet, :drink, :smoke, :drug, :education, :income, :job, :politic, :religion, :zodiac]
    
      scope :with_attributes,
        includes(ALL_ATTRIBUTES).
        select((ALL_ATTRIBUTES.collect { |a| "`#{reflect_on_association(a).table_name}`.*" } + ["`#{table_name}`.*"]).join(', '))
    end
    

    两个要点是:

    • 调用includes,传递我想要的关系符号
    • 调用select 确保检索相关表的所有列。请注意,我调用了reflect_on_association,这样我就不必对相关表的名称进行硬编码,让 Rails 模型为我完成工作。

    我现在可以打电话了:

    Profile.with_attributes.where(:username => params[:id]).limit(1).first
    

    将 sled 的答案标记为正确,因为是他的帮助(答案 + cmets 相结合)导致我来到这里,即使这是我最终使用的代码。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-01-13
      • 2018-03-18
      • 1970-01-01
      相关资源
      最近更新 更多