Rails model scope with multiple optional associations - ruby-on-rails

I have a model Deployment, it has following relations:
belongs_to :user
belongs_to :user_group, optional: true
I want to be able to define scope for easier filtering. I can easily filter by user, something like this:
scope :by_owner, -> (ow) { joins(:user).where("users.name like ?", "%#{ow}%") }
But what I really want to use user_group first if association exists and if it doesn't, then fall back on user. Logic would work like this:
If this deployment belongs to at least one user_group
scope :by_owner, -> (ow) { joins(:user_group).where("user_groups.name like ?", "%#{ow}%") }
else
scope :by_owner, -> (ow) { joins(:user).where("users.name like ?", "%#{ow}%") }
end
I am guessing that I have to achieve it somehow with an SQL query, but I can't seem to figure it out. Any suggestions?
Thanks!
Update
I got a bit closer with the following query:
scope :by_owner, -> (ow) { joins(:user, :user_group).where("(users.display_name like ?) OR (user_groups.name like ?)", "%#{ow}%", "%#{ow}%") }
But it's still not it because 1st of all the records that don't have user_groups association are ignored in the response and also it searches by user even if user_group is defined. Still looking for suggestions if there are any.

Related

In Rails5, how do I return the union of two scopes or combine them into one?

I'm struggling to either return the union of two scopes or combine them into one in my Rails app. The outcome I'm looking for is to retrieve all orders that satisfy either of the scopes: Order.with_buyer_like or the Order.with_customer_like. That is I'm looking for the union of the sets. Importantly, I need these to be returned as an ActiveRecord::Relation, rather than an array. Otherwise, I'd just do
results = Order.with_buyer_like + Order.with_customer_like
I've also tried to use the "or" operator but get an error:
Order.with_seller_like('pete').or(Order.with_customer_like('pete'))
ArgumentError: Relation passed to #or must be structurally compatible. Incompatible values: [:joins]
I've also tried combining the scopes into one, but I can't quite get it to work.
Here's my setup:
class Company
has_many :stores
end
class Store
belongs_to :company
end
class Order
belongs_to :buying_store, class_name: 'Store', foreign_key: 'buying_store_id', required: true
belongs_to :selling_store, class_name: 'Store', foreign_key: 'selling_store_id', required: true
scope :with_buyer_like, ->(search_term) { joins(buying_store: [:company], :customer).where(['stores.name LIKE ? OR companies.name LIKE ?', "%#{search_term.gsub(/ /, '_').downcase}%", "%#{search_term.gsub(/ /, '_').downcase}%"]) }
scope :with_customer_like, ->(search_term) { joins(:customer).where(['first_name LIKE ? OR last_name LIKE ? OR mobile_number LIKE ? OR email LIKE ?', "%#{search_term.gsub(/ /, '_').downcase}%", "%#{search_term.gsub(/ /, '_').downcase}%", "%#{search_term.gsub(/ /, '_').downcase}%", "%#{search_term.gsub(/ /, '_').downcase}%"] ) }
end
This blog has good explanation of issue you are facing.
To summarize here, you have to make sure the joins, includes, select (in short structure of AR data to be fetched) is consistent between both the scopes.
This syntax is the way to go as long as you make sure both the scopes have same joins.
Order.with_seller_like('pete').or(Order.with_customer_like('pete'))
To take first step, check if this works(not recommended though):
Order.where(id: Order.with_seller_like('pete')).or(Order.where(id: Order.with_customer_like('pete')))
I would recommend to move away from joins and use sub-query style of querying if you are looking for only Order data in output.

Rails Ancestry Gem: how to write scope to match all parents

Using the Rails ancestry gem ,
what is the best way to write a scope on the User model to find all records which have children / which are parents?
class User < ActiveRecords::Base
has_ancestry
def is_manager?
has_children
end
scope :is_manager, -> { ... ? ... }
end
try this, I think that is the best way
scope :is_manager, -> {where(id: User.pluck(:ancestry).compact.map { |e| e.split('/') }.flatten.uniq)}
it select only ancestry field from the database, remove nil, split it and take the number, flatten it, and make it uniq, then select all User based on it.
also check this question
Along the same lines as icemelt's answer..
this does the same, but instead of loading all the records from the database into memory, it only selects the unique ancestries and processes them - that should make a bit of a difference in terms of memory impact
scope :is_manager, -> { where(id: User.select(:ancestry).distinct.pluck(:ancestry).compact.map { |x| x.split('/') }.flatten.uniq) }

Rails ActiveRecord model scope with joins on has_many associations

I’m currently setting up a scope in my Rails model to be used by ActiveAdmin. The scope I want to build should find every Job that has a survey_date in the past, with a Job.survey present, and no Job.quotes present.
Here is an abbreviated version of my Job model:
has_many :quotes
has_many :surveys
scope :awaiting_quote, lambda { joins(:surveys, :quotes).where('survey_date < :current_time AND surveys.id IS NOT NULL AND quotes.id IS NULL', { current_time: Time.current }) }
How should I change my scope so that it correctly finds the revelant Job records?
Update for Rails 5
As mad_raz mentions, in Rails 5.0+, you can use left_outer_joins:
scope :awaiting_quote, -> { joins(:surveys).
left_outer_joins(:quotes).
where('survey_date < :current_time', { current_time: Time.current }).
where('quotes.id IS NULL')
}
However, you must still provide a where('quotes.id IS NULL') check to only return those records that have not yet received a quote. See https://stackoverflow.com/a/16598900/1951290 for a great visual representation of outer joins.
It still probably makes the most sense to split these into two separate scopes.
Rails 4
You can create left outer joins using joins, you just have to be a bit more explicit.
scope :awaiting_quote, -> { joins(:surveys).
joins('LEFT OUTER JOIN quotes ON quotes.job_id = jobs.id').
where('survey_date < :current_time', { current_time: Time.current }).
where('quotes.id IS NULL')
}
You don't need surveys.id IS NOT NULL since a successful inner join will not include nil ids.
It probably makes more sense to split these into two separate scopes, :has_survey and :without_quote, which can then be combined into a method.
def self.awaiting_quote
Job.has_survey.without_quote
end
Rails 5 introduced left_outer_joins method that can be used
scope :awaiting_quote, -> { joins(:surveys).left_outer_joins(:quotes).where('yada yada') }

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.

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.

Resources