Is it possible to call model method in where clause? - ruby-on-rails

I have two models
class Project
has_one: user
end
class User
# Attributes
# active: Boolean
# under_18: Boolean
def can_work?
active? && under_18 == false
end
end
The logic for can_work?
if active is true and under_18 is false then they can work
I want to do something like this but it's not possible
Project.all.joins(:user).where('users.can_work? = ?', false)
Essentially what I'm looking for is to find all users who can't work
I know I can use Scope, but copying the logic that I specified above in scope is confusing.
Here's the scenario that I'm looking for
active | under_18
------------------
T T = F
T F = T
F T = F
F F = F
Thanks

As per Sergio Tulentsev's answer, you can't do it. But if you want it DRY so badly you can use scopes.
Scope definition alt. 1:
scope :can_work, -> { where active: true, under_18: false }
scope :cant_work, -> { where.not id: User.can_work.pluck(:id) }
Scope definition alt. 2:
scope :can_work, ->(t=true) { where(active: t).where.not(under_18: t) }
New can_work? method
def can_work?
User.can_work.pluck(:id).include?(id)
end
Now you can call:
Project.joins(:user).merge(User.cant_work)
Or
Project.joins(:user).merge(User.can_work(false))
PS. Good luck with the speed.

Try
Project.joins(:users).where(user: {under_18: <Bool>, active: <Bool>})
Insert the Boolean values (true or false) that you want in place of <Bool>s above.
Source
Side note
If users are workers then I suggest changing names in your schema to look like:
workers
id
project_id
under_18
active
1
1
TRUE
TRUE
...
...
...
...
projects
id
title
1
"lorem"
...
...
So the associations would be
class Project
has_one :worker
end
class Worker
belongs_to :project
end
And the query would then look like
Project.joins(:workers).where(worker: {under_18: <Bool>, active: <Bool>})
I think it makes more sense from a naming standpoint.
That is since users are oftentimes regarded as the owners in association relationships (i.e. a project would belong to a user, and a user would have many projects). Just a quick side suggestion.

Answer: Scope
Scopes are custom queries defined in Rails model inside method named scope
A scope can have two parameters, the name of the scope and lambda which contains the query.
Something like:
class User < ApplicationRecord
scope :is_active, -> { where(active: true) }
scope :under_18, -> { where(under_18: true) }
end
Scope returns ActiveRecord::Relation object. So scopes can be chained with multiple scopes like,
User.is_active.under_18
The scope is nothing but a method same as class methods. The only difference is that you get a guarantee of getting ActiveRecord::Relation as an output of scope. This helps you to write specific code and helps to reduce errors in the code.
Default Scope:
You can also add a default scope on any model which is implicitly applied to every query made on the respective model.
Be cautious while using this though as this might cause unpredictable results.
For example:
class User < ApplicationRecord
default_scope :is_active, -> { where(active: true) }
scope :under_18, -> { where(under_18: true) }
end
In the above case, User.first will return the first user who is active.
Benefits of Scope
Testable - The separate scopes are more testable now as they now follow the Single Responsibility Principle
Readable
DRY - Combining multiple scopes allows you not to repeat the code

Related

Complicated scope involving multiple models

