Rails query: get all parent records based on latest child records - ruby-on-rails

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'

Related

Creating ActiveRecord scope with multiple conditionals

I have a Rails application with a number of Products, some of which are associated with an Issue model. Some of these products have an issue_id (so an Issue has_many products) and some do not. The products without an issue ID are a new addition I'm working on.
I previously had a named scope so that I can list products using Product.published, which looks like this:
scope :published, -> {
joins(:issue).reorder('products.created_at ASC, products.number ASC')
.where('products.status = ?', Product.statuses[:available])
.where('issues.status = ?', Issue.statuses[:published])
}
The result of this is that I can find only products that are associated with a published issue (think magazine issue).
I'm now adding products that will not be associated with a particular issue but will still have a draft/available state. The above scope does not find these products, as it looks for an issue_id that does not exist.
I thought I could modify the scope like this, adding the OR issue_id IS NULL part in the last line:
scope :published, -> {
joins(:issue).reorder('products.created_at ASC, products.number ASC')
.where('products.status = ?', Product.statuses[:available])
.where('issues.status = ? OR issue_id IS NULL', Issue.statuses[:published])
}
But this doesn't work. I still only get 'available' products associated with a 'published' issue. The products without an issue_id are not included in the returned collection.
(There is a window in which a product will be set to available before its associated issue is published, so for these situations I do need to check the status of both records.)
Here's the SQL generated by the above (wrapped for readability):
pry(main)> Product.published.to_sql
=> "SELECT `products`.* FROM `products` INNER JOIN `issues` ON `issues`.`id` =
`products`.`issue_id` WHERE (products.status = 1) AND (issues.status = 1 OR
issue_id IS NULL) ORDER BY products.created_at ASC, products.number ASC"
I've already created a Product class method that takes an argument as an alternate approach but doesn't work in all cases because I'm often looking up a product based on the ID without knowing in advance whether there's an Issue association or not (eg, for the product's show view).
Also, Product.published is nice and concise and the alternative is to load all published products (eg, Product.where(:status => :published)) and then iterate through to remove those associated with a not-yet-published issue in a second operation.
I feel like there's something I'm not quite grasping about doing more complex queries within a scope. My ideal outcome is a modified scope that can return available products, both with and without an issue, and without supplying an argument.
Is this possible, or should I resign myself to finding an alternate approach now that I'm adding these unassociated products?
The problem is that you are using joins(:issue). That method does an INNER JOIN between products and issues tables and discards all the products that doesn't have an issue_id. Maybe you could use LEFT JOIN so you can keep all the products regardless they have an issue.
scope :published, -> {
joins('LEFT JOIN issues ON issues.id = products.issue_id')
.select('products.*')
.reorder('products.created_at ASC, products.number ASC')
.where('products.status = ?', Product.statuses[:available])
.where('issues.status = ? OR products.issue_id IS NULL', Issue.statuses[:published])
}

Rails: eager load related model with condition at third model

I am very stuck on this problem:
I have next models (it's all from Redmine):
Issue (has_many :journals)
Journal (has_many :details, :class_name => "JournalDetails)
JournalDetails
I want fetch last changing of a state of a issue: date saved in Journal model but for filter only changing state I must join to JournalDetails.
Issue.eager_load(:journals).eager_load(journals: :details)
It's work but journals have all items not only changes of state - and how I can filter only changing of state without additional query I don't know
My next try:
Issue.eager_load({:journals => :details}).where("journal_details.prop_key = 'status_id'")
In this case I don't get issues which has not changes of state.
This should work:
Issue.joins(journals: :details).where(details: { prop_key: "status_id" })
Or, you can merge the scope from the details model:
class JournalDetails
scope :for_prop_key, -> (status_id) { where(prop_key: status_id )}
end
Issue.joins(journals: :details).merge(
JournalDetails.for_prop_key("status_id")
)
To fetch all issues, which have a non null prop_key in journal details:
class JournalDetails
scope :with_non_null_prop_key, -> { where("prop_key IS NOT NULL") }
end
Issue.joins(journals: :details).merge(
JournalDetails.with_non_null_prop_key("status_id")
)

Rails scope without using SQL

I'm trying to set up a scoped query where I can find customers that have paid (or partially paid) an invoice.
However, the value I want to use in the scope isn't actually in the database, it's a method.
For example:
Bidder.rb
class Bidder < ActiveRecord::Base
...
scope :unpaid, where(payment_status: 'unpaid')
...
def payment_status
"paid" if whatever
"partial" if whatever
"unpaid" if whatever
end
...
end
When I try to use:
#auction.bidders.unpaid
I see this:
SQLite3::SQLException: no such column: bidders.payment_status: SELECT COUNT(*) FROM "bidders" WHERE "bidders"."auction_id" = 7 AND "bidders"."payment_status" = 'unpaid'
What do I need to change to get something like this to work? Should I even be using scopes?
Also, how can I change that scope to search for both 'unpaid' and 'partial' values?
Thanks
You could use scope like this:
scope :unpaid, all.select{ |bidder| bidder.payment_status == 'unpaid' }
This way you will have an array of unpaid bidders, but if you want to chain methods you have to convert that array to a relation like this:
scope :unpaid, where(id: all.select{ |bidder| bidder.payment_status == 'unpaid' }.map(&:id))
See that you are hitting database eagerly, you maybe want to have payment_status as a computed value for performance.

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 })

Condition true for ALL records in join

I'm trying to return records from A where all matching records from B satisfy a condition. At the moment my query returns records from A where there is any record from B that satisfies the condition. Let me put this into a real world scenario.
Post.joins(:categories)
.where(:categories => { :type => "foo" })
This will return Posts that have a category of type "foo", what I want is Posts whose categories are ALL of type "foo"!
Help appreciated!
Using your db/schema.rb as posted in #rubyonrails on IRC something like:
Incident.select("incidents.id").
joins("INNER JOIN category_incidents ON category_incidents.incident_id = incidents.id").
joins("INNER JOIN category_marks ON category_marks.category_id = category_incidents.category_id").
where(:category_marks => { :user_group_id => current_user.user_group_id }).
group("incidents.id").
having("SUM(CASE WHEN category_marks.inc = 1 THEN 1 ELSE 0 END) = count(category_indicents.incident_id)")
would do the trick.
It joins the category_marks for the current_user and checks if the count of records with .inc = 1 equals the count of all joined records.
Do note that this only fetches incident.id
I would add a select to the end of this query to check if all categories have type foo. I would also simplify that check by adding an instance method to the Category model.
Post.joins(:categories).select{|p| p.categories.all?(&:type_foo?)}
Category Model
def type_foo?
type == "foo"
end
ADDITION: This is a bit "hacky" but you could make it a scope this way.
class Post < ActiveRecord::Base
scope :category_type_foo, lambda{
post_ids = Post.all.collect{|p| p.id if p.categories.all?(&:type_foo?).compact
Post.where(id: post_ids) }
end
Have you tried query in the opposite direction? i.e.
Categories.where(type: 'foo').joins(:posts)
I may have misunderstood your question though.
Another alternative is
Post.joins(:classifications).where(type: 'foo')

Resources