Rails 3 - nested select and ".last" - ruby-on-rails

I have a scenario which seems to be should be something addressed on the model side of things but can't figure out a combination of Activerecord methods that works. The relevant parts of my db are as below:
Part:
id
part_id
created_at
...
Parttest:
id
part_id
pass (boolean)
created_at
A Part can have many Parttests and a Parttest belongs to a single part (setup in the relevant models as has_many and belongs_to).
In it's life - a part can be tested many times and can pass, fail, pass etc.
Using rails / activerecord I would like to retrieve the recordset of Parts where the last (most recent) Parttest is a pass. Earlier records of parttest should not be taken into consideration and are for historical use only.
I've tried nested selects without much success. I could always code the logic in the controller to check each Part object in Part.all.each and push those into an array but this doesn't feel like the right way to do it. All help much appreciated :)

Edit: Misread your post, you want to do something like this reference
Part.joins(:parttests).where(parttests: {pass: true}) will give you all parts who have passing tests
try Part.joins(:parttests).last.where(parttests: {pass: true})

Using a scope
class Parttest
scope :passed_tests, -> { where(pass: true) }
end
#part = Part.find(1) # Fetch part instance
#part_tests = #part.parttests.passed_tests.last # Fetch its tests
Or using a condition on the association:
class Part
has_many :passed_tests, class_name: 'Parttest', -> { where(pass: true) }
end
#part_tests = #part.passed_tests.last
Update
Now that I have understood the question correctly. How about this?
class Part
has_many :passed_tests, class_name: 'Parttest', -> { where(pass: true) }
scope :with_last_passed_test, -> { joins(:passed_tests).last }
end
Part.with_last_passed_test

I had originally posted a lengthy SQL query as one option, but that suggestion wasn't very clean so I've removed it. Here is a way to break this down into a few small pieces.
Say we start with these Parttest rows:
id part_id created_at pass
1 1 2014-04-26 true
2 1 2014-04-27 false
3 2 2014-04-26 false
4 2 2014-04-27 true
1) First, we want to find the most recent Parttest row for each Part. We can do this with:
# Parttest model
def self.most_recent
max_rows = select("part_id, MAX(created_at) as max_created_at").group(:part_id)
joins("INNER JOIN (#{max_rows.to_sql}) AS max_rows
ON parttests.part_id = max_rows.part_id
AND parttests.created_at = max_rows.max_created_at")
end
The select will grab these values:
part_id created_at
1 2014-04-27
2 2014-04-27
And the join will connect us back up with the other Parttest columns:
id part_id created_at pass
2 1 2014-04-27 false
4 2 2014-04-27 true
2) Then we want to only keep the passing tests. We can do this with:
# Parttest model
scope :passed, -> { where(pass: true) }
Combining this with (1), we can get the most recent tests only if they were passing tests by calling:
Parttest.most_recent.passed
This would only include return this row from our example:
id part_id created_at pass
4 2 2014-04-27 true
3) Then to find all Parts where the most recent test was passing we can provide this scope:
# Part model
scope :most_recent_test_passed, -> { where("id IN (?)", PartTest.most_recent.passed.map(&:part_id)) }

Related

Are .select and or .where responsible for causing N+1 queries in rails?

