Rails жаждет загрузки, похоже, неправильно запрашивает
Я пытаюсь загрузить приложение в 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: я также попытался создать область, которая включает в себя как 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
2 ответа
С вашим примером все в порядке, и он закончится двумя запросами, потому что именно так рьяная загрузка реализована в rails. Это становится удобным, если у вас есть много связанных записей. Вы можете прочитать больше об этом здесь
То, что вы, вероятно, хотите, это простое соединение:
@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
Поработав с предложениями салазок, я наконец-то нашел это решение. Я уверен, что это может быть сделано чище с плагином, но вот что у меня есть сейчас:
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
Собираюсь отметить ответ Следя как правильный, поскольку именно его помощь (ответы + комментарии объединены) привела меня сюда, хотя этот код я в конечном итоге использую.