I am trying to create a scope that would provide me with all clients that made a purchase after they've :transitioned_to_maintenance (a term we use internally).
My models and scopes are organized as follows:
class Client < ApplicationRecord
has_many :program_transitions
has_many :purchases
scope :transitioned_to_maintenance, -> { where(id: ProgramTransition.to_maintenance.pluck(:client_id)) }
scope :has_purchases, -> { where(id: Purchase.pluck(:client_id)) }
end
class ProgramTransition < ApplicationRecord
belongs_to :client, required: true
scope :to_fm, -> { where(new_status: "full maintenance") }
scope :to_lm, -> { where(new_status: "limited maintenance") }
scope :to_maintenance, -> { to_fm.or(to_lm)}
end
class Purchase < ActiveRecord::Base
belongs_to :client
end
I could accomplish what I am trying to do by looping through the clients and selected those that meet my criteria, but I was hoping it'd be possible through a scope. Here it is as a model level function:
def self.purchased_after_maintenance
clients = Client.transitioned_to_maintenance.has_purchases
final = []
clients.each do |client|
min_date = client.program_transitions.to_maintenance.first.created_at
final << client if client.purchases.last.created_at >= ? min_date
end
end
Is this possible to do with a scope and without looping through all clients?
A couple of things before addressing the main question. Don't mean to be annoying and not address the main question first, but I thought this could be helpful.
Instead of scope :transitioned_to_maintenance, -> { where(id: ProgramTransition.to_maintenance.pluck(:client_id)) }, I would do this (see below). merge is really powerful and something we have been using a lot lately at our company.
scope :transitioned_to_maintenance, -> { joins(:program_transitions).merge(ProgramTransition.to_maintenance) }
Instead of doing scope :has_purchases, -> { where(id: Purchase.pluck(:client_id)) }, I would just do scope :has_purchases, -> { joins(:purchases) }, as the joins on purchases will only return clients that have associated purchases.
Regarding the main question, I can't think of a good way to do this with scopes, but it's probably possible. If I were you I would ask your data team what the SQL would look like for that query, and then try to figure out the active record needed to create it. However, if you stick with your way, here is a slight refactor I would recommend. Using select instead of each and eager loading to avoid n+1 queries.
def self.purchased_after_maintenance
Client.joins(:purchases, :program_transitions).includes(:purchases, :program_transitions).transitioned_to_maintenance.select do |client|
transition_date = client.program_transitions.to_maintenance.first.created_at
client.purchases.last.created_at >= transition_date
end
end
Hope this helps, and sorry I don't have the answer with the scopes.

Call boolean instance method as a 'scope'