I have two methods here, distinct_question_ids and #correct_on_first attempt. The goal is to show a user how many distinct multiple choice questions have been answered that are correct.
The second one will let me know how many of these distinct MCQs have been answered correctly on the first attempt. (A user can attempt a MCQ many times)
Now, when a user answers thousands of questions and has thousands of user answers, the page to show their performance is taking 30 seconds to a minute to load. And I believe it's due to the .select method, but I don't know how to replace .select without using .select, since it loops just like .each
Is there any method that doesn't cause N+1?
distinct_question_ids = #user.user_answers.includes(:multiple_choice_question).
where(is_correct_answer: true).
distinct.pluck(:multiple_choice_question_id)
#correct_on_first_attempt = distinct_question_ids.select { |qid|
#user.user_answers.
where(multiple_choice_question_id: qid).first.is_correct_answer
}.count
.pluck returns an Array of values, not an ActiveRecord::Relation.
So when you do distinct_question_ids.select you're not calling ActiveRecord's select, but Array's select. Within that select, you're issuing a fresh new query against #user for every id you just plucked -- including ones that get rejected in the select.
You could create a query named distinct_questions that returns a relation (no pluck!), and then build correct_on_first_attempt off of that, and I think you'll avoid the N+1 queries.
Something along these lines:
class UserAnswer < ActiveRecord::Base
scope :distinct_correct, -> { includes(:multiple_choice_question)
.where(is_correct_answer: true).distinct }
scope :first_attempt_correct, -> { distinct_correct
.first.is_correct_answer }
end
class User < ActiveRecord::Base
def good_guess_count
#correct_on_first_attempt = #user.user_answers.distinct_correct.first_attempt_correct.count
end
end
You'll need to ensure that .first is actually getting their first attempt, probably by sorting by id or created_at.
As an aside, if you track the attempt number explicitly in UserAnswer, you can really tighten this up:
class UserAnswer < ActiveRecord::Base
scope :correct, -> { where(is_correct_answer: true) }
scope :first_attempt, -> { where(attempt: 1) }
end
class User < ActiveRecord::Base
def lucky_guess_count
#correct_on_first_attempt = #user.user_answers.includes(:multiple_choice_question)
.correct.first_attempt.count
end
end
If you don't have an attempt number in your schema, you could .order and .group to get something similar. But...it seems that some of your project requirements depend on that sequence number, so I'd recommend adding it if you don't have it already.
ps. For fighting N+1 queries, use gem bullet. It is on-point.

Rails query: get all parent records based on latest child records

An order has_many order_events.
An order_event is a record of the state of the order, e.g., pending_approval, confirmed, etc.
Changes to an order are tracked by creating order_events.
I usually get the current state of an order by doing something like: order.order_events.last.try(:state).
I would like a query to get all of the orders with a given current state, e.g., all orders where the current state is cancelable.
I initially had a scope in the OrderEvent model:
scope :cancelable, -> { where('state = ? OR state = ?', 'pending_approval', 'pending_confirmation') }
and then a scope in the Order model:
scope :with_dates_and_current_state_cancelable, -> { with_dates.joins(:order_events).merge(OrderEvent.cancelable) }
and simply used the latter for other purposes.
The problem here is that it returns all orders that are currently or have in the past satisfied the condition.
What is the best way to get all of the orders that currently satisfy the condition?
I ended up using a query like this:
scope :with_dates_and_current_state_cancelable, -> {
with_dates
.joins(:order_events)
.where('order_events.created_at = (SELECT MAX(order_events.created_at) FROM order_events WHERE order_events.order_id = orders.id)')
.where('order_events.state = ? OR order_events.state = ?', 'pending_approval', 'pending_confirmation')
.group('orders.id')
}
A bit hard to read, but it seems to work.
A classic solution here would be to use Rails enum.
Add this to your order model:
class Order
enum status: [ :pending_approval, :confirmed, etc.. ]
...
end
The status can be changed by doing the following:
# order.update! status: 0
order.pending_approval!
order.pending_approval? # => true
order.status # => "pending_approval"
No need for the order_events model.
To query all the orders that are pending approval:
Order.where(status: :pending_approval)
Edit:
Alternate solution when order_event has necessary columns.
Add a column to the order_event called archived which can either be set to 1 or 0. Set the default scope in the order_event model to this:
default_cope where(:archived => 0)
Assuming 0 is not archived.
Now, when you create a new order event set the old event to 1.
old_event = OrderEvent.find(params[:order_id])
old_event.update(archived: 1)
new_event = OrderEvent.create(...archived: 0)
Whenever you query for pending review like so:
OrderEvent.where(:status => pending_approval)
Only events that are not archived will be shown.
I think I figured out a query that might work. I didn't turn it in to ActiveRecord methods, but here it is:
SELECT t.order_id
FROM
(SELECT MAX(created_at) AS created, order_id
FROM order_events
GROUP BY order_id) as t
INNER JOIN order_events
ON t.order_id = order_events.order_id AND
t.created = order_events.created_at
WHERE order_events.state = 'whatever_state_you_want'

