class User
has_many :books
I need a query that returns:
Users whose most recent book has :complete => true. i.e. If a user's most recent book has :complete => false, I do not want them in my resul开发者_如何学运维t.
What I have so far
User.joins(:books).merge(Book.where(:complete => true))
which is a promising start but does not give me the result I need. I've tried adding an .order("created_on desc").limit(1)
Thanks!
If you aren't going to go with @rubyprince's ruby solution, this is actually a more complex DB query than ActiveRecord can handle in it's simplest form because it requires a sub-query. Here's how I would do this entirely with a query:
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
To convert this into ActiveRecord I would do the following:
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
You can then get a list of all users that have a last completed book by doing the following:
User.last_book_completed
This adds a little overhead, but saves complexity and increases speed later when it matters.
Add a "most_recent" column to books. Make sure you add an index.
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
Then, when you save a book, update 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
Now, for your query
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
This allows you to write it like this and the result is a Relation to be used with other scopes.
User.last_book_completed
I recently came across a similar problem and here is how I solved it:
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
This way we only use ActiveRecord methods (no extra SQL) and can reuse it when considering any subset of books for users (first, last, last n books, etc...). You need the last 'uniq' cause otherwise each user would appear twice..
I cant think of a way to do it in a single query but you can do:
User.all.select { |user| user.books.order("created_at desc").limit(1).complete }
you should be using scopes here - they will make your life much simple (this is a rails3 example)
In your book.rb model
scope :completed, where("complete = ?", true)
scope :recent, order("created_on ASC")
Then you can call User.books.recent.completed
to get what you want
based off Pan's answer but with a left join instead of an inner join, and no grouping:
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
}
精彩评论