I have a model
Email
and an instance method
def sent_to_user?(user)
self.who_to == user
end
where who_to is another instance method doing some complicated stuff to check.
There's a lot more stuff going on in the background there so I can't easily turn it into an activerecord query.
I want to do something like:
scope :sent_to_user, -> (user) { sent_to_user?(user)}
#user.emails.sent_to_user
and return a only those emails that return true for 'sent_to_user?'
Have tried
scope :sent_to_user, -> (user) { if sent_to_user?(user)}
....etc.
Not quite sure how to build that scope / class method
You can't (at least shouldn't) use scopes this way. Scopes are for returning ActiveRecord relations onto which additional scopes can be chained. If you want to use a scope, you should produce the necessary SQL to perform the filtering in the database.
If you want to filter results in Ruby and return an array, you should use a class-level method, not a scope:
class Email < ActiveRecord::Base
def self.sent_to_user(user)
select { |record| record.sent_to_user?(user) }
end
end
You should write who_to in ActiveRecord logic, like
scope :sent_to_user, -> (user) { joins(:recipient)where(recipient_id: user.id)}
Assuming that user has_many :emails you can do something like:
class User < ActiveRecord::Base
has_many :emails
# ...
def emails_sent_to_user
emails.select { |e| e.sent_to_user?(self) }
end
end
Funnily enough, I worked out a method to do the checking which is a bit of a hack but works in this case, might be useful to somebody.
The issue with the accepted solution, though it's certainly the only way to do it based on the restrictions I described, is that performance can be seriously sluggish.
I'm now using:
scope :sent_to_user, -> (user) {"json_email -> 'to' ILIKE ?", "%#{user.email)}%"}
Where "json_email" is an email object parsed into json (it's the way we store them in the db). This cut out the need to use the Mail gem and increased performance dramatically.

ActiveRecord query with includes that avoids default scope on relations

Conceptually I have 2 models, one with a default scope
class Model
default_scope where: "is_acitve = 1"
end
class SpecialUser
has_many :model
## 1
def model
Model.unscoped { super }
end
end
I'm trying to have a few select places that override the default scope on Model - many other users of model exist, and they all should only see active Model's. Only a handful of special cases should have access to the inactive ones.
With method #1, I can handle this case
s = SpecialUser.find_by_id x
s.model # <-- works for even is_active =0 cases.
But if I attempt something like below (for performance):
s = SpecialUser.includes(:model).where("id = 5")
the default scope is injected into the query.
Any way of avoiding the default scope even when using includes ?
What you want is unscoped, which will remove the default_scope when querying:
s = SpecialUser.unscoped.includes(:model).where("id = 5")

Using OR with queries in a scope

In Rails3 I have:
Class Teacher
# active :boolean
has_and_belongs_to_many :subjects
Class Subject
# active :boolean
has_and_belongs_to_many :teachers
I am trying to construct a Teacher scope that returns all Teachers that are active or are associated with a Subject that is active.
These scopes work individually, but how to combine them as a single scope with an OR?
scope :active_teachers, where(active: true)
scope :more_active_teachers, joins(:subjects).where(:subjects => {active: true})
I've tried this without success:
scope :active_teachers, where(active: true).or(joins(:subjects)
.where(:subjects => {active: true}))
UPDATE:
I thought I had a solution, but this no longer lazy loads, hits the database twice and — most importantly — returns an array rather than an AR object!
scope :active_teachers, where(active: true) |
joins(:subjects).where(:subjects => {active: true})
You have Squeel to your rescue. More details here.
Using that, you could define something like:
class Teacher
...
scope :active_teachers, joins{subjects}.where {(active == true) | (subjects.active == true)}
...
end
I think the short answer is you can't.
Oring in the code is going to break lazy loading ... really no way around it as you need the database to make the evaluation. ActiveRecord can't make the evaluations on the scopes without executing each subclause individually.
Something like this the following should work:
joins(:subjects).where("subjects.active = true OR teachers.active = true")
Not quite as elegant, but can be wrapped into a method for reuse.
You can solve this by dropping to AREL. See this SO Question for how to do that.
AREL OR Condition
Or from the AREL Source Code README.md. I think (but haven't verified) that this would translate to the following for your particular example.
teachers.where(teachers[:active].eq(true).or(subjects[:active].eq(true)))
Good Luck!
There's a rails pull request for this (https://github.com/rails/rails/pull/9052), but in the meantime, some one has created a monkey patch that you can include in your initializers that will allow you to do this and still give you an ActiveRecord::Relation:
https://gist.github.com/j-mcnally/250eaaceef234dd8971b
With that, you'll be able to OR your scopes like this
Teacher.active_teachers.or.more_active_teachers
or write a new scope
scope :combined_scopes, active_teachers.or.more_active_teachers

Scope for multiple models

I have several objects that all have an approved field.
What would be the best way to implement a scope to use across all models?
For example, I have a sighting object and a comment object. They both have to be approved by an admin before being availalbe to the public.
So how can I create a scope that returns comment.approved as well as sighting.approved respectively without repeating it on each model? Is this where concerns comes into play?
While just declaring a scope in each model is fine if you just want the scoping functionality. Using an ActiveSupport::Concern will give you the ability to add additional methods as well if that's something you think is going to happen. Here's an example:
# /app/models/concerns/approved.rb
module Approved
extend ActiveSupport::Concern
included do
default_scope { where(approved: false) }
scope :approved, -> { where(approved: true) }
end
def unapprove
update_attribute :approved, false
end
end
class Sighting < ActiveRecord::Base
include Approved
end
class Comment < ActiveRecord::Base
include Approved
end
Then you can make calls like Sighting.approved and Comment.approved to get the appropriate list of approved records. You also get the unapprove method and can do something like Comment.approved.first.unapprove.
In this example, I've also included default_scope which will mean that calls like Sighting.all or Comment.all will return only unapproved items. I included this just as an example, it may not be applicable to your implementation.
Although I've noticed the scope pulled from the concerns needs to be the last scope when concatenating scopes. I'm not quite sure why.
Comment.existing.approved
When I tried it as:
Comment.approved.existing
It failed silently.
And I take that back. I was migrating older code and using scopes with conditions instead of lambdas. When I replaced :conditions the scope order no longer mattered.
scope :existing, -> { where("real = 1") }
replaced
scope :existing, :conditions => "real = 1"

Resources