Use Ruby's select method on a Rails relation and update it

I have an ActiveRecord relation of a user's previous "votes"...
#previous_votes = current_user.votes
I need to filter these down to votes only on the current "challenge", so Ruby's select method seemed like the best way to do that...
#previous_votes = current_user.votes.select { |v| v.entry.challenge_id == Entry.find(params[:entry_id]).challenge_id }
But I also need to update the attributes of these records, and the select method turns my relation into an array which can't be updated or saved!
#previous_votes.update_all :ignore => false
# ...
# undefined method `update_all' for #<Array:0x007fed7949a0c0>
How can I filter down my relation like the select method is doing, but not lose the ability to update/save it the items with ActiveRecord?
Poking around the Google it seems like named_scope's appear in all the answers for similar questions, but I can't figure out it they can specifically accomplish what I'm after.
The problem is that select is not an SQL method. It fetches all records and filters them on the Ruby side. Here is a simplified example:
votes = Vote.scoped
votes.select{ |v| v.active? }
# SQL: select * from votes
# Ruby: all.select{ |v| v.active? }
Since update_all is an SQL method you can't use it on a Ruby array. You can stick to performing all operations in Ruby or move some (all) of them into SQL.
votes = Vote.scoped
votes.select{ |v| v.active? }
# N SQL operations (N - number of votes)
votes.each{ |vote| vote.update_attribute :ignore, false }
# or in 1 SQL operation
Vote.where(id: votes.map(&:id)).update_all(ignore: false)
If you don't actually use fetched votes it would be faster to perform the whole select & update on SQL side:
Vote.where(active: true).update_all(ignore: false)
While the previous examples work fine with your select, this one requires you to rewrite it in terms of SQL. If you have set up all relationships in Rails models you can do it roughly like this:
entry = Entry.find(params[:entry_id])
current_user.votes.joins(:challenges).merge(entry.challenge.votes)
# requires following associations:
# Challenge.has_many :votes
# User.has_many :votes
# Vote.has_many :challenges
And Rails will construct the appropriate SQL for you. But you can always fall back to writing the SQL by hand if something doesn't work.
Use collection_select instead of select. collection_select is specifically built on top of select to return ActiveRecord objects and not an array of strings like you get with select.
#previous_votes = current_user.votes.collection_select { |v| v.entry.challenge_id == Entry.find(params[:entry_id]).challenge_id }
This should return #previous_votes as an array of objects
EDIT: Updating this post with another suggested way to return those AR objects in an array
#previous_votes = current_user.votes.collect {|v| records.detect { v.entry.challenge_id == Entry.find(params[:entry_id]).challenge_id}}
A nice approach this is to use scopes. In your case, you can set this up the scope as follows:
class Vote < ActiveRecord::Base
scope :for_challenge, lambda do |challenge_id|
joins(:entry).where("entry.challenge_id = ?", challenge_id)
end
end
Then your code for getting current votes will look like:
challenge_id = Entry.find(params[:entry_id]).challenge_id
#previous_votes = current_user.votes.for_challenge(challenge_id)
I believe you can do something like:
#entry = Entry.find(params[:entry_id])
#previous_votes = Vote.joins(:entry).where(entries: { id: #entry.id, challenge_id: #entry.challenge_id })

Select the complement of a set

