Подтвердить что ты не робот

Порядок по счету отношения has_many

Это проблема, с которой я часто сталкиваюсь. Были некоторые аналогичные вопросы по этой проблеме, но ни одна из них не была очень полной (и они, возможно, устарели, поскольку Rails 4, возможно, внедрили новые функции, которые помогают с этой проблемой)

Позвольте мне привести простой пример проблемы и известные способы "решить" проблему:


Скажем, у меня есть модель User и модель Post и User has_many :posts

Теперь я хочу получить первую пятерку пользователей с наибольшим количеством сообщений.

Ниже приведены параметры, которые я знаю, но все они имеют свои недостатки:

1)

users = User.all
@top_users = users.sort {|a,b| a.posts.count <=> b.posts.count}.take(5)

Недостатки: для каждого пользователя создается запрос DataBase, что делает это решение очень медленным.

2) Используйте код SQL непосредственно с помощью Join (См. Например этот вопрос и ответ)

select('users.*, COUNT(posts.id) AS posts_count').joins(:posts).group('users.id').order('posts_count DESC').take(5)

Выполняет всю логику сортировки в базе данных. Однако:

  • Мы используем много специфичного для БД кода (в PostgreSQL, например, нам нужен другой синтаксис). Было бы лучше использовать методы ActiveRecord, если это возможно.
  • Использование Inner Join означает, что пользователи без каких-либо сообщений никогда не будут возвращены. Это проблема, когда мы хотим вернуть пользователей без сообщений.

3) Используйте SQL напрямую с Outer Join (см. например этот вопрос и ответы)

User.select("users.*, COUNT(posts.id) as posts_count").joins("LEFT OUTER JOIN posts ON posts.user_id = users.id").group("posts.id").order("posts_count DESC")

Это также возвращает пользователей без сообщений. Недостатки:

  • Еще больше кода, специфичного для БД, как №2, и еще труднее читать.

4) Используйте столбец Counter Cache (Полное объяснение этой техники см. В этот эпизод Railscasts)

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

Это очень быстро и доступно для чтения. Недостатком является то, что мы можем использовать его только после того, как мы определили новое поле на User. Для многих ситуаций это приемлемо, но будет сложнее сделать гибким, потому что таблица пользователей должна быть изменена, чтобы это работало на одну ассоциацию, которую мы могли бы создать для пятерки. Кроме того, поскольку это кешированное поле, существуют манипуляции с базами данных, которые не будут инициировать обновление в поле.

Есть ли более удобный (понятный и эффективный) способ сделать это? Предпочтительное, что использует встроенные методы ActiveRecord.

4b9b3361

Ответ 1

Другой метод, с некоторыми ограничениями, которые могут сделать его скорее частью решения:

User.where(:id => Post.group(:user_id).
                       order("count(*) desc").
                       limit(5).
                       keys)

Это было бы чрезвычайно эффективно в терминах базы данных при поиске пяти пользователей с наибольшим количеством сообщений, поскольку нужно всего лишь сканировать индекс в столбцах столбцов user_id таблицы posts, поэтому это было бы полезно для очень больших наборов данных. Это также довольно "чистый" код Rails/ActiveRecord, который должен быть практически независимым от базы данных.

Если возврат Пользователей в порядке их пост-счета является критическим, тогда менее эффективный метод сортировки может быть использован после того, как эти пять были идентифицированы, или порядок извлечения ключей можно использовать в ruby ​​для сортировки возвращаемых Пользователей.

Ответ 2

Здесь стоит посмотреть:

User.joins("left join posts on posts.user_id = users.id").
     group(:id).
     order("count(*) desc").
     limit(5)

Это немного ручное в соединении, но если вы знали, что по крайней мере пять пользователей имели сообщение или не хотели перечислять пользователей, у которых не было сообщения, вы могли бы использовать регулярное соединение:

User.joins(:posts).
     group(:id).
     order("count(*) desc").
     limit(5)

Счетчик (*) не обязательно является надежным, если в нем есть другие has_many, но в этом случае вы, вероятно, захотите создать запрос, например:

select ...
from   users ...
order by (select count(*) from posts where posts.user_id = users.id)

p.s. Протестировано на PostgreSQL. Группа GROUP BY в столбце ID, конечно, не будет работать на Oracle, но не уверен в других.

Ответ 3

Этот параметр, возможно, стоит посмотреть, не тестировал его, поэтому может потребоваться некоторая настройка.

class Post < ActiveRecord::Base
  belongs_to :user, counter_cache: true
end

Используйте counter_cache, и вы попадете в одну таблицу в свой db.

class User < ActiveRecord::Base
  has_many :posts

  def self.top_5
    order('post_counts DESC').limit(5)
  end
end

Добавить posts_count целочисленный столбец в таблице пользователей со значением по умолчанию 0.

class AddPostsCountToUsers < ActiveRecord::Migration
  def change
    add_column :users, :posts_count, :integer, default: 0
  end
end

Если у вас уже есть существующие пользователи в вашем db.

Вам нужно будет запустить следующее в консоли или сделать это в задаче rake, если вам нужно запустить ее несколько раз:

User.find_each { |user| User.reset_counters(user.id, :posts) }

Ответ 4

Вы можете сделать, как показано ниже,

User.joins(:posts).select('users.*, count(*) as posts_count').group('users.id').order('posts_count')