Rails-запрос через ассоциацию, ограниченную самой последней записью?
class User
has_many :books
Мне нужен запрос, который возвращает:
Пользователи, у которых самая последняя книга: complete = > true. т.е. если у самой последней книги пользователя: complete = > false, я не хотят их в моем результате.
Что я до сих пор
User.joins(:books).merge(Book.where(:complete => true))
который является многообещающим началом, но не дает мне результата, который мне нужен. Я попробовал добавить .order("created_on desc").limit(1)
до конца вышеприведенного запроса, но затем я получаю только один результат, когда ожидаю многих.
Спасибо!
Ответы
Ответ 1
Если вы не собираетесь использовать рубиновое решение @rubyprince, это на самом деле более сложный запрос БД, чем ActiveRecord может обрабатывать в нем простейшую форму, потому что для этого требуется подзапрос. Вот как бы я сделал это полностью с запросом:
SELECT users.*
FROM users
INNER JOIN books on books.user_id = users.id
WHERE books.created_on = ( SELECT MAX(books.created_on)
FROM books
WHERE books.user_id = users.id)
AND books.complete = true
GROUP BY users.id
Чтобы преобразовать это в ActiveRecord, я бы сделал следующее:
class User
scope :last_book_completed, joins(:books)
.where('books.created_on = (SELECT MAX(books.created_on) FROM books WHERE books.user_id = users.id)')
.where('books.complete = true')
.group('users.id')
end
Затем вы можете получить список всех пользователей, у которых есть последняя заполненная книга, выполнив следующие действия:
User.last_book_completed
Ответ 2
Это добавляет немного накладных расходов, но экономит сложность и увеличивает скорость позже, когда это имеет значение.
Добавьте столбец "most_recent" в книги. Убедитесь, что вы добавили индекс.
class AddMostRecentToBooks < ActiveRecord::Migration
def self.change
add_column :books, :most_recent, :boolean, :default => false, :null => false
end
add_index :books, :most_recent, where: :most_recent # partial index
end
Затем, когда вы сохраняете книгу, обновите most_recent
class Book < ActiveRecord::Base
on_save :mark_most_recent
def mark_most_recent
user.books.order(:created_at => :desc).offset(1).update_all(:most_recent => false)
user.books.order(:created_at => :desc).limit(1).update_all(:most_recent => true)
end
end
Теперь, для вашего запроса
class User < ActiveRecord::Base
# Could also include and preload most-recent book this way for lists if you wanted
has_one :most_recent_book, -> { where(:most_recent => true) }, :class_name => 'Book'
scope :last_book_completed, -> { joins(:books).where(:books => { :most_recent => true, :complete => true })
end
Это позволяет вам записать его так, и результат будет связан с другими областями.
User.last_book_completed
Ответ 3
Я не могу подумать о способе сделать это в одном запросе, но вы можете сделать:
User.all.select { |user| user.books.order("created_at desc").limit(1).complete }
Ответ 4
Недавно я столкнулся с подобной проблемой, и вот как я ее решил:
most_recent_book_ids = User.all.map {|user| user.books.last.id }
results = User.joins(:books).where('books.id in (?) AND books.complete == ?', most_recent_book_ids, true).uniq
Таким образом, мы используем только методы ActiveRecord (без дополнительного SQL) и можем повторно использовать его при рассмотрении любого подмножества книг для пользователей (сначала, последние, последние русские книги и т.д.). Вам понадобится последняя "uniq" причина, иначе каждый пользователь появится дважды.
Ответ 5
вы должны использовать области здесь - они сделают вашу жизнь очень простой (это пример rails3)
В вашей модели book.rb
scope :completed, where("complete = ?", true)
scope :recent, order("created_on ASC")
Затем вы можете вызвать User.books.recent.completed
, чтобы получить то, что вы хотите
Ответ 6
основан на Панорате ответа, но с левым соединением вместо внутреннего соединения и без группировки:
scope :with_last_book_complete, -> {
subquery = Book.select(:id)
.where("books.user_id = users.id")
.order(created_at: :desc).limit(1)
joins(<<-SQL).where(books: { complete: true })
LEFT JOIN books
ON books.user_id = users.id
AND books.id = (#{subquery.to_sql})
SQL
}