Rails急切加载似乎是在查询错误

时间:2011-05-15 18:15:22

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

我正试图在我的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后,我仍然看到相同的结果。

代码:

@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

...但是在访问genders的属性时,它仍会在@profile.gender上执行第二次查询。

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

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

2 个答案:

答案 0 :(得分:5)

您的示例很好,它最终会出现两个查询,因为这是在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 => [:gender, :color]它可能会更多一些查询,但它是更清晰的“轨道方式”。如果遇到性能问题,您可能需要重新考虑数据库架构,但是你真的有这么大的负载吗?

我的一位朋友为这个简单的1:n关系(如性别)写了一个很好的小解决方案,它被称为simple_enum

答案 1 :(得分:1)

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

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

将雪橇的答案标记为正确,因为这是他的帮助(答案+评论合并)导致我在这里,即使这是我最终使用的代码。