Оператор SQL для первой строки в ассоциации has_many - PullRequest
2 голосов
/ 27 июня 2011

Я пытаюсь получить доступ только к первым строкам связанных таблиц.Какой самый лучший и быстрый способ сделать это?(В идеале оператор SQL)

Ассоциации:

class User
  has_many :addresses


class Address
  belongs_to :user

Самый быстрый способ, который я мог выяснить, был:

User.includes(:addresses).find(:all).map {|m| m.addresses.first.area_code}

Но эта операция слишком медленная для моих требований(представьте, что у меня 200 Users со средним 20 Adresses (~4000 Adresses в целом))

Это всего лишь пример того, что я пытаюсь сделать.

Я думал, что этоможет быть выражено оператором SQL JOIN, но я не вижу, как получить таблицу адресов каждого пользователя в первый раз.

Пример ожидаемого вывода:

12345 #=>User.find(1).addresses.first.area_code

56789 #=>User.find(2).addresses.first.area_code 

23456 #=>User.find(3).addresses.first.area_code 

.....

Ответы [ 2 ]

0 голосов
/ 05 октября 2011

Существует несколько подходов для этого.Несмотря на то, что 4000 записей не так уж много в схеме вещей, избегать необходимости посещать несколько запросов (как вы намекнули в своем вопросе) - это «хорошая вещь».

Ключом к решению являетсякак вы выбираете, какой адрес вы хотите посмотреть.«Первый» является относительным и недетерминированным без квалификатора (упорядочить по или каким-либо другим атрибутом).

Простой подход - ассоциация has_one

Один простой подход заключается в определении отношения has_one, например

class User
  has_many :addresses
  has_one :primary_address, :class_name => 'Address', :conditions => ["primary = ?", true]
end

При условии, что у вас есть атрибут в адресе, вы можете использовать решающий элемент.Если вы не укажете условие, вы получите только то, что вводится адресом «первым», так как has_one применяет SQL LIMIT 1. к базовому запросу.

Это упрощает сбор пользователей и одного запроса.сведения об адресе:

# iterating over users
User.scoped.each { |user| puts user.primary_address.area_code }
# or collect area_codes
area_codes = User.scoped.collect{ |user| user.primary_address.area_code }

Тем не менее, он все еще посещает каждую строку, поэтому для каждого пользователя будет добавлен дополнительный запрос (и обработка массива ruby).

Намного лучше - Арель

После того, как вы установили критерии выбора между несколькими адресами, которые может иметь пользователь, можно свернуть это в один запрос.

Если я предполагаю, что мы просто выберем адрес поmin (id) - и при условии, что вы находитесь на Rails 3 - вот подход Arel для запроса различных кодов области:

address = Address.arel_table
primary_addresses = Address.where(
  address[:id].in(
    address.group(address[:user_id]).project(address[:id].minimum)
  )
)

primary_addresses - это область, которая дает вам полный набор «первичных» адресов дляпользователи.Затем вы можете обработать / составить список / собрать, как вам нужно, например:

area_codes = primary_addresses.collect(&:area_code)

Запрос фактически сгенерирует SQL следующим образом:

SELECT "addresses".* 
FROM "addresses" 
WHERE "addresses"."id" IN (
  SELECT MAX("addresses"."id") AS max_id FROM "addresses" GROUP BY "addresses"."user_id"
)

Как видите, суб-выбор возвращаетсясписок идентификаторов адресов - по одному для каждого пользователя.Если вы хотите использовать другой критерий для выбора используемого адреса, вы можете изменить подзапрос (например, чтобы он был выбран по первичному = true с пределом 1)

0 голосов
/ 27 июня 2011
area_codes = 
  Address.find_by_sql(
    "SELECT DISTINCT user_id, area_code FROM addresses"
  ).index_by(&:user_id)
User.find(:all).map{ |u| [u, area_codes[u.id]] }

, если вы хотите, чтобы все они были в одном куске.

...