Это проблема, с которой я часто сталкиваюсь. Были некоторые аналогичные вопросы по этой проблеме, но ни одна из них не была очень полной (и они, возможно, устарели, поскольку 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.