Rails scope without using SQL - ruby-on-rails

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.

Related

ActiveRecord select with OR and exclusive limit

I have the need to query the database and retrieve the last 10 objects that are either active or declined. We use the following:
User.where(status: [:active, :declined]).limit(10)
Now we need to get the last 10 of each status (total of 20 users)
I've tried the following:
User.where(status: :active).limit(10).or(User.where(status: : declined).limit(10))
# SELECT "users".* FROM "users" WHERE ("users"."status" = $1 OR "users"."status" = $2) LIMIT $3
This does the same as the previous query and returns only 10 users, of mixed statuses.
How can I get the last 10 active users and the last 10 declined users with a single query?
I'm not sure that SQL allows doing what you want. First thing I would try would be to use a subquery, something like this:
class User < ApplicationRecord
scope :active, -> { where status: :active }
scope :declined, -> { where status: :declined }
scope :last_active_or_declined, -> {
where(id: active.limit(10).pluck(:id))
.or(where(id: declined.limit(10).pluck(:id))
}
end
Then somewhere else you could just do
User.last_active_or_declined()
What this does is to perform 2 different subqueries asking separately for each of the group of users and then getting the ones in the propper group ids. I would say you could even forget about the pluck(:id) parts since ActiveRecord is smart enough to add the proper select clause to your SQL, but I'm not 100% sure and I don't have any Rails project at hand where I can try this.
limit is not a permitted value for #or relationship. If you check the Rails code, the Error raised come from here:
def or!(other) # :nodoc:
incompatible_values = structurally_incompatible_values_for_or(other)
unless incompatible_values.empty?
raise ArgumentError, "Relation passed to #or must be structurally compatible. Incompatible values: #{incompatible_values}"
end
# more code
end
You can check which methods are restricted further down in the code here:
STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope, :references]
def structurally_incompatible_values_for_or(other)
STRUCTURAL_OR_METHODS.reject do |method|
get_value(method) == other.get_value(method)
end
end
You can see in the Relation class here that limit is restricted:
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
:reverse_order, :distinct, :create_with, :skip_query_cache,
:skip_preloading]
So you will have to resort to raw SQL I'm afraid
I don't think you can do it with a single query, but you can do it with two queries, get the record ids, and then build a query using those record ids.
It's not ideal but as you're just plucking ids the impact isn't too bad.
user_ids = User.where(status: :active).limit(10).pluck(:id) + User.where(status: :declined).limit(10).pluck(id)
users = User.where(id: user_ids)
I think you can use UNION. Install active_record_union and replace or with union:
User.where(status: :active).limit(10).union(User.where(status: :declined).limit(10))

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'

Creating a record not found through default_scope

Consider the model
class Product < ActiveRecord::Base
scope :queued, lambda { where(queued: true) }
scope :unqueued, lambda { where(queued: false) }
default_scope unqueued
end
Product.first yields the SQL query
SELECT "products".* FROM "products" WHERE "products"."queued" = 'f'
LIMIT 1
Now what if I want to create a record that is not "true to the default scope?" Like so:
Product.queued.create!
The product is in fact created, but ActiveRecord yields an error since it tries to find the product by it's id AND the default scope:
ActiveRecord::RecordNotFound:
Couldn't find Product with id=15 [WHERE "products"."queued" = 'f']
Is there a workaround for this? I need to make sure that the product I create is queued. A simple workaround would be
p = Product.create
p.update_column(:queued, true)
It seems like the wrong answer to another problem though, or perhaps it is the right answer. Are there alternatives?
Thanks for your time.
The best solution would be to not use default_scope. default_scope should only be used when you always need to apply the scope when searching for records. So, if you ever need to find a record where queued is true, then you should not be using default_scope.
One other way to get past default_scope is to use the unscoped method, i.e.:
Product.unscoped.queued
But, in general, if you need to use ActiveRecord to find queued Products, I would recommend removing your default_scope.

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

Resources