I am using Rails 3.0. I have two tables: Listings and Offers. A Listing has-many Offers. An offer can have accepted be true or false.
I want to select every Listing that does not have an Offer with accepted being true. I tried
Listing.joins(:offers).where('offers.accepted' => false)
However, since a Listing can have many Offers, this selects every listing that has non-accepted Offers, even if there is an accepted Offer for that Listing.
In case that isn't clear, what I want is the complement of the set:
Listing.joins(:offers).where('offers.accepted' => true)
My current temporary solution is to grab all of them and then do a filter on the array, like so:
class Listing < ActiveRecord::Base
...
def self.open
Listing.all.find_all {|l| l.open? }
end
def open?
!offers.exists?(:accepted => true)
end
end
I would prefer if the solution ran the filtering on the database side.
The first thing that comes to mind is to do essentially the same thing you're doing now, but in the database.
scope :accepted, lambda {
joins(:offers).where('offers.accepted' => true)
}
scope :open, lambda {
# take your accepted scope, but just use it to get at the "accepted" ids
relation = accepted.select("listings.id")
# then use select values to get at those initial ids
ids = connection.select_values(relation.to_sql)
# exclude the "accepted" records, or return an unchanged scope if there are none
ids.empty? ? scoped : where(arel_table[:id].not_in(ids))
}
I'm sure this could be done more cleanly using an outer join and grouping, but it's not coming to me immediately :-)

Rails has_many association count child rows

What is the "rails way" to efficiently grab all rows of a parent table along with a count of the number of children each row has?
I don't want to use counter_cache as I want to run these counts based on some time conditions.
The cliche blog example:
Table of articles. Each article has 0 or more comments.
I want to be able to pull how many comments each article has in the past hour, day, week.
However, ideally I don't want to iterate over the list and make separate sql calls for each article nor do I want to use :include to prefetch all of the data and process it on the app server.
I want to run one SQL statement and get one result set with all the info.
I know I can hard code out the full SQL, and maybe could use a .find and just set the :joins, :group, and :conditions parameters... BUT I am wondering if there is a "better" way... aka "The Rails Way"
This activerecord call should do what you want:
Article.find(:all, :select => 'articles.*, count(posts.id) as post_count',
:joins => 'left outer join posts on posts.article_id = articles.id',
:group => 'articles.id'
)
This will return a list of article objects, each of which has the method post_count on it that contains the number of posts on the article as a string.
The method executes sql similar to the following:
SELECT articles.*, count(posts.id) AS post_count
FROM `articles`
LEFT OUTER JOIN posts ON posts.article_id = articles.id
GROUP BY articles.id
If you're curious, this is a sample of the MySQL results you might see from running such a query:
+----+----------------+------------+
| id | text | post_count |
+----+----------------+------------+
| 1 | TEXT TEXT TEXT | 1 |
| 2 | TEXT TEXT TEXT | 3 |
| 3 | TEXT TEXT TEXT | 0 |
+----+----------------+------------+
Rails 3 Version
For Rails 3, you'd be looking at something like this:
Article.select("articles.*, count(comments.id) AS comments_count")
.joins("LEFT OUTER JOIN comments ON comments.article_id = articles.id")
.group("articles.id")
Thanks to Gdeglin for the Rails 2 version.
Rails 5 Version
Since Rails 5 there is left_outer_joins so you can simplify to:
Article.select("articles.*, count(comments.id) AS comments_count")
.left_outer_joins(:comments)
.group("articles.id")
And because you were asking about the Rails Way: There isn't a way to simplify/railsify this more with ActiveRecord.
From a SQL perspective, this looks trivial - Just write up a new query.
From a Rails perspective, The values you mention are computed values. So if you use find_by_sql, the Model class would not know about the computed fields and hence would return the computed values as strings even if you manage to translate the query into Rails speak. See linked question below.
The general drift (from the responses I got to that question) was to have a separate class be responsible for the rollup / computing the desired values.
How to get rails to return SUM(columnName) attributes with right datatype instead of a string?
A simple way that I used to solve this problem was
In my model I did:
class Article < ActiveRecord::Base
has_many :posts
def count_posts
Post.where(:article_id => self.id).count
end
end
Now, you can use for example:
Articles.first.count_posts
Im not sure if it can be more efficient way, But its a solution and in my opinion more elegant than the others.
I made this work this way:
def show
section = Section.find(params[:id])
students = Student.where( :section_id => section.id ).count
render json: {status: 'SUCCESS', section: students},status: :ok
end
In this I had 2 models Section and Student. So I have to count the number of students who matches a particular id of section.

Resources